Start Line Sources
From SecondSailing
The startline consists of 6 scripts. If the web loader is used (Madaket Hotlaps / SFL), a 7th script is edit.
They are
Contents |
Colour changer
just responsible for changing the colour in all segments. Must be present in all startline segments
// This block of functions reads a notecard
// Variables
integer currentLine;
key requestInFlight;
string currentName;
list allLines;
// Start reading a notecard line. Only call this when this notecard exists
readfirstNCLine(string name) {
currentLine=0;
currentName=name;
allLines=[];
// llSay(0, "Reading the notecard "+ name + " here");
requestInFlight=llGetNotecardLine(currentName, currentLine);
}
// Call this with any data server event
integer readNextNCLine(key eventId, string line) {
// Return value semantics
// -1. Not my event. Process further
// 0. Notecard line read. More to come. Do nothing (just info)
// 1. Last notecard line read. Take further actions
if (eventId!=requestInFlight)
return -1;
if (line==EOF)
return 1;
// llSay(0, "Line seen is "+line + "size=" + (string)llStringLength(line));
currentLine++;
requestInFlight=llGetNotecardLine(currentName, currentLine);
// llSay(0, "Requested "+ currentName + " line " + (string)currentLine);
line=llStringTrim(line, STRING_TRIM);
if (line=="")
return 0;
if (llGetSubString(line, 0, 0)!="#")
allLines=allLines+line;
return 0;
}
list getNCLineList() {
return allLines;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
integer channel; // The channel to listen for start, reset and miscellanous other commands
parseKeyValue(list parts) {
if (llGetListLength(parts)!=2) {
llSay(0, "malformed line:" + llList2String(parts, 0));
}
string keystring=llList2String(parts, 0);
keystring=llStringTrim(keystring, STRING_TRIM);
if (keystring=="channel") {
channel=llList2Integer(parts, 1);
return;
}
// llSay(0, "Unknown keyword: \""+keystring+"\"");
}
parseSettings() {
list settings=getNCLineList();
integer length=llGetListLength(settings);
integer i;
// llSay(0, "Settings has "+(string)length+" members");
for (i=0; i<length; i++) {
string line=llList2String(settings, i);
list parts=llParseString2List(line, ["="], []);
parseKeyValue(parts);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
integer start_at; // The point in time (as unix time) when the start is planned / has been. -1 if not active
integer announce_time=0; // The last counter that got shouted out as "XXX seconds to the start"
integer last_tick; // The last second (Unix time) the counter worked
vector COLOR_RED = <1,0,0>;
vector COLOR_ORANGE = <1,0.5,0>;
vector COLOR_GREEN = <0,1,0>;
vector COLOR_BLUE = <0,0,1>;
vector COLOR_YELLOW = <1,1,0>;
vector COLOR_WHITE = <1,1,1>;
SetColour(integer sec_to_start) // Update the line's appearance and shouts out verbal messages
// Note, that secs_to_start is negative when the race is started
{
if (announce_time==sec_to_start)
return;
announce_time=sec_to_start;
if (sec_to_start>60) {
llSetLinkColor(LINK_SET,COLOR_YELLOW,ALL_SIDES);
}
else if (sec_to_start>10) {
llSetLinkColor(LINK_SET,COLOR_ORANGE,ALL_SIDES);
}
else if (sec_to_start>0) {
llSetLinkColor(LINK_SET,COLOR_RED,ALL_SIDES);
}
else {
llSetLinkColor(LINK_SET,COLOR_GREEN,ALL_SIDES);
llSetTimerEvent(0.0);
}
}
parseCommands(list in) {
string cmd=llList2String(in,0);
//check the first entry in the list to identify the command
if (cmd=="start_at") {
start_at=llList2Integer(in, 1);
llSetTimerEvent(0.5);
return;
}
if (cmd == "reset") {
llResetScript();
return;
}
}
///////////////////////////////////////////////////////////////////////////////////////////
default {
state_entry() {
if (llGetInventoryType("settings")!=INVENTORY_NOTECARD) {
llSay(0, "Unable to find the settings notecard. Please provide one.");
return;
}
readfirstNCLine("settings");
}
dataserver( key queryid, string data ) {
integer ncResult;
ncResult=readNextNCLine(queryid, data);
if (ncResult==1) { // 1=We are done.
// targets=getNCLineList();
parseSettings();
state running;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
state running
{
state_entry()
{
start_at=-1;
llSetText("",ZERO_VECTOR,0);
SetColour(120);
llListen(channel, "", NULL_KEY, "");
}
listen(integer channel, string name, key id, string message)
{
//put this into a list so we can parse it
list in=llParseString2List(llToLower(message),[" "],[]);
parseCommands(in);
}
link_message(integer sender_num,integer num,string message,key id) {
list in=llParseString2List(message,[" "],[]);
parseCommands(in);
}
timer()
{
if (start_at==-1)
return;
integer now=llGetUnixTime();
if (now==last_tick)
return;
last_tick=now;
integer diff=now-start_at; // negative during prestart
SetColour(-diff);
}
on_rez(integer whocares)
{
llResetScript();
}
}
detector
The detector script detects individual boats (more exact, prims). It filters them to avoid messages for each prim, detects the owner and sends out a message for each boat crossing.
// This block of functions reads a notecard
// Variables
integer currentLine;
key requestInFlight;
string currentName;
list allLines;
// Start reading a notecard line. Only call this when this notecard exists
readfirstNCLine(string name) {
currentLine=0;
currentName=name;
allLines=[];
// llSay(0, "Reading the notecard "+ name + " here");
requestInFlight=llGetNotecardLine(currentName, currentLine);
}
// Call this with any data server event
integer readNextNCLine(key eventId, string line) {
// Return value semantics
// -1. Not my event. Process further
// 0. Notecard line read. More to come. Do nothing (just info)
// 1. Last notecard line read. Take further actions
if (eventId!=requestInFlight)
return -1;
if (line==EOF)
return 1;
// llSay(0, "Line seen is "+line + "size=" + (string)llStringLength(line));
currentLine++;
requestInFlight=llGetNotecardLine(currentName, currentLine);
// llSay(0, "Requested "+ currentName + " line " + (string)currentLine);
line=llStringTrim(line, STRING_TRIM);
if (line=="")
return 0;
if (llGetSubString(line, 0, 0)!="#")
allLines=allLines+line;
return 0;
}
list getNCLineList() {
return allLines;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
integer channel; // The channel to listen for start, reset and miscellanous other commands
integer needsConfig=0;
parseKeyValue(list parts) {
if (llGetListLength(parts)!=2) {
llSay(0, "malformed line:" + llList2String(parts, 0));
}
string keystring=llList2String(parts, 0);
keystring=llStringTrim(keystring, STRING_TRIM);
if (keystring=="channel") {
channel=llList2Integer(parts, 1);
return;
}
// llSay(0, llGetScriptName()+": Unknown keyword: \""+keystring+"\"");
}
parseSettings() {
list settings=getNCLineList();
integer length=llGetListLength(settings);
integer i;
// llSay(0, "Settings has "+(string)length+" members");
for (i=0; i<length; i++) {
string line=llList2String(settings, i);
list parts=llParseString2List(line, ["="], []);
parseKeyValue(parts);
}
}
/////////////////////////////////////////////////////////////////////////////////////
integer start_at=-1;
integer raceNumLaps=1;
list allowed_owners;
string non_authorized=" is not allowed to use the line!";
integer DEBUG=0;
integer pingChannel=2905797;
integer ways=1;
dbgSay(string text) {
if (DEBUG & 4) llSay(0, llGetScriptName()+":"+text);
}
dbgShout(string text) {
if (DEBUG) llSay(0, text);
else llShout(0, text);
}
sendOut(string text) {
llMessageLinked(LINK_THIS, 0, text, NULL_KEY);
llShout(channel, text);
}
list mute1=[];
list mute2=[];
list mute3=[];
list mute4=[];
integer isMuted(key id) {
list idl=[id];
integer idx;
idx=llListFindList(mute1, idl);
if (idx>=0)
return 1;
mute1=mute1+idl; // Put the boat into the muted list now. This "re-mutes" it so the 4 secs mute interval is retriggered
idx=llListFindList(mute2, idl);
if (idx>=0)
return 1;
idx=llListFindList(mute3, idl);
if (idx>=0)
return 1;
idx=llListFindList(mute4, idl);
if (idx>=0)
return 1;
return 0;
}
//checks if boat is crossing in right direction (in direction of root local y-axis)
integer fwdCrossing(vector v) {
if (ways!=1)
return TRUE;
rotation rot=llGetRot();
vector left=llRot2Left(rot);
float projection=left*v;
if (left*v>0) return TRUE;
else return FALSE;
}
parseMessage(list in) {
string cmd=llList2String(in,0);
if (cmd == "start_at") {
start_at = llList2Integer(in,1);
llSetStatus(STATUS_PHANTOM, 0);
llVolumeDetect(TRUE);
dbgSay("Detected START command with a point in time of "+(string)start_at);
return;
}
if (cmd == "reset") {
llResetScript();
return;
}
if (cmd == "ways") {
ways=llList2Integer(in, 1);
dbgSay("Ways set to "+(string)ways);
}
if (cmd=="+debug") {
integer value=llList2Integer(in, 1);
DEBUG=value;
dbgSay("Debug on");
}
if (cmd=="config_end") {
if (needsConfig==1) // Only decrement if we saw a previous config_start. This is meant to make sure we see a complete set of configs
needsConfig=0;
}
if (cmd=="config_start") {
if (needsConfig==2) // Only decrement if this is the first start. This will prepare proper downcounting for the the following config_end.
needsConfig=1;
}
}
handle_collision(integer num, integer diff, integer now) {
dbgSay("Handling collision for object "+(string)num);
string name=llDetectedName(num);
key ownerKey=llDetectedOwner(num);
string ownerName=llKey2Name(ownerKey);
integer idStrIdx=llSubStringIndex(name,"#")+1;//takos that are registerd will have "#nn" in their name
string strType=llGetSubString(name, 0, idStrIdx-2);
string strId =llGetSubString(name, idStrIdx, 100000);
if (idStrIdx<0) { // Silently ignore
dbgSay("Collision with non-id obect owned by "+ownerName);
return;
}
if (idStrIdx==0) {
dbgSay("No id (hash) found");
return;
}
if (isMuted(ownerKey)) {
dbgSay("Muted-->ignore");
return;
}
if (llGetListLength(allowed_owners)>0) {
dbgSay("Allowed_owners is "+llDumpList2String(allowed_owners, "|"));
integer listIndex=llListFindList(allowed_owners, [ownerName]);
if (listIndex<0) {
dbgShout(ownerName+non_authorized);
return;
}
} else
dbgSay("allowed_owners is empty");
vector vel=llDetectedVel(num);
if (!fwdCrossing(vel)) {
dbgShout("wrong way!");
return;
}
if (diff<0) { //over early
dbgShout(ownerName+" is over early! Go around and recross!");
sendOut("early "+(string)ownerKey+"|"+(string)diff+"|"+(string)now);
return;
}
sendOut("crossing "+(string)ownerKey+"|"+(string)diff+"|"+(string)now+"|"+strType+"|"+strId);
}
/////////////////////////////////////////////////////////////////////////////////////
default {
state_entry() {
if (llGetInventoryType("settings")!=INVENTORY_NOTECARD) {
llSay(0, "Unable to find the settings notecard. Please provide one.");
return;
}
readfirstNCLine("settings");
}
dataserver( key queryid, string data ) {
integer ncResult;
ncResult=readNextNCLine(queryid, data);
if (ncResult==1) { // 1=We are done.
// targets=getNCLineList();
parseSettings();
state running;
}
}
}
/////////////////////////////////////////////////////////////////////////////////////
state running {
state_entry() {
llVolumeDetect(FALSE);
llSetStatus(STATUS_PHANTOM, 1);
llListen(channel,"",NULL_KEY,"");
start_at=-1;
DEBUG=0;
// DEBUG=4;
llSetTimerEvent(1.0);
needsConfig=2; // Note the 3, it's downcounted with config_start and config_end to make sure both aer seen
}
listen(integer channel,string name,key id,string message) {
dbgSay("Detector saw this on the chat:"+message);
list in=llParseString2List(message,[" "],[]);
string cmd=llList2String(in,0);
parseMessage(in);
}
collision_start(integer num) {
dbgSay("Collision detected");
if (start_at==-1) {
dbgSay("I'm not on. leaving premature");
return;
}
integer now=llGetUnixTime(); // Get the time as early as possible
integer diff=now-start_at;
dbgSay("Collision detected with "+(string)num+" objects at "+(string)diff);
integer i;
for (i=0;i<num;++i) {
handle_collision(i, diff, now);
} // for
}
link_message(integer sender_num,integer num,string message,key id) {
dbgSay("saw this on the link :"+message+" from sender "+(string)num);
if (num==0) {
list in=llParseString2List(message,[" "],[]);
parseMessage(in);
return;
}
if (num==10) {
// dbgSay<"Received allowed_list");
allowed_owners=llParseString2List(message,["|"],[]);
non_authorized=llList2String(allowed_owners, 0);
dbgSay("The text to send to the unauthorized owners is:"+non_authorized);
allowed_owners=llDeleteSubList(allowed_owners, 0, 0);
if (llGetListLength(allowed_owners)==0) {
allowed_owners=["*"];
// This is necessary as an empty list means everybody allowed
}
dbgSay("List is now "+(string)allowed_owners);
}
}
timer() {
mute4=mute3;
mute3=mute2;
mute2=mute1;
mute1=[];
if (needsConfig>=2) {
sendOut("config-request");
}
}
}
Honk
The honk script was meant to be easily replaced by customers. It just receives the "start" event and triggers the "honk".
default
{
link_message(integer sender_num,integer num,string message,key id) {
list in=llParseString2List(message,[" "],[]);
string m=llList2String(in ,0);
if (m=="start_at") {
llPreloadSound("w1hs");
return;
}
if (m=="start_signal") {
llPlaySound("w1hs",1);
return;
}
}
}
The manager's role is to read (most) of the user input and decode it. After decoding related messages are sent and actions triggered.
// This block of functions reads a notecard
// Variables
integer currentLine;
key requestInFlight;
string currentName;
list allLines;
// Start reading a notecard line. Only call this when this notecard exists
readfirstNCLine(string name) {
currentLine=0;
currentName=name;
allLines=[];
// llSay(0, "Reading the notecard "+ name + " here");
requestInFlight=llGetNotecardLine(currentName, currentLine);
}
// Call this with any data server event
integer readNextNCLine(key eventId, string line) {
// Return value semantics
// -1. Not my event. Process further
// 0. Notecard line read. More to come. Do nothing (just info)
// 1. Last notecard line read. Take further actions
if (eventId!=requestInFlight)
return -1;
if (line==EOF)
return 1;
// llSay(0, "Line seen is "+line + "size=" + (string)llStringLength(line));
currentLine++;
requestInFlight=llGetNotecardLine(currentName, currentLine);
// llSay(0, "Requested "+ currentName + " line " + (string)currentLine);
line=llStringTrim(line, STRING_TRIM);
if (line=="")
return 0;
if (llGetSubString(line, 0, 0)!="#")
allLines=allLines+line;
return 0;
}
list getNCLineList() {
return allLines;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
integer channel; // The channel to listen for start, reset and miscellanous other commands
parseKeyValue(list parts) {
if (llGetListLength(parts)!=2) {
llSay(0, "malformed line:" + llList2String(parts, 0));
}
string keystring=llList2String(parts, 0);
keystring=llStringTrim(keystring, STRING_TRIM);
if (keystring=="channel") {
channel=llList2Integer(parts, 1);
return;
}
if (keystring=="ways") {
ways=llList2Integer(parts, 1);
return;
}
if (keystring=="group") {
group=llList2Integer(parts, 1);
return;
}
llSay(0, llGetScriptName()+": Unknown keyword: \""+keystring+"\"");
}
parseSettings() {
list settings=getNCLineList();
integer length=llGetListLength(settings);
integer i;
// llSay(0, "Settings has "+(string)length+" members");
for (i=0; i<length; i++) {
string line=llList2String(settings, i);
list parts=llParseString2List(line, ["="], []);
parseKeyValue(parts);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
integer set_time=120; // The preset time to start
integer start_at; // The point in time (as unix time) when the start is planned / has been. -1 if not active
integer announce_time=0; // The last counter that got shouted out as "XXX seconds to the start"
integer last_tick; // The last second (Unix time) the counter worked
integer started=FALSE;
list cmd_list;
string cmd;
integer listen_handle;
integer ways=1;
integer dbg=0;
integer group=TRUE;
integer locktime=0;
integer continuous=0;
integer lock=FALSE;
key lockID=NULL_KEY;
dbgSay(string text) {
if (dbg & 1) llSay(0, llGetScriptName()+":"+text);
}
DisplayTime(integer sec_to_start) // Update the line's appearance and shouts out verbal messages
// Note, that secs_to_start is negative when the race is started
{
if (announce_time==sec_to_start)
return;
announce_time=sec_to_start;
if (sec_to_start==120) {
llShout(0,"TWO minutes to the start");
return;
}
if (sec_to_start==60) {
llShout(0,"ONE minute to the start");
return;
}
if (sec_to_start==30) {
llShout(0,"30 SECONDS to the start");
return;
}
if (sec_to_start==20) {
llShout(0,"20 SECONDS to the start");
return;
}
if (sec_to_start==15) {
llShout(0,"15 SECONDS to the start");
return;
}
if ( (sec_to_start<=10) && (sec_to_start>0) ) {
llShout(0,(string)sec_to_start+" SECONDS to the start");
return;
}
if (sec_to_start<=0) {
if (!started) {
started=TRUE;
// llPlaySound("w1hs",1);
llShout(0,"RACE STARTED");
sendOut("start_signal");
}
}
}
//convert float time in seconds to "hh:mm:ss" string
integer hr;
integer mn;
integer sc;
string hms;
string sec2hms(integer seconds) {
if (seconds<0) hms="-";
else hms="+";
seconds=llAbs(seconds);
hr=llFloor(seconds/3600.);
mn=llFloor((seconds-hr*3600)/60);
sc=llRound(seconds-mn*60-hr*3600);
if (hr<10) hms+="0"+(string)hr+":";
else hms+=(string)hr+":";
if (mn<10) hms+="0"+(string)mn+":";
else hms+=(string)mn+":";
if (sc<10) hms+="0"+(string)sc;
else hms+=(string)sc;
return hms;
}
sendOut(string text) {
llMessageLinked(LINK_THIS, 0, text, NULL_KEY);
llShout(channel, text);
}
parseDesc() {
// llSay(0, "Parsing Description");
string desc=llGetObjectDesc();
list allOptions=llParseString2List(desc, [","], []);
integer l=llGetListLength(allOptions);
integer i;
for (i=0; i<l; i++) {
list thisOption=llParseString2List(llList2String(allOptions, i), ["="], []);
string option=llStringTrim(llList2String(thisOption, 0), STRING_TRIM);
string value=llList2String(thisOption, 1);
dbgSay("parsing "+option+"="+value);
if (option=="version") {
// ignore
} else if (option=="ways") {
ways=(integer)value;
} else
if (option=="dbg") {
dbg=(integer)value;
} else
if (option=="group") {
group=(integer)value;
// llSay(0, "Group is now :"+ (string)group);
} else
if (option=="locktime") {
locktime=((integer)value)*60; // Time given in minutes, we need seconds
// llSay(0, "locktime is now :"+ (string)locktime);
} else
if (option=="continuous") {
continuous=(integer)value;
} else {
llSay(0, "Unknown option name:"+option);
}
}
}
sendConfig() {
sendOut("config_start");
sendOut("+debug "+(string)dbg);
sendOut("ways "+(string)ways);
sendOut("continuous "+(string)continuous);
sendOut("group "+(string)group);
sendOut("off");
sendOut("config_end");
}
parseInternalCommand(string message) {
dbgSay("Got an internal command of "+message);
if (message=="config-request") {
sendConfig();
return;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
default {
state_entry() {
if (llGetInventoryType("settings")!=INVENTORY_NOTECARD) {
llSay(0, "Unable to find the settings notecard. Please provide one.");
return;
}
readfirstNCLine("settings");
}
dataserver( key queryid, string data ) {
integer ncResult;
ncResult=readNextNCLine(queryid, data);
if (ncResult==1) { // 1=We are done.
// targets=getNCLineList();
parseSettings();
state running;
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
state running
{
state_entry()
{
//llSetObjectName("SLSF Race Start Line");
parseDesc();
llListen(channel, "", NULL_KEY, ""); // Internal command channel
start_at=-1;
sendOut("reset");
llSleep(5);
sendConfig();
llSetText("",ZERO_VECTOR,0);
DisplayTime(set_time);
llSay(0, "Startline is ready");
}
touch_start(integer num_detected)
{
//start listening for commands
listen_handle = llListen(0,"",NULL_KEY,""); // listen for commands
llSay(0,"Say '+help' to display help text.");
parseDesc();
sendOut("+debug "+(string)dbg);
}
listen(integer incoming_channel, string name, key id, string message)
{
if (incoming_channel==channel) {
parseInternalCommand(message);
return;
}
if (group && !llSameGroup(id)) {
llSay(0, "You must wear the right group tag to operate this line");
return;
}
//put this into a list so we can parse it
cmd_list=llParseString2List(llToLower(message),[" "],[]);
cmd=llList2String(cmd_list,0);
//check the first entry in the list to identify the command
if (cmd=="+unlock") {
lockID=NULL_KEY;
llSay(0, "The line is now unlocked");
return;
}
// Check for a lock, first
if ( (lockID!=NULL_KEY) && (lockID!=id) ) {
integer lockedEnd=start_at+locktime;
integer lockedLeft=lockedEnd-llGetUnixTime();
lockedLeft/=60;
lockedLeft+=1;
llSay(0, "This line is locked for another "+(string)lockedLeft+" minutes");
llSay(0, "Use the +unlock command to override the lock if you know what you do");
llSay(0, "But be aware that there might be a reason why the line was initially locked");
return;
}
//put this into a list so we can parse it
cmd_list=llParseString2List(llToLower(message),[" "],[]);
cmd=llList2String(cmd_list,0);
//check the first entry in the list to identify the command
if (cmd=="+time") {
set_time=llList2Integer(cmd_list,1);
llSay(0,"Countdown time: "+sec2hms(-set_time));
DisplayTime(set_time);
return;
}
if (cmd=="+start") {
llListenRemove(listen_handle);
integer now=llGetUnixTime();
start_at=now+set_time;
llSay(0,"The race start timer is running.");
string text="start_at "+(string)start_at;
sendOut(text);
llMessageLinked(LINK_THIS, 9, "racestart|"+(string)start_at, NULL_KEY);
llSetTimerEvent(0.5);
if (locktime!=0) {
llSay(0, "Line is locked!");
lockID=id;
}
return;
}
if (cmd == "+reset") {
llResetScript();
return;
}
if (cmd == "+help") {
// output the help display
llSay(0, "Start line command list:");
llSay(0, "+unlock - unlock a locked line" );
llSay(0, "+time x - set the countdown time to x seconds" );
llSay(0, "+numlaps n - set number of race laps" );
llSay(0, "+minlaptime x - set minimum possible lap time" );
llSay(0, "+start - start the countdown timer running" );
// llSay(0, "+pause - stop the countdown timer momentarily");
llSay(0, "+reset - stop the countdown timer and reset" );
llSay(0, "+help - displays help text" );
llSay(0, "See http://www.secondsailing.org/wiki/index.php/Startline_User_Manual for more information about the start line");
return;
}
}
link_message(integer sender_num, integer num, string str, key id)
{
parseInternalCommand(str);
}
timer()
{
integer now=llGetUnixTime();
if (now==last_tick)
return;
last_tick=now;
integer diff=now-start_at; // negative during prestart
DisplayTime(-diff);
if ( (lockID!=NULL_KEY) && (now > (start_at+locktime)) ) {
lockID=NULL_KEY;
llSay(0, "Line is now unlocked");
}
}
on_rez(integer whocares)
{
integer i=0;
integer number=llGetInventoryNumber(INVENTORY_SCRIPT);
for (i=0; i<number; i++) {
string name=llGetInventoryName(INVENTORY_SCRIPT, i);
if (name!=llGetScriptName())
llResetOtherScript(name);
}
llResetScript();
}
}
Registar
The registar's role is to manage lap times. It keeps track of crossing events and identifies them as start, lap or finish times.
// This block of functions reads a notecard
// Variables
integer currentLine;
key requestInFlight;
string currentName;
list allLines;
integer group=0;
// Start reading a notecard line. Only call this when this notecard exists
readfirstNCLine(string name) {
currentLine=0;
currentName=name;
allLines=[];
// llSay(0, "Reading the notecard "+ name + " here");
requestInFlight=llGetNotecardLine(currentName, currentLine);
}
// Call this with any data server event
integer readNextNCLine(key eventId, string line) {
// Return value semantics
// -1. Not my event. Process further
// 0. Notecard line read. More to come. Do nothing (just info)
// 1. Last notecard line read. Take further actions
if (eventId!=requestInFlight)
return -1;
if (line==EOF)
return 1;
// llSay(0, "Line seen is "+line + "size=" + (string)llStringLength(line));
currentLine++;
requestInFlight=llGetNotecardLine(currentName, currentLine);
// llSay(0, "Requested "+ currentName + " line " + (string)currentLine);
line=llStringTrim(line, STRING_TRIM);
if (line=="")
return 0;
if (llGetSubString(line, 0, 0)!="#")
allLines=allLines+line;
return 0;
}
list getNCLineList() {
return allLines;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
integer channel; // The channel to listen for start, reset and miscellanous other commands
parseKeyValue(list parts) {
if (llGetListLength(parts)!=2) {
llSay(0, "malformed line:" + llList2String(parts, 0));
}
string keystring=llList2String(parts, 0);
keystring=llStringTrim(keystring, STRING_TRIM);
if (keystring=="channel") {
channel=llList2Integer(parts, 1);
return;
}
// llSay(0, "Unknown keyword: \""+keystring+"\"");
}
parseSettings() {
list settings=getNCLineList();
integer length=llGetListLength(settings);
integer i;
// llSay(0, "Settings has "+(string)length+" members");
for (i=0; i<length; i++) {
string line=llList2String(settings, i);
list parts=llParseString2List(line, ["="], []);
parseKeyValue(parts);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This script registers events
// It is triggered by messages from the detector
integer i;
string id;
integer idStrIdx;
integer listIdx;
list lapList=[];
list timeList=[];
list ownerList=[];
list allLapsList=[];
list finishList=[];
list tmp;
string name;
float raceTime=0;
integer continuous=0;
//minTime is used to prevent people from crossing, turning around,
//and recrossing again without going around the course. it should be
//set to a large enough value in case someone accidentally recrosses,
//but not so large that someone who has raced around the course doesn't
//get counted.
float minTime=60.0;
integer lapNum=0; //this is the number of laps in the race
integer raceNumLaps=1;
integer winnerFlag=FALSE;
integer runnerupFlag=FALSE;
key ownerKey;
string ownerName;
integer DEBUG=0;
integer SHOUT=TRUE; // In shout mode, things are shouted, otherwise said
integer pingChannel=2905797;
dbgSay(string text) {
if (DEBUG & 2)
llSay(0, llGetScriptName()+": "+text);
}
shoutOut(string text) {
if (SHOUT)
llShout(0, text);
else
llSay(0, text);
}
//when racer crosses line for the first time (after the start), add place to lists
addRacer(string name, key owner, integer startTimeRel, integer startTimeAbs, string boatType, string raceID) {
lapList+=[0];
timeList+=[startTimeRel];
ownerList+=[owner];
allLapsList+=[llKey2Name(owner),0,startTimeRel];
shoutOut(name+" starts at time "+sec2hms(startTimeRel));
llShout(pingChannel,name+",lock");
llMessageLinked(LINK_THIS, 9, "start|"+name+"|"+(string)startTimeRel+"|"+(string)startTimeAbs+"|"+boatType+"|"+raceID, NULL_KEY);
}
//when racer crosses line for the first time (after the start), add place to lists
removeRacer(key ownerKey) {
dbgSay("Removing racer");
integer listIdx=llListFindList(ownerList,[ownerKey]);
lapList=llDeleteSubList(lapList, listIdx, listIdx);
timeList=llDeleteSubList(timeList, listIdx, listIdx);
ownerList=llDeleteSubList(ownerList, listIdx, listIdx);
listIdx=1;
while (listIdx>=0) {
listIdx=llListFindList(allLapsList, [llKey2Name(ownerKey)]);
if (listIdx>=0) {
allLapsList=llDeleteSubList(allLapsList, listIdx, listIdx+2);
}
}
listIdx=llListFindList(finishList, [ownerKey]);
finishList=llDeleteSubList(finishList, listIdx, listIdx);
dbgSay("lapList="+llDumpList2String(lapList, "|"));
dbgSay("timeList="+llDumpList2String(timeList, "|"));
dbgSay("ownerList="+llDumpList2String(ownerList, "|"));
dbgSay("allLapsList="+llDumpList2String(allLapsList, "|"));
dbgSay("finishList="+llDumpList2String(finishList, "|"));
}
//convert float time in seconds to "hh:mm:ss" string
integer hr;
integer mn;
integer sc;
string hms;
string sec2hms(float seconds) {
if (seconds<0) hms="-";
else hms="+";
seconds=llFabs(seconds);
hr=llFloor(seconds/3600.);
mn=llFloor((seconds-hr*3600)/60);
sc=llRound(seconds-mn*60-hr*3600);
if (hr<10) hms+="0"+(string)hr+":";
else hms+=(string)hr+":";
if (mn<10) hms+="0"+(string)mn+":";
else hms+=(string)mn+":";
if (sc<10) hms+="0"+(string)sc;
else hms+=(string)sc;
return hms;
}
integer isEarly(key onwerKey, integer raceTimeAbs) {
// Functionality still missing
return 0;
}
handle_crossing(key ownerKey, integer raceTimeRel, integer raceTimeAbs, string boatType, string raceID) {
string ownerName=llKey2Name(ownerKey);
listIdx=llListFindList(ownerList,[ownerKey]);
if (listIdx==-1) { //not in list... will add to list if not over early
dbgSay("Didn't find racer "+ownerName+" in list. Adding him/her");
addRacer(ownerName,ownerKey, raceTimeRel, raceTimeAbs, boatType, raceID);
} else { //in the list, so they must have started already
dbgSay("Found racer "+ownerName+" in list. Checking for lap time.");
if (llFabs(llList2Float(timeList,listIdx)-raceTimeRel)>minTime) { //is time dif > min lap time?
dbgSay("Racer "+ownerName+" is over min lap time, checking for lap numbers");
lapNum=llList2Integer(lapList,listIdx);
++lapNum;
if (lapNum<=raceNumLaps) {
lapList=llListReplaceList(lapList,[lapNum],listIdx,listIdx); //update lap number for this racer
timeList=llListReplaceList(timeList,[raceTimeRel],listIdx,listIdx); //update race time for this racer
//add
allLapsList+=[ownerName,lapNum,raceTimeRel];
}
if (lapNum<raceNumLaps) {
shoutOut(ownerName+": "+(string)lapNum+" lap(s) completed at time "+sec2hms(raceTimeRel));
}
if (lapNum==raceNumLaps) {
if (llListFindList(finishList,[ownerKey])==-1) finishList+=ownerKey;
if (!winnerFlag && !continuous) {
shoutOut("Winner: "+ownerName+"! Race time: "+sec2hms(raceTimeRel));
winnerFlag=TRUE;
}
else if (!runnerupFlag && !continuous) {
shoutOut("Runner up: "+ownerName+"! Race time: "+sec2hms(raceTimeRel));
runnerupFlag=TRUE;
}
else {
shoutOut("#"+(string)llGetListLength(finishList)+": "+ownerName+"! Race time: "+sec2hms(raceTimeRel));
}
llShout(pingChannel,ownerName+",unlock");
llMessageLinked(LINK_THIS, 9, "finish|"+ownerName+"|"+(string)raceTimeRel+"|"+(string)raceTimeAbs, NULL_KEY);
if (continuous)
removeRacer(ownerKey);
}
}
}
}
parseMessage(list in) {
dbgSay("Parsing a message of "+llDumpList2String(in," "));
string cmd=llList2String(in,0);
if (cmd=="reset") {
llResetScript();
}
if (cmd=="group") {
group=llList2Integer(in, 1);
return;
}
if (cmd=="start_at") {
lapList=[];
timeList=[];
ownerList=[];
allLapsList=[];
finishList=[];
winnerFlag=FALSE;
runnerupFlag=FALSE;
return;
}
if (cmd=="crossing") {
string rest="";
integer i;
for (i=1; i<llGetListLength(in); i++) {
rest=rest+" "+llList2String(in , i);
}
list restList=llParseString2List(rest, ["|"], []);
key id=(key)llStringTrim(llList2String(restList, 0), STRING_TRIM);
integer start_timeRel=llList2Integer(restList, 1);
integer start_timeAbs=llList2Integer(restList, 2);
string boatType=llList2String(restList, 3);
string raceID=llList2Key(restList, 4);
string racerName=llKey2Name(id);
dbgSay("Received crossing command for "+racerName+" in a "+boatType+" under id "+raceID);
handle_crossing(id, start_timeRel, start_timeAbs, boatType, raceID);
return;
}
if (cmd=="+minlaptime") {
minTime=llList2Float(in,1);
llSay(0, "New minimum lap time: "+sec2hms(minTime));
return;
}
if (cmd=="+numlaps") {
raceNumLaps=llList2Integer(in,1);
llSay(0, "New number of race laps: "+(string)raceNumLaps);
return;
}
if (cmd=="+settings") {
llSay(0,"minimum lap time: "+sec2hms(minTime));
llSay(0,"number of laps: "+(string)raceNumLaps);
return;
}
if (cmd=="+debug") {
integer value=llList2Integer(in, 1);
DEBUG=value;
dbgSay("Debugging is "+(string)DEBUG);
return;
}
if (cmd=="+results") {
//final standings
llSay(0,"Race Results:");
integer len=llGetListLength(finishList);
for (i=0;i<len;++i) {
key racerKey=llList2Key(finishList,i);
string racerName=llKey2Name(racerKey);
listIdx=llListFindList(ownerList,[racerKey]);
string line=(string)(i+1)+": ";
line+=llKey2Name(racerKey)+" - ";
line+=sec2hms(llList2Float(timeList,listIdx));
llSay(0,line);
}
//lap times
llSay(0,"Lap Times:");
len=llGetListLength(allLapsList);
for (i=0;i<len;i=i+3) {
list lap=llList2List(allLapsList,i,i+2);
string nameStr=llList2String(lap,0);
string lapStr=llList2String(lap,1);
string timeStr=sec2hms(llList2Float(lap,2));
llSay(0,nameStr+"-- lap "+lapStr+": "+timeStr);
}
return;
}
if (cmd=="continuous") {
continuous=llList2Integer(in, 1);
dbgSay("continuous mode is "+(string)continuous);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////
default {
state_entry() {
if (llGetInventoryType("settings")!=INVENTORY_NOTECARD) {
llSay(0, "Unable to find the settings notecard. Please provide one.");
return;
}
readfirstNCLine("settings");
}
dataserver( key queryid, string data ) {
integer ncResult;
ncResult=readNextNCLine(queryid, data);
if (ncResult==1) { // 1=We are done.
// targets=getNCLineList();
parseSettings();
state running;
}
}
}
//////////////////////////////////////////////////////////////////////////////////
state running {
state_entry() {
llListen(0,"",NULL_KEY,"");
llListen(channel,"",NULL_KEY,"");
DEBUG=TRUE;
}
listen(integer channel,string name,key id,string message) {
if (group && !llSameGroup(id)) {
return;
}
list in=llParseString2List(message,[" "],[]);
parseMessage(in);
}
link_message(integer sender_num,integer num,string message,key id) {
list in=llParseString2List(message,[" "],[]);
parseMessage(in);
}
}
texture mover
Just to be complete: the texture mover script that is responsible for animating the arrow mark that indicates the start line orientation.
parseMessage(list in) {
string cmd=llList2String(in,0);
if (cmd == "reset") {
llResetScript();
return;
}
if (cmd == "ways") {
integer ways;
ways=llList2Integer(in, 1);
if (ways==2) {
llSetAlpha(0.0, ALL_SIDES);
} else {
llSetAlpha(0.5, ALL_SIDES);
}
return;
}
}
default
{
state_entry()
{
//llSetTextureAnim(FALSE, ALL_SIDES, 1, 1, 0, 0, 0.5);
llSetTextureAnim(ANIM_ON | SMOOTH | LOOP | REVERSE,ALL_SIDES,1,1,0,0,0.3);
}
link_message(integer sender_num,integer num,string message,key id) {
if (num==0) {
list in=llParseString2List(message,[" "],[]);
parseMessage(in);
return;
}
}
}
Configuration notecard
Last, not least the config notecard as a template
# the following is the channel that the startline uses to communicate with external parts # external parts are e.g. extensions, the start timer clock, flag poles, ..... # change this if you need to operate two lines within shouting range of each other channel = -70704 # the "ways" parameter determines if the line is a one way or a two way line. Allowed values are 1 and 2 ways = 2 # group determines whether only group members can operate the line. Allowed values are 0=everybody and 1=only group group=0
instructions
and the instructions, just out of completeness
Official SLSF Race Start Line
Group Version
(Any members of the group to which the start line belongs may command it)
Directions for use:
--------------------
1) Rez the start line, preferably over water.
- The start line will automatically rez a digital clock overhead. The clock is transparent on one side, so you may not see it at first.
2) Position and rotate the start line how you want it. Likewise for the clock.
- The start line has arrows indicating the race direction. Racers moving in the direction of the arrows will be registered by the start line.
- Rotate the clock so that it is facing the racers as they start.
- The clock and start line are phantom linksets, so they will not interfere with moving objects.
- Keep the clock within shouting distance of the center of the start line.
3) IMPORTANT!!! Before a race, all racers must set the IDs on their Flying Takos to unique numbers (see boat instructions for details).
- The ID number ensures that only official racers are detected by the start line; without it, a racer will never be detected.
4) IMPORTANT!!! You must click on the start line once to activate its listen event.
- The listen event is automatically deactivated after the timer starts, and requires a touch to reactivate.
5) Start line command list (note preceding "+"):
"+time xx" - set the countdown time to xx seconds (default is 120 s).
"+start" - start the countdown timer running.
"+pause" - stop the countdown timer momentarily.
"+reset" - stop the countdown timer and reset.
"+help" - displays help text.
"+minlaptime x" - sets the minimum possible lap time (default is 60 s). Used to detect cheating or inadvertent recrossings.
"+numlaps n" - sets the number of race laps (default is 3).
"+results" - reports the final standings.
"+settings" - reports current minlaptime and numlaps values
6) When you have set all the variables, say "+start" to begin the race countdown.
- The start line will automatically track individual racers during the race.
- As racers cross the line, their lap number and elapsed time will be shouted.
- Racers' finishing positions will be shouted as they cross the line after the last lap.
7) Upon activation, the start line timer will count down to the actual start event, then display the elapsed race time after the start.
Web loader
And here the web loader script:
list collectedOutput;
integer sentMarker=0; // Holds the number of output that was sent
key idEvent;
string lineID="Madaket";
integer iAllowedTimer;
key idAllowed;
sendEvents() {
if (sentMarker!=0)
return;
sentMarker=llGetListLength(collectedOutput);
// llSay(0, "Marker is "+(string)sentMarker);
if (sentMarker==0)
return;
integer i;
string postString;
for (i=0; i<sentMarker; i++) {
postString=postString+llList2String(collectedOutput, i) + "\n";
}
string URL="http://www.secondsailing.org/RFL/accept_lap.php?line="
+lineID;
// llSay(0, "Posting:" + postString + " to URL " + URL);
idEvent=llHTTPRequest(URL, [HTTP_METHOD, "POST"], postString);
}
sendQuery() {
idAllowed=llHTTPRequest("http://www.secondsailing.org/RFL/allowed_sailors_startline.php", [HTTP_METHOD, "GET"], "");
// llSay(0, "the key is "+(string)id);
}
default
{
state_entry()
{
llSetTimerEvent(1.0);
}
link_message(integer sender_num, integer num, string str, key id) {
if (num!=9)
return;
// llSay(0, "Web loader: Message received, num="+(string)num+" "+str);
collectedOutput=collectedOutput+[str];
}
timer() {
// llSay(0, "Timer");
sendEvents();
// iAllowedTimer++;
// if (iAllowedTimer>20) {
// sendQuery();
// iAllowedTimer=0;
// }
}
http_response(key request_id, integer status, list metadata, string body)
{
// llSay(0, "Status is:"+(string)status);
// llSay(0, "Body is "+body);
if (request_id==idEvent) {
collectedOutput=llDeleteSubList(collectedOutput, 0, sentMarker-1);
sentMarker=0;
idEvent=NULL_KEY;
return;
}
// if (request_id==idAllowed) {
// body=": Please donate to use this line. See http://www.secondsailing.org/RFL/info.html for more information|"+body;
// llMessageLinked(LINK_THIS, 10, body, NULL_KEY);
// idAllowed=NULL_KEY;
// return;
// }
}
}
Remarks to the web loader script: It tries to minimize calls to the web server by collecting events for a moment and then transmit them as a bunch.