Tic Tac Toe/Version Control
LSL Portal | Functions | Events | Types | Operators | Constants | Flow Control | Script Library | Categorized Library | Tutorials |
Version Control
So, let's interrupt our quest for the tic tac toe game and build a little infrastructure.
Here, I've added two scripts: RootNanny and LinkNanny. The RootNanny listens for a /1 update and starts a remote upload of a predetermined list of scripts onto the linked prims.
// Message constants integer MSG_RESET = 0; integer MSG_LINK_QUERY = 10; integer MSG_LINK_REPLY = 11; // Globals to propagate string version = "1.4"; integer pin = 321; // State integer current_link_nr; // Stuff to send out list remote_scripts = [ "TouchDisplay", "LinkNanny" ]; request_next_object_id(integer link_nr) { // Send message, transmitting pin, hoping to get back objectid llMessageLinked(link_nr, MSG_LINK_QUERY, (string)pin, NULL_KEY); // Set timer in case link isn't replying llSetTimerEvent(2); // 2 secs seems generous } default { state_entry() { string name = llGetObjectName(); integer space = llSubStringIndex(name, " v"); if (space < 0) llSetObjectName(name + " v" + version); else llSetObjectName(llDeleteSubString(name, space+2, -1) + version); llListen(1, "", llGetOwner(), "update"); } listen(integer channel, string name, key id, string message) { state uploading; } } state uploading { state_entry() { current_link_nr = llGetNumberOfPrims(); // Check if it's more than one if (1 < current_link_nr) { // avatars sitting on us get added at the end, so subtract... while (llGetAgentSize(llGetLinkKey(current_link_nr))) --current_link_nr; request_next_object_id(current_link_nr); } } link_message(integer from, integer msg_id, string str, key id) { if (from == current_link_nr && msg_id == MSG_LINK_REPLY) { llSetTimerEvent(0); // Cancel timeout integer i; for (i = 0; i < llGetListLength(remote_scripts); ++i) { string script = llList2String(remote_scripts, i); llSay(0, "Uploading '"+script+"' to link nr "+(string)current_link_nr); llRemoteLoadScriptPin(id, script, pin, TRUE, 0); } current_link_nr--; if (1 < current_link_nr) { request_next_object_id(current_link_nr); } else { llSay(0, "Done uploading remote scripts."); state default; } } } timer() { llSay(0, "Failed to receive reply from link nr "+(string)current_link_nr); current_link_nr--; if (1 < current_link_nr) { request_next_object_id(current_link_nr); } else { llSay(0, "Done uploading remote scripts."); state default; } } }
The LinkNannys are there to set the remote upload pin and to reply with their object id:
// Message constants integer MSG_RESET = 0; integer MSG_LINK_QUERY = 10; integer MSG_LINK_REPLY = 11; default { state_entry() { // If we are in the the root prim... if (llGetLinkNumber() < 2) { // ... disable ourselves llSetScriptState(llGetScriptName(), FALSE); llSleep(2); } } link_message(integer from, integer msg_id, string str, key id) { if (msg_id == MSG_LINK_QUERY) { // Set pin given llSetRemoteScriptAccessPin((integer)str); // Tell caller who we are llMessageLinked(from, MSG_LINK_REPLY, "", llGetKey()); } } }
With this structure, we can keep all scripts in the root prim, and the appropriate ones will be replicated to all the remaining prims, greatly simplifying the task of developing those scripts.
For this specific project, this is almost overkill, and could have been done simpler - for example, we don't need to set the pin for every prim, since the pin is an object property, and we don't really need to ask every prim what their object id is, because we can use llGetLinkKey(). But for larger projects, you can use the LinkNannys to return the list of scripts in that prim, and thereby remove the need to keep a mapping of which scripts go where - in this project we just happen to luck out simply because all the linked prims are identical. In most real projects, this is not the case. Also, if you are really paranoid, you would negotiate a separate pin for every upload.
This implementation also illustrates some common idioms and ways to avoid scaling pitfalls as your project gets larger.
Note how we query each link in sequence, the end of every event handler having an llMessageLinked() call to request the next one. This is a common pattern, used to process notecards, email, http requests and much else. Here it is again:
state process { state_entry() { llSetTimerEvent(<some_timeout>); llMessageLinked(...<get_first_object>...); } link_message(...) { // Disable timer llSetTimerEvent(0); // Process data ... // exit if we know we're done if (<end_condition>) state done_processing; // Set timeout again llSetTimerEvent(<some_timeout>); llMessageLinked(...<get_next_object>...); } timer() { // We didn't get a response llSetTimerEvent(0); // Error handling - retries, whatever... ... } }
A naive implementation of this would have simply broadcast the query to all prims and relied on the event queue to stash them and serve them back one by one. This is dangerous as the event queue is of undefined size, and due to the delay involved with running llRemoteLoadScriptPin(), any subsequent replies could easily be flushed out by whatever other events may be traversing your build.
Also note the timeout. Never assume anything is they way you wish it is. In your previous run you could have distributed a script which kills off the LinkNanny, or maybe you just screwed up the LinkNanny" itself... Debugging sucks, so add as many catchers as you can.
Finally, note the code that changes the object name to reflect the current version. In a real product, you would use a separate object to contain an updater which will itself upload the latest version of the scripts onto your build, and having this code in place allows you to see whether the updater was applied.
Two more observations:
The scripts were added as new scripts and not shoehorned into the existing scripts, for two reasons:
- We want to make it easy to reuse the framework for other projects;
- We want to preserve full freedom in the application to change state as needed. Note that this allows us to use states to prevent any incorrect processing of multiple "/1 upload" commands.
Maintaining remote scripts in the root prim is convenient, but we don't want them to actually run there. Therefore we place the following suicide code at the state_entry handler of the default state in the DisplayTouch and LinkNanny scripts:
default { state_entry() { // If we are in the the root prim... if (llGetLinkNumber() < 2) { // ... disable ourselves llSetScriptState(llGetScriptName(), FALSE); // ... and sleep to ensure nothing else gets executed llSleep(2); } } ... }
One slight drawback of the LinkNanny is that we will need to distribute it one initial time by hand, but we're comforted by the fact that it will be the last time we need to actually open the contents of the linked prims.