A long time ago, in a blog far, far away…well, it has been a long time but maybe not that long. Or maybe it has been a long time. This post has been sitting out in the queue for weeks now and I bet Ener is tired of seeing it :).
Continuing on a theme beginning with “Mono Bytecode Sharing“, followed by “Does your script need socks“, and then by “Put object description to good use”, in this post, we’ll look at using notecards for configuring options for a script.
To summarize the theme again, being able to configure scripts for multiple purposes has at least two advantages.
Notecard processing in SL is really fairly easy. The event oriented nature of LSL scripting combined with the asynchronous implementation of reading notecard lines can be confusing at first. But after understanding what is happening, most people deal with it fairly well. And after you build one model, just copy it over and over again.
So why are we doing this?
I think using the socks or object description methods are easier but have limitations if you want to support multiple configuration options. Using notecards provides a nearly unlimited opportunity for configuration.
While not an absolute requirement, generally you probably want to see if your notecard exists before trying to read it. You can choose to make the notecard required or optional but still it’s good to know whether it is there or not.
llGetInventoryType(NOTECARD)
The llGetInventoryType with a string variable naming the notecard is how you can tell if there is a notecard with the expected name in the objects inventory. The variable used to define the notecard name will likely need to be a global variable (defined at the top of the script outside of any subroutines or states). It needs to be global so its value is available inside events. More about that later. The function actually returns either the type of the item with the requested name or an INVENTORY_NONE flag if there is no object by that name.
A sidenote on my variable naming conventions. I obtained this from some LSL style suggestions somewhere. For a variable that I treat as a constant, I capitalize all letters as in NOTECARD. For a variable that I’ve defined such that it is global but is not a constant, I begin the variable with a small “g” followed by a capital letter that begins the descriptive name. For a variable that is not global but only lives inside a subroutine or event, I leave as all lowercase.
if (llGetInventoryType(NOTECARD) == INVENTORY_NOTECARD)
Assuming the string NOTECARD contains the desired notecard name, this if statement will be true if the notecard exists.
The llGetNotecardLine function is the next important function.
gQuery = llGetNotecardLine(NOTECARD, gLine);
In this example, NOTECARD is still the string that contains the name of the notecard. The gLine variable is the actual notecard line to read and will likely need to be a global variable. This should always start as a zero and be incremented with each read. If you don’t increment it, you will continue to read the same line over and over. The gQuery variable returns a unique key that can be used to identify that data is being returned from the function. And yes, this will likely need to be a global variable.
This is where LSL differs from most high level languages you might know. You’d normally expect data to be available to the next sequential statement following llGetNotecardLine. Remember I mentioned event driven and asynchronous before? In LSL, data is returned via the dataserver event. LSL returns lots of stuff via the dataserver event and that’s why we need to save the key from llGetNotecardLine as in the example that uses the gQuery variable to hold the returned key, above.
The key point here is don’t write your script expecting that the read has happened after the llGetNotecardLine function is called. For the most part, the rest of the action will take place in the dataserver event until you are completely done reading the notecard. The example supplied can serve as a model and demonstrates a technique to use to help you logically separate the processing of the notecard data from the main function of your script.
Now to the dataserver event. The first thing you need to do is check that the triggered event is the one that gives you the notecard line. You do this by checking that the key supplied to you in the event matches the key you saved from the last llGetNotecardLine.
dataserver(key query_id, string data)
At entry to dataserver, the variable passed as the first parameter is the key that goes with the event and the second variable is the data. In the case of a dataserver event for llGetNotecardLine, the data will be a string that contains the notecard line or an indication that there are no more lines.
if (query_id == gQuery)
Continuing with our example, this if statement will be true if this dataserver event goes with the last llGetNotecardLine.
if (data == EOF)
We need to have a way to know that there is no more notecard data. If the string variable contains the EOF indicator (End of File), there is no more data. So in this example, the if statement is true if there is no more data. So the code that goes with this statement being true needs to do whatever you want to happen after you’ve read all of the notecard. Sometimes this can be a little tricky to think about as you are inside the dataserver event and what are you gonna do? If you just have something simple to do maybe you can just call a subroutine or even just include the code inline following the if statement. I mentioned earlier that the example would illustrate a way to logically disconnect the notecard processing from all subsequent activity. I often do this by changing states to a new state.
state begin;
In this example, I switch to a state I imaginatively call begin. Begin as in “now I will begin to use my data”. But we’re getting a little ahead and we haven’t parsed the data yet. So, if the check for EOF is not true, we do the else part of the if and in the else part, we parse the data.
If you have some complicated parsing to do, it might be cleanest to put it into a subroutine. In many cases, that would make the script easier to read and understand. In this case we are just going to do a little bit of parsing so we won’t call a subroutine to do it.
Parsing a set of commands can be fairly complicated. Out in the real world, there are two very old programs that originated on Unix that can be used to create parsers for complicated command input. Lex and YACC. The name of Lex comes from “lexical analyzer” and YACC means “Yet Another Compiler Compiler”. Lex and YACC are great tools and they or their derivatives are still used today. But aside from the fact that Lex and YACC generate C code and not LSL, they would likely be way to heavy for the vast majority of parsing requirements needed for LSL. So, we’ll approach parsing in a little bit simpler way.
Our grammer will be pretty simple.
keyword=keyvalue;
This will allow a lot of flexibility but require very simple parsing.
Before getting serious with the parsing, we want to do a couple of things. Check to see if the line from the notecard actually contains any data and check to see if it is a comment. In this example we are using a “#” in the first position to indicated the line is a comment. I always include the capability to have comments in the notecard as it is a good way to self-document the syntax of the options you want to support.
Check for an empty line.
if ( llStringTrim(data, STRING_TRIM) != “” )
If this statement is true, the line is not blank and we continue to process it. We used llStringTrim to remove any leading and trailing blanks from the line in case it only has blanks, otherwise the compare with a null string wouldn’t work. Since we used the llStringTrim function inside the conditional without doing an assignment, the string will be left unchanged for further processing.
Check for a comment line.
if (llGetSubString(data, 0, 0) != “#”)
If this statement is true, then the line does not contain the selected comment character in the first position and we continue. The llGetSubString takes three parameters. The string to work on, the starting position where to begin extracting characters and the ending position. The positions are relative to zero and a “0, 0″ means take the 1st character from the string.
To assist in taking apart a notecard line with data on it, we’ll use llSubStringIndex to look for the “=” and “;” in our grammer. llParseString2List is another function that can be used to easily parse a line of data into a list of words or strings. But in this example, we’ll stick with llSubStringIndex.
i = llSubStringIndex(data, “=”);
j = llSubStringIndex(data, “;”);
These two functions will look for our two separators and set their respective index variables, i and j, to the relative locations in the string. The function returns a -1 if nothing is found in the string.
if (i == -1 || j == -1)
This if statement will validate that we found both of our separators. If either of the variables is set to -1, then we write out an error message and proceed to get the next data card. If we found both of our separators, then we continue parsing this line.
keyword = llGetSubString(data, 0, i – 1);
This will set the keyword variable to the string found looking from the beginning of the line to the first “=”, not including the “=”. Remember i is the location in the string of “=”. We’re just doing simple parsing here and not directly checking for every possible mistake someone might make. For instance, these checks don’t directly check to see that the “=” comes before the “;”. We just look to see if they are both in the string and get the substrings based on their locations. This could mean that we get garbage for our variables. But if so, the following checks will fail anyway.
You might want to do some things to make the grammer a little less stringent. For instance, we assume there are no spaces at the beginning of the line and before the “=”. To allow for the possibility that there are some spaces and permit things to work anyway, you can include a function to drop extra spaces before and after the key word. Also, if the case isn’t important, you can translate things to lower case and then code assuming everything is in lower case.
keyword = llStringTrim(llToLower(keyword), STRING_TRIM);
This will drop leading and trailing spaces from keyword and translate the string to lower case.
keyvalue = llGetSubString(data, i + 1, j – 1);
Next we set the variable keyvalue, to the substring that starts at the first position after the “=” and ends at the position prior to the “;”.
keyvalue = llStringTrim(keyvalue, STRING_TRIM);
In this case, we just remove leading and trailing spaces. You could also translate to lower case but there may be times when saving values, that you want to keep the case as specified. For instance, if you are supplying the text of a message to write, you likely would want to supply a mixed case value.
So at this point, we’ve collected a key word in the keyword variable and a value in the keyvalue variable. So, we’re ready to start checking and setting options.
We have a beginning if statement followed by as many else if statements as we need to compare all of our desired keywords. For any matched key word, the required function is performed with the corresponding value.
gSeconds = (float)keyvalue;
We won’t go through each of these compares but I will point out one thing. The example above is where we take something read in as a string and convert it to a floating point value.
There is an important point to note regarding converting strings to other types. If you perform an explicit type cast such as (float) and the string isn’t really a numeric string, LSL just quietly gives you a zero. And, there are no functions to check what kind of data the string is composed of. So, if you wanted to validate that someone entered a string of digits and not a string of alpha characters, you’re likely left with constructing a messy function to check that the contents of the string is all digits or digits and a period. Since I mostly create stuff for myself or friends, I just let this one go and consider it not worth the effort. If you’re creating something to sell, you’ll have to consider just how much you think you need to protect your customers from making mistakes.
If we fall through the if statement and all of the else if statements to the else statement, then we’ve encountered a key word/key value pair that we don’t know how to process.
llOwnerSay(“Configuration notecard contains unknown keyword at line ” + (string)gLine + “.\n Keyword found is ” + keyword + “\nCorrect the error and reset.”);
To help someone pinpoint their error, it’s a good idea to give them as much information as possible. This example will print the keyword and value along with the relative line in the notecard. Remember that notecard lines are relative to zero. If this is going to confuse someone, you could add one to the notecard line variable to help someone out.
After completely processing this notecard line, we need a new one.
++gLine;
gQuery = llGetNotecardLine(NOTECARD, gLine);
The llGetNotecard function reads the line number you tell it to. So we first need to increment the line number with ++gLine and then call the llGetNotecardLine function. In our example, we’re now at the end of the dataserver event and have nothing else to do. So the script will wait for the next event to occur which will likely be another notecard line. As mentioned earlier, this process will repeat until the EOF indicator is returned and we switch to the begin state.
As mentioned earlier, I use this state switch at the completion of processing the notecard, as a way to cleanly divide the logic between dealing with the notecard and everything else that happens that depends on the notecard. It is not required to use different states.
Three key words are processed in this example. They are seconds, debug and text. The seconds key word is only used to demonstrate the conversion of numeric data. The debug key word doesn’t do much either except print some additional messages if set to yes. The text key word is used to set the floating text for the object.
The example script also contains two events that I generally include in any script that processes notecards. The first is the on_rez event used to detect the object being rezzed from inventory and to reset the script when that happens. The other event is the changed event used to detect an inventory change. The changed event is coded to cause a script reset and a reprocessing of the notecard. This is to support rereading the notecard if you’ve made changes to it.
And because there are two states in this example, both the on_rez and changed events are coded in each state. This is a downside of having multiple states. Event handlers are only effective in the state they are coded in. So any event that you want to handle in any state has to be coded in all states.
Now for the example code. As usual, this code example will work for both Second Life and OpenSim. An example notecard follows the code.
// Config Demo - Notecard
//
// Sample code to demonstrate using a notecard to set configuration
// parameters.
//
// Micheil Merlin - 05/18/2010
string NOTECARD = "Config"; // Configuration notecard.
key gQuery; // Notecard read key.
integer gLine = 0; // Notecard line to read.
integer gError = FALSE; // Error status.
float gSeconds = 0.0; // Seconds value from notecard.
integer gDebug = FALSE; // Debug flag from notecard.
string gText = ""; // Hover text from notecard.
default
{
state_entry()
{
llOwnerSay("Initializing...");
// Check to see if a notecard with teh specified name is in the
// object inventory.
if (llGetInventoryType(NOTECARD) == INVENTORY_NOTECARD)
{
// If the notecard exists, get the first line. Save the key
// so we can use it in the dataserver event to identify that
// the event is for this notecard read.
gQuery = llGetNotecardLine(NOTECARD, gLine);
} else
{
// If no notecard exists, just switch to the begin state.
// There are no configuration parameters to change.
state begin;
}
}
// When object is rezzed, reset the script.
on_rez(integer num)
{
llResetScript(); // Reset the script.
}
// Use the changed event to see if the inventory has changed. We can use
// this to detect that the notecard has been changed and reset the script
// so the notecard can be reprocessed. There is no way to determine what
// in the inventory has changed without doing some sort of processing on the
// inventory. So, the script will be reset regardless of what changed in the
// inventory.
changed(integer change)
{
// If inventory has changed, assume script reset is needed. Many other
// things can trigger the change event but in this case, we are only
// looking for changed inventory.
if (change == CHANGED_INVENTORY)
{
llResetScript(); // Reset the script.
}
}
// Dataserver event to process notecard data.
dataserver(key query_id, string data)
{
integer i; // Temp index.
integer j; // Temp index.
string keyword; // Configuration keyword.
string keyvalue; // Configuration value.
// The dataserver event is used for many things. To make sure we process
// the correct data, check that the key is from the last notecard read
// request. In this sample code, the dataserver event should not be
// triggered for any other purpose than to present the notecard data.
// But it is still a good coding practice to check to make sure
// the event is what you think it is.
if (query_id == gQuery)
{
// If 'End of File' is returned, then there is no more data. Switch
// to the begin state.
if (data == EOF)
{
state begin;
}
// Process the notecard data.
else
{
// Compress out blanks and make sure the notecard line is not empty.
if ( llStringTrim(data, STRING_TRIM) != "" )
{
// A notecard line with a '#' in the first position is taken to be a
// comment and will not be further processed.
if (llGetSubString(data, 0, 0) != "#")
{
// If line is not a comment, get keyword and value.
// The format of the data we are looking for is...
// keyward=keyvalue;
i = llSubStringIndex(data, "="); // Look for keyword/keyvalue separator.
j = llSubStringIndex(data, ";"); // Look for end of line marker.
if (i == -1 || j == -1)
{
// Check for 'keyword=value;' syntax.
llOwnerSay("Configuration notecard contains invalid syntax at list " +
(string)gLine + ". Data line follows.\n" + data +
"\nCorrect the error and reset.");
gError = TRUE; // Set error condition.
} else
{
// Extract the keyword and keyvalue strings.
keyword = llGetSubString(data, 0, i - 1);
// Translate keyword to lower case to reduce problems from typos and
// also remove leading and trailing blanks.
keyword = llStringTrim(llToLower(keyword), STRING_TRIM);
keyvalue = llGetSubString(data, i + 1, j - 1);
// Only remove leading and trailing blanks from keyvalue. Assume
// the existing case is what is intended. There may be specific
// uses of keyvalue where it still may be desireable to force
// everything to one case.
keyvalue = llStringTrim(keyvalue, STRING_TRIM);
// Process keywords in config notecard.
if (keyword == "seconds") // seconds keyword.
{
// We don't really have anything to do with the seconds value.
// We are just demonstrating translating a string from
// notecard to a float.
gSeconds = (float)keyvalue; // Translate value to float and save.
} else if (keyword == "debug") // debug keyword.
{
// Here is a case where we want to force a compare to lower case
// as we are using keyvalue as on on/off switch.
// Also, we assume that any other reply than yes means no. If you
// really want to make sure someone doesn't just typo the yes and
// it is taken as no, you'd want to check for yes or no and then
// indicate an error if something else was encountered.
if (llToLower(keyvalue) == "yes")
{
gDebug = TRUE; // Set debug flag.
}
} else if (keyword == "text") // text keyword.
{
gText = keyvalue; // Set text value.
} else
{
llOwnerSay(
"Configuration notecard contains unknown keyword at line "
+ (string)gLine + ".\n Keyword found is " + keyword +
"\nCorrect the error and reset.");
gError = TRUE; // Set error condition.
}
}
}
}
// Request the next notecard line.
++gLine;
gQuery = llGetNotecardLine(NOTECARD, gLine);
}
}
}
}
state begin
{ // Initialization completed.
state_entry()
{
// Make some use of the debug flag. Normally, you'd sprinkle this through your
// script printing values where ever you wanted to check that things were as
// expected.
if (gDebug)
{
llOwnerSay("Debug mode is on.");
llOwnerSay("Seconds=" + (string)gSeconds + ".");
llOwnerSay("Value of text is " + gText + ".");
}
// Check to see if the error condidition was set during configuration. We
// may not want to do anything else if a configuration error occurred.
if (gError == FALSE)
{
llOwnerSay("Ready to run.");
llSetText(gText, <1.0, 1.0, 1.0>, 1.0); // Set text as requested.
} else
{
llOwnerSay("Not ready to run.");
}
}
// Since events are specfiic to a state, we need to duplicate the on_rez event in this
// state also.
on_rez(integer num)
{
llResetScript(); // Reset script.
}
// Since events are specific to a state, we need to duplicate the changed event in this
// state also.
changed(integer change)
{
// If inventory has changed, assume script reset is needed.
if (change == CHANGED_INVENTORY)
{
llResetScript(); // Reset script.
}
}
// Use the touch event to report what is in the settings.
touch_start(integer num)
{
string debugstr; // Text string for debug flag.
if (gDebug) // Translate debug flag to yes or no.
{
debugstr="yes";
} else
{
debugstr="no";
}
llOwnerSay("Debug=" + debugstr + ".");
llOwnerSay("Seconds=" + (string)gSeconds + ".");
llOwnerSay("Text=" + gText + ".");
}
}
The following is the text for a notecard to go along with this coding example. Paste this text into a notecard named Config to work with this example.
# Config notecard for Config Demo script.
# debug may be set to yes or no.
debug=yes;
# This text will be used to set the floating text of the object.
text=Config Demo;
# Supply a numeric value for seconds.
seconds=5;









awesome post! and i actually understand what you are teaching! thank you! =)
a universal script or a selection of a few “go to” universal scripts is a fast way to do professional development in-world
and it’s phenomenal that you tested and worked this so that it functions in both Second Life and OpenSim – that is the sign of a true expert scripter! nicely done!
Ener Hax
7 Jun 10 at 10:58 pm
Do you remember taking me through the notecard reader step by step about two years ago? That still ranks with me as the best explanation of a technical device that I’ve ever had.
Good job. :-)
Nickola Martynov
8 Jun 10 at 6:37 am
Thanks Ener.
And yes, Nikki, I remember. :) I tend to do it that way because I’ve read too many technical “how to’s” that had big massive gaps of information that I had to discover on my own. You actually learn it pretty well when that happens, but it takes so much time.
Micheil Merlin
8 Jun 10 at 5:23 pm
I find notecard reading to be horrifyingly slow, which is why I usually put my configs in super small scripts that pass the config to the main script via link messages. Instantaneous config loads are awfully nice, even if editing is slightly more complicated.
Winter Seale
10 Jun 10 at 5:31 am
Excellent commenting within your code! This type of commenting makes it realistic to be able to crack open the code a year down the road and still know what it does!
Nicely done.
David Miller
11 Jun 10 at 12:28 am
David
Thanks. The commenting there was fairly detailed to assist in learning. I’m not quite that verbose with normal scripts. But I probably do comment more than most for exactly the reason you state. When I began life long ago…lol…I found that it was hard to remember why I did some little particular thing a year later when I picked it up again and comments help with that. Also, I wanted someone to understand if they had to come behind me and make changes.
Micheil Merlin
11 Jun 10 at 4:41 pm
So, Winter Seale, do you use the notecard script such that it doesn’t normally get reset and already has all the configuration loaded? Ready to hand off when asked for it without reading the notecard again? I can see how that would be faster.
Micheil Merlin
11 Jun 10 at 4:45 pm
[...] Scripting Tips: Configure by notecard at iliveisl [...]
Scary Costume Ideas- Halloween Treats - Buy Purchase Halloween Online
17 Jul 10 at 12:31 am
[...] contains a few parameters including the UUIDs or keys of the remote objects. A post last year, Scripting Tips: Configure by notecard, describes what you have to do to process a [...]
Scripting Tips: Remote object update at i live in science land
7 May 11 at 11:32 am