Difference between revisions of "LSL Protocol/Restrained Love Relay/Reference Implementation"

From Second Life Wiki
Jump to: navigation, search
(added pointer to object in world for non-techies)
(Reference Implementation)
(One intermediate revision by the same user not shown)
Line 40: Line 40:
  
 
<lsl>
 
<lsl>
//~ RestrainedLife Viewer Relay Script example code
 
//~ By Marine Kelley, Maike Short, Felis Darwin
 
//~ Thanks to Chorazin Allen, Azoth Amat, Gregor Mougin, Nano Siemens, Cerdita Piek
 
  
 +
 +
//~ RestrainedLife Viewer Relay Script example code
 +
//~ By Marine Kelley
 +
//~ 2008-02-03
 +
//~ 2008-02-03
 +
//~ v1.1
 +
//~ 2008-02-16 with fixes by Maike Short
 +
//~ 2008-02-24 more fixes by Maike Short
 +
//~ 2008-03-03 code cleanup by Maike Short
 +
//~ 2008-03-05 silently ignore commands for removing restrictions if they are not active anyway
 +
//~ 2008-06-24 fix of loophole in ask-mode by Felis Darwin
 +
//~ 2008-09-01 changed llSay to llShout, increased distance check (MK)
 +
 
//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy,
 
//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy,
 
//~ completeness or performance. It may only be distributed in its full source code,
 
//~ completeness or performance. It may only be distributed in its full source code,
 
//~ this header and disclaimer and is not to be sold.
 
//~ this header and disclaimer and is not to be sold.
 
+
integer DEBUG = FALSE;
+
//~ * Possible improvements
 
+
//~ Do some error checking
// make cheating (adding exceptions) a bit more difficult by not allowing
+
//~ Handle more than one object
// attachment to control the viewer as they are not subjected to land building
+
//~ Periodically check that the in-world objects are still around, when one is missing purge its restrictions
// restrictions
+
//~ Manage an access list
integer ALLOW_CONTROL_BY_ATTACHMENTS = FALSE;
+
//~ Reject some commands if not on access list (force remove clothes, force remove attachments...)
 
+
//~ and much more...
 
+
// list of refused commands. Use [] to allow everything or
+
// ["@detach", "@remoutfit"] to prevent striping for example
+
list refusedForceCommands = [];
+
 
+
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
//                    Constants
 
//                    Constants
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
   
 
   
integer RLVRS_PROTOCOL_VERSION = 1015; // version of the protocol, stated on the specification page
+
integer RLVRS_PROTOCOL_VERSION = 1014; // version of the protocol, stated on the specification page
string RLVRS_IMPL_VERSION = "Reference Implementation 1.015.a"; // version of the implementation for debugging
+
 
   
 
   
 
string PREFIX_RL_COMMAND = "@";
 
string PREFIX_RL_COMMAND = "@";
 
string PREFIX_METACOMMAND = "!";
 
string PREFIX_METACOMMAND = "!";
 
+
 
integer RLVRS_CHANNEL = -1812221819;  // RLVRS in numbers
 
integer RLVRS_CHANNEL = -1812221819;  // RLVRS in numbers
 
integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers
 
integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers
integer STATUS_LINK_CHANNEL = -1373421300;
+
integer RLV_LINK_CHANNEL = -1373421301;
+
integer MAX_OBJECT_DISTANCE = 100;    // 100m is llShout distance
integer CMD_LINK_CHANNEL = -1373421302;
+
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 300; // 300 is 5 minutes
integer DIALOG_LINK_CHANNEL = -1373421303;
+
integer ASK_LINK_CHANNEL = -1373421304;
+
 
+
integer MAX_OBJECT_DISTANCE = 20;    // 20m is llSay distance
+
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds
+
 
+
 
integer PERMISSION_DIALOG_TIMEOUT = 30;
 
integer PERMISSION_DIALOG_TIMEOUT = 30;
 +
 
integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
 
integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
 
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
 
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
 
+
 
integer MODE_OFF = 0;
 
integer MODE_OFF = 0;
 
integer MODE_ASK = 1;
 
integer MODE_ASK = 1;
 
integer MODE_AUTO = 2;
 
integer MODE_AUTO = 2;
 
+
list MODE_DESCRIPTIONS = ["RLV Relay is OFF", "RLV Relay is ON (permission needed)", "RLV Relay is ON (auto-accept)"];
+
 
+
string RVL_COMMAND_START = "@detach=n";
+
string RVL_COMMAND_END = "@detach=y";
+
 
+
 
+
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
//                      Variables
 
//                      Variables
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
   
 
   
integer mode;
+
integer nMode;
 
+
list restrictions; // restrictions currently applied (without the "=n" part)
+
list lRestrictions; // restrictions currently applied (without the "=n" part)
key source;        // UUID of the object I'm commanded by, always equal to NULL_KEY if restrictions is empty, always set if not
+
key kSource;        // UUID of the object I'm commanded by, always equal to NULL_KEY if lRestrictions is empty, always set if not
 
+
string pendingName; // name of initiator of pending request (first request of a session in mode 1)
+
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)
key pendingId;      // UUID of initiator of pending request (first request of a session in mode 1)
+
key sPendingId;      // UUID of initiator of pending request (first request of a session in mode 1)
string pendingMessage; // message of pending request
+
string sPendingMessage; // message of pending request (first request of a session in mode 1)
integer pendingTime;
+
integer sPendingTime;
 
+
 
// used on login
 
// used on login
 
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)
 
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)
 
integer loginWaitingForPong;
 
integer loginWaitingForPong;
 
integer loginPendingForceSit;
 
integer loginPendingForceSit;
 
+
 
key    lastForceSitDestination;
 
key    lastForceSitDestination;
 
integer lastForceSitTime;
 
integer lastForceSitTime;
 
+
integer isNotEmbedded = TRUE;
+
 
+
 
// ---------------------------------------------------
 
// ---------------------------------------------------
//              Helper functions
+
//              Low Level Communication
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
+
 
+
 
debug(string x)
 
debug(string x)
 
{
 
{
    if (DEBUG)
+
//    llOwnerSay("DEBUG: " + x);
    {
+
        llOwnerSay("DEBUG: " + x);
+
    }
+
 
}
 
}
 
// checks whether this object is an attachment of an avatar
 
integer isAttachment(key id)
 
{
 
    vector objpos1 = llList2Vector(llGetObjectDetails(id, [OBJECT_POS]), 0);
 
    vector ownerpos = llList2Vector(llGetObjectDetails(llGetOwnerKey(id), [OBJECT_POS]), 0);
 
    vector objpos2 = llList2Vector(llGetObjectDetails(id, [OBJECT_POS]), 0);
 
    return (llVecMag(objpos1 - ownerpos) <= llVecMag(objpos1 - objpos2));
 
}
 
 
 
// ---------------------------------------------------
 
//              Low Level Communication
 
// ---------------------------------------------------
 
 
 
   
 
   
 
// acknowledge or reject
 
// acknowledge or reject
 
ack(string cmd_id, key id, string cmd, string ack)
 
ack(string cmd_id, key id, string cmd, string ack)
 
{
 
{
     llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
+
     llShout(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
 
}
 
}
 
+
 
// cmd begins with a '@'  
 
// cmd begins with a '@'  
 
sendRLCmd(string cmd)
 
sendRLCmd(string cmd)
 
{
 
{
     if (cmd != "")
+
     llOwnerSay(cmd);
    {
+
        if (isNotEmbedded)
+
        {
+
            llOwnerSay(cmd);
+
        }
+
        llMessageLinked(LINK_SET, RLV_LINK_CHANNEL, cmd, source);
+
    }
+
 
}
 
}
 
+
 
+
// get current mode as string
 +
string getModeDescription()
 +
{
 +
    if (nMode == 0) return "RLV Relay is OFF";
 +
    else if (nMode == 1) return "RLV Relay is ON (permission needed)";
 +
    else return "RLV Relay is ON (auto-accept)";
 +
}
 +
 
// check that this command is for us and not someone else
 
// check that this command is for us and not someone else
 
integer verifyWeAreTarget(string message)
 
integer verifyWeAreTarget(string message)
 
{
 
{
 
     list tokens = llParseString2List(message, [","], []);
 
     list tokens = llParseString2List(message, [","], []);
     if (llGetListLength(tokens) != 3) // this is not a normal command
+
     if (llGetListLength(tokens) == 3) // this is a normal command
 
     {
 
     {
        return FALSE;
+
      if (llList2String(tokens, 1) == llGetOwner()) // talking to me ?
 +
      {
 +
        return TRUE;
 +
      }
 
     }
 
     }
 
+
     return FALSE;
     return (llList2String(tokens, 1) == llGetOwner()); // talking to me ?
+
 
}
 
}
 
+
// checks that the type of object (world object, attachment) is allowed
+
integer verifySourceType(key id)
+
{
+
    if (ALLOW_CONTROL_BY_ATTACHMENTS)
+
    {
+
        return TRUE;
+
    }
+
   
+
    return !isAttachment(id);
+
}
+
 
+
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
//              Permission Handling
 
//              Permission Handling
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
+
 
+
 
// are we already under command by this object?
 
// are we already under command by this object?
 
integer isObjectKnow(key id)
 
integer isObjectKnow(key id)
 
{
 
{
 +
    // first some error handling
 +
    if (id == NULL_KEY)
 +
    {
 +
        return FALSE;
 +
    }
 +
 
     // are we already under command by this object?
 
     // are we already under command by this object?
     if (source == id)
+
     if (kSource == id)
 
     {
 
     {
 
         return TRUE;
 
         return TRUE;
 
     }
 
     }
 
+
 
     // are we not under command by any object but were we forced to sit on this object recently?
 
     // are we not under command by any object but were we forced to sit on this object recently?
     if ((source == NULL_KEY) && (id == lastForceSitDestination))
+
     if ((kSource == NULL_KEY) && (id == lastForceSitDestination))
 
     {
 
     {
 
         debug("on last force sit target");
 
         debug("on last force sit target");
Line 214: Line 187:
 
         }
 
         }
 
     }
 
     }
 
+
 
     return FALSE;
 
     return FALSE;
 
}
 
}
 
+
 
+
// check whether the object is in llSay distance.
+
// check whether the object is in llShout distance. It could have moved
// The specification requires llSay instead of llShout or llRegionSay
+
// before the message is received (chatlag)
// to be used to limit the range. But this has to be checked here again
+
// because the objects are not trustworthy.
+
 
integer isObjectNear(key id)
 
integer isObjectNear(key id)
 
{
 
{
Line 231: Line 202:
 
     return distance <= MAX_OBJECT_DISTANCE;
 
     return distance <= MAX_OBJECT_DISTANCE;
 
}
 
}
 
+
 
// do a basic check on the identity of the object trying to issue a command
 
// do a basic check on the identity of the object trying to issue a command
 
integer isObjectIdentityTrustworthy(key id)
 
integer isObjectIdentityTrustworthy(key id)
Line 239: Line 210:
 
     key object_owner=llGetOwnerKey(id);
 
     key object_owner=llGetOwnerKey(id);
 
     key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
 
     key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
 
+
 
     debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
 
     debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
 
     debug("group= " + (string) parcel_group + " / " + (string) object_group);
 
     debug("group= " + (string) parcel_group + " / " + (string) object_group);
 
+
 
     if (object_owner==llGetOwner ()        // IF I am the owner of the object
 
     if (object_owner==llGetOwner ()        // IF I am the owner of the object
 
       || object_owner==parcel_owner        // OR its owner is the same as the parcel I'm on
 
       || object_owner==parcel_owner        // OR its owner is the same as the parcel I'm on
Line 252: Line 223:
 
     return FALSE;
 
     return FALSE;
 
}
 
}
 
+
 
+
 
// Is this a simple request for information or a meta command like !release?
 
// Is this a simple request for information or a meta command like !release?
 
integer isSimpleRequest(list list_of_commands)  
 
integer isSimpleRequest(list list_of_commands)  
Line 259: Line 230:
 
     integer len = llGetListLength(list_of_commands);
 
     integer len = llGetListLength(list_of_commands);
 
     integer i;
 
     integer i;
 
+
 
     // now check every single atomic command
 
     // now check every single atomic command
 
     for (i=0; i < len; ++i)
 
     for (i=0; i < len; ++i)
Line 269: Line 240:
 
         }
 
         }
 
     }
 
     }
 
+
 
     // all atomic commands passed the test
 
     // all atomic commands passed the test
 
     return TRUE;
 
     return TRUE;
 
}
 
}
 
+
// is this a simple atomar command
+
// is this a simple atmar command
 
// (a command which only queries some information or releases restrictions)
 
// (a command which only queries some information or releases restrictions)
 
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)
 
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)
Line 289: Line 260:
 
             return TRUE;
 
             return TRUE;
 
         }
 
         }
 
+
 
         // removing restriction
 
         // removing restriction
 
         if ((param == "y") || (param == "rem"))
 
         if ((param == "y") || (param == "rem"))
Line 296: Line 267:
 
         }
 
         }
 
     }
 
     }
 
+
 
     // check for a leading ! (meta command)
 
     // check for a leading ! (meta command)
 
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
 
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
Line 302: Line 273:
 
         return TRUE;
 
         return TRUE;
 
     }
 
     }
 
+
 
     // check for @clear
 
     // check for @clear
 
     // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
 
     // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
Line 312: Line 283:
 
         return TRUE;
 
         return TRUE;
 
     }
 
     }
 
+
 
     // this one is not "simple".
 
     // this one is not "simple".
 
     return FALSE;
 
     return FALSE;
 
}
 
}
 
+
 
// If we already have commands from this object pending
 
// If we already have commands from this object pending
 
// because of a permission request dialog, just add the
 
// because of a permission request dialog, just add the
Line 324: Line 295:
 
integer tryToGluePendingCommands(key id, string commands)
 
integer tryToGluePendingCommands(key id, string commands)
 
{
 
{
     if (llStringLength(pendingMessage) > 4000)
+
     if ((sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
 
     {
 
     {
        llSay(0, llKey2Name(id) + " is flooding commands. Releasing restrictions.");
+
         debug("Gluing " + sPendingMessage + " with " + commands);
        releaseRestrictions();
+
         sPendingMessage = sPendingMessage + "|" + commands;
        return TRUE;
+
    }
+
    if ((pendingId == id) && (pendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
+
    {
+
         debug("Gluing " + (string) pendingMessage + " with " + commands);
+
         pendingMessage = pendingMessage + "|" + commands;
+
 
         return TRUE;
 
         return TRUE;
 
     }
 
     }
 
     return FALSE;
 
     return FALSE;
 
}
 
}
 
+
 
+
// accept !release even if out of range
+
handleCommandsWhichAreAcceptedOutOfRange(key id, string message)
+
{
+
    if (id != source)
+
    {
+
        return;
+
    }
+
 
+
    list tokens = llParseString2List (message, [","], []);
+
    if (llGetListLength (tokens) < 3)
+
    {
+
        return;
+
    }
+
    string commands = llList2String(tokens, 2);
+
    list list_of_commands = llParseString2List(commands, ["|"], []);
+
    if (llListFindList(list_of_commands, ["!release"]) > -1)
+
    {
+
        debug("accepted !release although it was out of range");
+
        releaseRestrictions();
+
    }
+
}
+
 
+
 
+
 
// verifies the permission. This includes mode  
 
// verifies the permission. This includes mode  
 
// (off, permission, auto) of the relay and the
 
// (off, permission, auto) of the relay and the
Line 369: Line 310:
 
{
 
{
 
     // is it switched off?
 
     // is it switched off?
     if (mode == MODE_OFF)
+
     if (nMode == MODE_OFF)
 
     {
 
     {
 
         return FALSE;
 
         return FALSE;
 
     }
 
     }
 
+
 +
    // extract the commands-part
 
     list tokens = llParseString2List (message, [","], []);
 
     list tokens = llParseString2List (message, [","], []);
 
     if (llGetListLength (tokens) < 3)
 
     if (llGetListLength (tokens) < 3)
Line 381: Line 323:
 
     string commands = llList2String(tokens, 2);
 
     string commands = llList2String(tokens, 2);
 
     list list_of_commands = llParseString2List(commands, ["|"], []);
 
     list list_of_commands = llParseString2List(commands, ["|"], []);
 
+
 +
    // accept harmless commands silently
 +
    if (isSimpleRequest(list_of_commands))
 +
    {
 +
        return TRUE;
 +
    }
 +
 
     // if we are already having a pending permission-dialog request for THIS object,
 
     // if we are already having a pending permission-dialog request for THIS object,
 
     // just add the new commands at the end of the pending command list.
 
     // just add the new commands at the end of the pending command list.
Line 388: Line 336:
 
         return FALSE;
 
         return FALSE;
 
     }
 
     }
 
+
    // accept harmless commands silently
+
    if (isSimpleRequest(list_of_commands))
+
    {
+
        return TRUE;
+
    }
+
 
+
 
     // check whether this object belongs here
 
     // check whether this object belongs here
 
     integer trustworthy = isObjectIdentityTrustworthy(id);
 
     integer trustworthy = isObjectIdentityTrustworthy(id);
Line 402: Line 344:
 
         warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";
 
         warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";
 
     }
 
     }
 
+
 
     // ask in permission-request-mode and/OR in case the object identity is suspisous.
 
     // ask in permission-request-mode and/OR in case the object identity is suspisous.
     if (mode == MODE_ASK || !trustworthy)
+
     if (nMode == MODE_ASK || !trustworthy)
 
     {
 
     {
         pendingId=id;
+
         sPendingId=id;
         pendingName=name;
+
         sPendingName=name;
         pendingMessage = message;
+
         sPendingMessage=message;
         pendingTime = llGetUnixTime();
+
         sPendingTime = llGetUnixTime();
         if (llKey2Name(llGetOwnerKey(id)) != "")
+
         llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
        {
+
            name += " (owned by " + llKey2Name(llGetOwnerKey(id)) + ")";
+
        }
+
       
+
        string text = name + " would like control your viewer." + warning + ".\n\nDo you accept ?";
+
        debug("Asking for permission isNotEmbedded=" + (string) isNotEmbedded);
+
        if (isNotEmbedded)
+
        {
+
            llDialog (llGetOwner(), text, ["Yes", "No"], DIALOG_CHANNEL);
+
        }
+
        llMessageLinked(LINK_SET, ASK_LINK_CHANNEL, text, id);
+
 
         debug("Asking for permission");
 
         debug("Asking for permission");
 
         return FALSE;
 
         return FALSE;
Line 427: Line 358:
 
     return TRUE;
 
     return TRUE;
 
}
 
}
 
+
 
+
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
//              Executing of commands
 
//              Executing of commands
 
// ---------------------------------------------------
 
// ---------------------------------------------------
 
+
 
// execute a non-parsed message
 
// execute a non-parsed message
 
// this command could be denied here for policy reasons, (if it were implemenetd)
 
// this command could be denied here for policy reasons, (if it were implemenetd)
Line 438: Line 369:
 
execute(string name, key id, string message)
 
execute(string name, key id, string message)
 
{
 
{
     list tokens = llParseString2List(message, [","], []);
+
     list tokens=llParseString2List (message, [","], []);
     string cmd_id = llList2String(tokens, 0); // CheckAttach
+
     if (llGetListLength (tokens)==3) // this is a normal command
    list list_of_commands = llParseString2List(llList2String(tokens, 2), ["|"], []);
+
 
+
    integer len = llGetListLength (list_of_commands);
+
    integer i;
+
    string command;
+
    string prefix;
+
    for (i=0; i<len; ++i) // execute every command one by one
+
 
     {
 
     {
         // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
+
         string cmd_id=llList2String (tokens, 0); // CheckAttach
        command = llList2String(list_of_commands, i);
+
         key target=llList2Key (tokens, 1); // UUID
         prefix = llGetSubString(command, 0, 0);
+
         if (target==llGetOwner ()) // talking to me ?
 
+
         if (prefix == PREFIX_RL_COMMAND) // this is a RLV command
+
 
         {
 
         {
             executeRLVCommand(cmd_id, id, command);
+
             list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);
        }
+
            integer len=llGetListLength (list_of_commands);
        else if (prefix == PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
+
            integer i;
        {
+
            string command;
            executeMetaCommand(cmd_id, id, command);
+
            string prefix;
 +
            for (i=0; i<len; ++i) // execute every command one by one
 +
            {
 +
                // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
 +
                command=llList2String (list_of_commands, i);
 +
                prefix=llGetSubString (command, 0, 0);
 +
 +
                if (prefix==PREFIX_RL_COMMAND) // this is a RL command
 +
                {
 +
                    executeRLVCommand(cmd_id, id, command);
 +
                }
 +
                else if (prefix==PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
 +
                {
 +
                    executeMetaCommand(cmd_id, id, command);
 +
                }
 +
            }
 
         }
 
         }
 
     }
 
     }
 
}
 
}
 
+
 
// executes a command for the restrained life viewer  
 
// executes a command for the restrained life viewer  
 
// with some additinal magic like book keeping
 
// with some additinal magic like book keeping
 
executeRLVCommand(string cmd_id, string id, string command)
 
executeRLVCommand(string cmd_id, string id, string command)
 
{
 
{
    command = llToLower(command);
 
 
     // we need to know whether whether is a rule or a simple command
 
     // we need to know whether whether is a rule or a simple command
     list tokens = llParseString2List(command, ["="], []);
+
     list tokens_command=llParseString2List (command, ["="], []);
     string behav = llList2String(tokens, 0); // @getattach:skull
+
     string behav=llList2String (tokens_command, 0); // @getattach:skull
     string param = llList2String(tokens, 1); // 2222
+
     string param=llList2String (tokens_command, 1); // 2222
     integer ind = llListFindList(restrictions, [behav]);
+
     integer ind=llListFindList (lRestrictions, [behav]);
 
+
   
    tokens = llParseString2List(behav, [":"], []); // @sit:<uuid>
+
     if (param=="n" || param=="add") // add to lRestrictions
    string behavName = llList2String (tokens, 0); // @sit
+
    string option = llList2String (tokens, 1);    // <uuid>
+
   
+
    debug("executeRLVCommand: behav=!" + behav + "! param=!" + param + "!");
+
 
+
     if (param=="n" || param=="add") // add to restrictions
+
 
     {
 
     {
         if (ind < 0)
+
         if (ind<0) lRestrictions+=[behav];
        {
+
         kSource=id; // we know that kSource is either NULL_KEY or id already
            if (llGetListLength(restrictions) == 0)
+
            {
+
                sendRLCmd(RVL_COMMAND_START);
+
                llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
+
            }
+
            restrictions += [behav];
+
         }
+
        source = id; // we know that source is either NULL_KEY or id already
+
 
     }
 
     }
     else if (param == "y" || param == "rem") // remove from restrictions
+
     else if (param=="y" || param=="rem") // remove from lRestrictions
 
     {
 
     {
         if (ind > -1)  
+
         if (ind > -1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind);
        {
+
         if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY;
            restrictions = llDeleteSubList (restrictions, ind, ind);
+
        }
+
         if (llGetListLength(restrictions) == 0)
+
        {
+
            source = NULL_KEY;
+
            sendRLCmd(RVL_COMMAND_END);
+
        }
+
        removeFromPendingList(behav);
+
 
     }
 
     }
    else if (param == "force")
+
    {
+
        if (llListFindList(refusedForceCommands, [behavName]) >= 0)
+
        {
+
            debug("rejected force-command: behav=!" + behav + "! behavName=!" + behavName + "!");
+
            ack(cmd_id, id, command, "ko");
+
            return;
+
        }
+
    }
+
 
+
 
     workaroundForAtClear(command);
 
     workaroundForAtClear(command);
     rememberForceSit(behavName, option, param);
+
     rememberForceSit(command);
 
     sendRLCmd(command); // execute command
 
     sendRLCmd(command); // execute command
 
     ack(cmd_id, id, command, "ok"); // acknowledge
 
     ack(cmd_id, id, command, "ok"); // acknowledge
 
}
 
}
 
+
 
// check for @clear
 
// check for @clear
 
// Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
 
// Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
Line 534: Line 439:
 
     }
 
     }
 
}
 
}
 
+
 
// remembers the time and object if this command is a force sit
 
// remembers the time and object if this command is a force sit
rememberForceSit(string behavName, string option, string param)
+
rememberForceSit(string command)
 
{
 
{
     // clear lastForceSitDestination in case we are now prevented from standing up and
+
     list tokens_command=llParseString2List (command, ["="], []);
    // the force sit was long ago. Note: restrictions is checked to prevent the
+
     string behav=llList2String (tokens_command, 0); // @sit:<uuid>
    // clearance in case @unsit is just send again on login
+
    string param=llList2String (tokens_command, 1); // force
    if (behavName == "@unsit")
+
     {
+
        if (llListFindList(restrictions, ["@unsit"]) < 0)
+
        {
+
            if (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT < llGetUnixTime())
+
            {
+
                debug("clearing lastForceSitDestination");
+
                lastForceSitDestination = NULL_KEY;
+
            }
+
        }
+
    }
+
   
+
 
     if (param != "force")
 
     if (param != "force")
 
     {
 
     {
 
         return;
 
         return;
 
     }
 
     }
 
+
     debug("'force'-command:" + behavName + "/" + option);
+
    tokens_command=llParseString2List(behav, [":"], []);
     if (behavName != "@sit")
+
    behav=llList2String (tokens_command, 0); // @sit
 +
    param=llList2String (tokens_command, 1); // <uuid>
 +
     debug("'force'-command:" + behav + "/" + param);
 +
     if (behav != "@sit")
 
     {
 
     {
 
         return;
 
         return;
 
     }
 
     }
     lastForceSitDestination = (key) option;
+
     lastForceSitDestination = (key) param;
 
     lastForceSitTime = llGetUnixTime();
 
     lastForceSitTime = llGetUnixTime();
 
     debug("remembered force sit");
 
     debug("remembered force sit");
 
}
 
}
 
+
 
// executes a meta command which is handled by the relay itself
 
// executes a meta command which is handled by the relay itself
 
executeMetaCommand(string cmd_id, string id, string command)
 
executeMetaCommand(string cmd_id, string id, string command)
 
{
 
{
     if (command == PREFIX_METACOMMAND + "version") // checking relay protocol version
+
     if (command==PREFIX_METACOMMAND+"version") // checking relay version
 
     {
 
     {
         ack(cmd_id, id, command, (string) RLVRS_PROTOCOL_VERSION);
+
         ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
 
     }
 
     }
    else if (command == PREFIX_METACOMMAND + "implversion") // checking relay version
+
     else if (command==PREFIX_METACOMMAND+"release") // release all the restrictions (end session)
    {
+
        ack(cmd_id, id, command, RLVRS_IMPL_VERSION);
+
    }
+
     else if (command == PREFIX_METACOMMAND + "release") // release all the restrictions (end session)
+
 
     {
 
     {
 
         releaseRestrictions();
 
         releaseRestrictions();
 
         ack(cmd_id, id, command, "ok");
 
         ack(cmd_id, id, command, "ok");
    }
 
    else if (command == PREFIX_METACOMMAND + "pong")
 
    {
 
        loginWaitingForPong = FALSE;
 
 
     }
 
     }
 
}
 
}
 
// removes a restriction from the list of pending commands
 
removeFromPendingList(string behav)
 
{
 
    string search = behav + "=";
 
 
    // don't do the expensive parsing (string operations are very slow in pre-
 
    // mono LSL) in case we can detect fast that this one is not in the list.
 
    if (llSubStringIndex(pendingMessage, search) < 0)
 
    {
 
        return;
 
    }
 
 
    list tokens = llParseString2List(pendingMessage, [","], []);
 
    list commands = llParseString2List(llList2String(tokens, 2), ["|"], []);
 
    integer modified = FALSE;
 
 
    integer len = llGetListLength(commands);
 
    integer i;
 
    for (i = len - 1; i >= 0; i--)
 
    {
 
        string cmd = llList2String(commands, i);
 
        if (llSubStringIndex(cmd, search) == 0)
 
        {
 
            if (llSubStringIndex(cmd, "=n") > -1 || llSubStringIndex(cmd, "=add") > -1)
 
            {
 
                commands = llDeleteSubList(commands, i, i);
 
                modified = TRUE;
 
            }
 
        }
 
    }
 
 
    if (modified)
 
    {
 
        if (llGetListLength(commands) > 0)
 
        {
 
            pendingMessage = llList2String(tokens, 0) + "," + llList2String(tokens, 1) + "," + llDumpList2String(commands, "|");
 
        }
 
        else
 
        {
 
            clearPendingMessages();
 
        }
 
    }
 
 
}
 
 
 
   
 
   
 
// lift all the restrictions (called by !release and by turning the relay off)
 
// lift all the restrictions (called by !release and by turning the relay off)
releaseRestrictions()
+
releaseRestrictions ()
 
{
 
{
     source = NULL_KEY;
+
     kSource=NULL_KEY;
 
     integer i;
 
     integer i;
     integer len = llGetListLength (restrictions);
+
     integer len=llGetListLength (lRestrictions);
     for (i = 0; i < len; ++i)
+
     for (i=0; i<len; ++i)
 
     {
 
     {
         sendRLCmd(llList2String (restrictions, i) + "=y");
+
         sendRLCmd(llList2String (lRestrictions, i)+"=y");
 
     }
 
     }
     sendRLCmd(RVL_COMMAND_END);
+
     lRestrictions = [];
    lastForceSitDestination = NULL_KEY;
+
    restrictions = [];
+
 
     loginPendingForceSit = FALSE;
 
     loginPendingForceSit = FALSE;
    clearPendingMessages();
 
    llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "release", NULL_KEY);
 
 
}
 
}
 
 
// deletes the list of pending messsages
 
clearPendingMessages()
 
{
 
    // clear pending request
 
    pendingName = "";
 
    pendingId = NULL_KEY;
 
    pendingMessage = "";
 
    pendingTime = 0;
 
}
 
 
 
// processes a message send on the relay channel
 
processRelayMessage(string name, key id, string message)
 
{
 
    if (mode== MODE_OFF)
 
    {
 
        debug("deactivated - ignoring commands");
 
        return; // mode is 0 (off) => reject
 
    }
 
 
    debug("LISTEN: " + message);
 
 
    if (!verifyWeAreTarget(message))
 
    {
 
      return;
 
    }
 
 
    if (!verifySourceType(id))
 
    {
 
        return;
 
    }
 
 
    debug("Got message (active world object " + (string) source + "): name=" + name+ " id=" + (string) id + " message=" + message);
 
 
   
 
   
    if (source)
 
    {
 
        if (source != id)
 
        {
 
            if (isObjectNear(source))
 
            {
 
                debug("already used by another object => reject");
 
                return;
 
            }
 
            else
 
            {
 
                debug("already used by another object, which is out of range");
 
                releaseRestrictions();
 
            }
 
        }
 
    }
 
 
    if (!isObjectNear(id))
 
    {
 
        handleCommandsWhichAreAcceptedOutOfRange(id, message);
 
        return;
 
    }
 
 
    if (!isObjectKnow(id))
 
    {
 
        debug("asking for permission because source is NULL_KEY");
 
        if (!verifyPermission(id, name, message))
 
        {
 
            return;
 
        }
 
    }
 
 
    debug("Executing: " + (string) source);
 
    execute(name, id, message);
 
}
 
 
// ---------------------------------------------------
 
//            mode and dialog handling
 
// ---------------------------------------------------
 
 
 
 
// shows current mode as string
 
showModeDescription()
 
{
 
    if (isNotEmbedded)
 
    {
 
        llOwnerSay(llList2String(MODE_DESCRIPTIONS, mode));
 
    }
 
    llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "show_mode", (key) ((string) mode));
 
}
 
 
 
// process the Yes/No buttons of the permission dialog
 
processDialogResponse(key id, string message)
 
{
 
    if (pendingId)
 
    {
 
        if (message == "Yes") // pending request authorized => process it
 
        {
 
            execute(pendingName, pendingId, pendingMessage);
 
        }
 
        clearPendingMessages();
 
    }
 
}
 
 
 
 
   
 
   
 
// ---------------------------------------------------
 
// ---------------------------------------------------
Line 762: Line 498:
 
   
 
   
 
init() {
 
init() {
     sendRLCmd("@clear");
+
     nMode=1;
     llOwnerSay("Waiting for plugins to initialize...");
+
     kSource=NULL_KEY;
     llSleep(5);
+
     lRestrictions=[];
 
+
    sPendingId=NULL_KEY;
     mode = MODE_ASK;
+
     sPendingName="";
 
+
    sPendingMessage="";
     llListen(RLVRS_CHANNEL, "", "", "");
+
     llListen (RLVRS_CHANNEL, "", "", "");
     llListen(DIALOG_CHANNEL, "", llGetOwner(), "");
+
     llListen (DIALOG_CHANNEL, "", llGetOwner(), "");
 
+
     llOwnerSay (getModeDescription());
    if (llGetFreeMemory() < 10000)
+
    {
+
        llSay(0, llKey2Name(llGetOwner()) + ", your relay is having very little free script memory and is likly to crash during use. Please verify that it was compiled using Mono.");
+
    }
+
    llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "init", NULL_KEY);
+
 
+
     llOwnerSay("Initialized and ready");
+
    showModeDescription();
+
    debug("Free Memory: " + (string) llGetFreeMemory());
+
 
}
 
}
 
+
 
+
 
// sends the known restrictions (again) to the RL-viewer
 
// sends the known restrictions (again) to the RL-viewer
 
// (call this functions on login)
 
// (call this functions on login)
Line 788: Line 514:
 
{
 
{
 
     integer i;
 
     integer i;
     integer len = llGetListLength(restrictions);
+
     integer len=llGetListLength(lRestrictions);
 
     string restr;
 
     string restr;
     debug("source=" + (string) source);
+
     debug("kSource=" + (string) kSource);
     if (len > 0)
+
     for (i=0; i<len; ++i)
 
     {
 
     {
        sendRLCmd(RVL_COMMAND_START);
+
         restr=llList2String(lRestrictions, i);
    }
+
    for (i=0; i < len; ++i)
+
    {
+
         restr = llList2String(restrictions, i);
+
 
         debug("restr=" + restr);
 
         debug("restr=" + restr);
         sendRLCmd(restr + "=n");
+
         sendRLCmd(restr+"=n");
         if (restr == "@unsit")
+
         if (restr=="@unsit")
 
         {
 
         {
 
             loginPendingForceSit = TRUE;
 
             loginPendingForceSit = TRUE;
Line 806: Line 528:
 
     }
 
     }
 
}
 
}
 
+
 
// send a ping request and start a timer
 
// send a ping request and start a timer
 
pingWorldObjectIfUnderRestrictions()
 
pingWorldObjectIfUnderRestrictions()
 
{
 
{
 
     loginWaitingForPong = FALSE;
 
     loginWaitingForPong = FALSE;
     if (source)
+
     if (kSource != NULL_KEY)
 
     {
 
     {
         ack("ping", source, "ping", "ping");
+
         ack("ping", kSource, "ping", "ping");
 
         timerTickCounter = 0;
 
         timerTickCounter = 0;
 
         llSetTimerEvent(1.0);
 
         llSetTimerEvent(1.0);
Line 819: Line 541:
 
     }
 
     }
 
}
 
}
 
+
sendForceSitDuringLogin()
+
{
+
    key sitTarget = source;
+
    if (lastForceSitDestination)
+
    {
+
        sitTarget = lastForceSitDestination;
+
    }
+
    debug("Force sit during login on " + (string) sitTarget + " (source=" + (string) source + ", lastForceSitDestination=" + (string) lastForceSitDestination + ")");
+
    sendRLCmd ("@sit:" + (string) sitTarget + "=force");
+
}
+
 
+
 
+
// processes a timer event
+
processTimer()
+
{
+
    timerTickCounter++;
+
    debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
+
    if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))
+
    {
+
        llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available.");
+
        loginWaitingForPong = FALSE;
+
        loginPendingForceSit = FALSE;
+
        releaseRestrictions();
+
    }
+
 
+
    if (loginPendingForceSit)
+
    {
+
        integer agentInfo = llGetAgentInfo(llGetOwner());
+
        if (agentInfo & AGENT_SITTING)
+
        {
+
            loginPendingForceSit = FALSE;
+
            debug("is sitting now");
+
        }
+
        else if (timerTickCounter == LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
+
        {
+
            llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
+
            loginPendingForceSit = FALSE;
+
            releaseRestrictions();
+
        }
+
        else
+
        {
+
            sendForceSitDuringLogin();
+
        }
+
    }
+
 
+
    if (!loginPendingForceSit && !loginWaitingForPong)
+
    {
+
        llSetTimerEvent(0.0);
+
    }
+
}
+
 
+
processLinkMessage(key id, string message)
+
{
+
    if (message == "safeword")
+
    {
+
        llSay(0, llKey2Name(llGetOwner()) + " said the safeword. Freeing and deactivating relay.");
+
        releaseRestrictions();
+
        mode = MODE_OFF;
+
        showModeDescription();
+
        return;
+
    }
+
 
+
    if (message == "unlock")
+
    {
+
        if (id)
+
        {
+
            llSay(0, llKey2Name(id) + " freed " + llKey2Name(llGetOwner()) + " by unlocking the relay.");
+
        }
+
        releaseRestrictions();
+
        return;
+
    }
+
 
+
    if (message == "mode")
+
    {
+
        if (source)
+
        {
+
            llOwnerSay("Sorry, you cannot change the relay mode while it is active.");
+
            return;
+
        }
+
        mode = ((integer) ((string) id)) % 3;   
+
        showModeDescription();
+
        return;
+
    }
+
 
+
    if (message == "refusedForceCommands")
+
    {
+
        refusedForceCommands = llParseString2List((string) id, [","], []);
+
        return;
+
    }
+
 
+
    if (message == "embedded")
+
    {
+
        isNotEmbedded = FALSE;
+
        return;
+
    }
+
}
+
 
+
 
+
 
default
 
default
 
{
 
{
Line 929: Line 553:
 
         // relogging, we must refresh the viewer and ping the object if any
 
         // relogging, we must refresh the viewer and ping the object if any
 
         // if mode is not OFF, fire all the stored restrictions
 
         // if mode is not OFF, fire all the stored restrictions
         if (mode)
+
         if (nMode)
 
         {
 
         {
 
             reinforceKnownRestrictions();
 
             reinforceKnownRestrictions();
Line 935: Line 559:
 
         }
 
         }
 
         // remind the current mode to the user
 
         // remind the current mode to the user
         showModeDescription();
+
         llOwnerSay(getModeDescription());
 
     }
 
     }
 
+
 +
 
     timer()
 
     timer()
 
     {
 
     {
         processTimer();
+
         timerTickCounter++;
    }
+
        debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
 
+
         if (loginWaitingForPong && (timerTickCounter == LOGIN_DELAY_WAIT_FOR_PONG))
    listen(integer channel, string name, key id, string message)
+
    {
+
         if (channel==RLVRS_CHANNEL)
+
 
         {
 
         {
             processRelayMessage(name, id, message);
+
             llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available.");
 +
            loginWaitingForPong = FALSE;
 +
            loginPendingForceSit = FALSE;
 +
            releaseRestrictions();
 
         }
 
         }
         else if (channel==DIALOG_CHANNEL)
+
 +
         if (loginPendingForceSit)
 +
        {
 +
            integer agentInfo = llGetAgentInfo(llGetOwner());
 +
            if (agentInfo & AGENT_SITTING)
 +
            {
 +
                loginPendingForceSit = FALSE;
 +
                debug("is sitting now");
 +
            }
 +
            else if (timerTickCounter == LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
 +
            {
 +
                llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
 +
                loginPendingForceSit = FALSE;
 +
                releaseRestrictions();
 +
            }
 +
            else
 +
            {
 +
                sendRLCmd ("@sit:"+(string)kSource+"=force");
 +
            }
 +
        }
 +
 +
        if (!loginPendingForceSit && !loginWaitingForPong)
 
         {
 
         {
             processDialogResponse(id, message);
+
             llSetTimerEvent(0.0);
 
         }
 
         }
 
     }
 
     }
   
+
     link_message(integer sender, integer channel, string message, key id)
+
     listen(integer channel, string name, key id, string message)
 
     {
 
     {
         if (channel == CMD_LINK_CHANNEL)
+
         if (channel==RLVRS_CHANNEL)
 
         {
 
         {
             processLinkMessage(id, message);
+
             if (!verifyWeAreTarget(message))
 +
            {
 +
              return;
 +
            }
 +
 +
            if (nMode== MODE_OFF)
 +
            {
 +
                debug("deactivated - ignoring commands");
 +
                return; // mode is 0 (off) => reject
 +
            }
 +
//~            if (!isObjectNear(id)) return;
 +
 +
            debug("Got message (active world object " + (string) kSource + "): name=" + name+ "id=" + (string) id + " message=" + message);
 +
 +
            if (kSource != NULL_KEY && kSource != id)
 +
            {
 +
                debug("already used by another object => reject");
 +
                return;
 +
            }
 +
 +
            loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request
 +
 +
            if (!isObjectKnow(id))
 +
            {
 +
                debug("asking for permission because kSource is NULL_KEY");
 +
                if (!verifyPermission(id, name, message))
 +
                {
 +
                    return;
 +
                }
 +
            }
 +
 +
            debug("Executing: " + (string) kSource);
 +
            execute(name, id, message);
 
         }
 
         }
         else if (channel == DIALOG_LINK_CHANNEL)
+
         else if (channel==DIALOG_CHANNEL)
 
         {
 
         {
             processDialogResponse(id, message);
+
             if (id != llGetOwner())
 +
            {
 +
                return; // only accept dialog responses from the owner
 +
            }
 +
            if (sPendingId!=NULL_KEY)
 +
            {
 +
                if (message=="Yes") // pending request authorized => process it
 +
                {
 +
                    execute(sPendingName, sPendingId, sPendingMessage);
 +
                }
 +
 +
                // clear pending request
 +
                sPendingName="";
 +
                sPendingId=NULL_KEY;
 +
                sPendingMessage="";
 +
            }
 
         }
 
         }
 
     }
 
     }
 
+
 
     touch_start(integer num_detected)
 
     touch_start(integer num_detected)
 
     {
 
     {
        if (!isNotEmbedded)
 
        {
 
            return;
 
        }
 
 
 
         // touched by user => cycle through OFF/ON_PERMISSION/ON_ALWAYS modes
 
         // touched by user => cycle through OFF/ON_PERMISSION/ON_ALWAYS modes
         key toucher = llDetectedKey(0);
+
         key toucher=llDetectedKey(0);
         if (toucher != llGetOwner())
+
         if (toucher==llGetOwner())
 
         {
 
         {
             return;
+
             if (kSource != NULL_KEY)
 +
            {
 +
                llOwnerSay("Sorry, you cannot change the relay mode while it is locked.");
 +
                return;
 +
            }
 +
            ++nMode;
 +
            if (nMode>2) nMode=0;
 +
            if (nMode==MODE_OFF) releaseRestrictions ();
 +
            llOwnerSay (getModeDescription());
 
         }
 
         }
 
        if (source)
 
        {
 
            llOwnerSay("Sorry, you cannot change the relay mode while it is active.");
 
            return;
 
        }
 
        mode = (mode + 1) % 3;
 
        if (mode == MODE_OFF) {
 
            releaseRestrictions();
 
        }
 
        showModeDescription();
 
 
     }
 
     }
 
+
 
     changed(integer change)
 
     changed(integer change)
 
     {
 
     {
         if (change & (CHANGED_OWNER | CHANGED_INVENTORY | CHANGED_ALLOWED_DROP))  
+
         if (change & CHANGED_OWNER)  
 
         {
 
         {
 
             llResetScript();
 
             llResetScript();

Revision as of 11:47, 1 September 2008

This particular example that anyone can distribute freely as open-source only (you are not allowed to sell this code) and including the header comments is just meant to give an idea of how a relay basically works. You can find the change history at Change History. You can get a free relay object at the MSM Restraints shop in "Stonehaven".


What it does

  • Implements the specification described hereabove.
  • Commented to facilitate reading and learning.
  • Tested and working with test objects and with real cages.


What it doesn't do

  • Error-checking.
  • Access lists.
  • Lock on the avatar.
  • Handle more than one object.
  • Reject some commands by nature.
  • And so much more that makes good scripts stand out in the crowd.


How to use it

  • Create a prim in which you put a script containing this code (don't forget to name it correctly by following the "RLVnnn" requirement).
  • Wear the prim, it says its current on/off status
  • Click on the prim to switch from "Off" to "On with permission" to "On without permission" and back to "Off"
  • Find an object that implements this specification and test.


Special thanks

  • Chorazin Allen for reviewing the code, giving ideas, coding and re-coding his own scripts to make sure everything works properly between the relay and the cage, and for not killing me every time I change my mind here and there on the spec.
  • Azoth Amat and Nano Siemens for helping to find a solution to the "force sit on login" problem
  • Gregor Mougin for discovering and fixing the not-pong reply on login.

Reference Implementation

Please add fixes, new features and stuff like that to Development & Contribution.

<lsl>


//~ RestrainedLife Viewer Relay Script example code //~ By Marine Kelley //~ 2008-02-03 //~ 2008-02-03 //~ v1.1 //~ 2008-02-16 with fixes by Maike Short //~ 2008-02-24 more fixes by Maike Short //~ 2008-03-03 code cleanup by Maike Short //~ 2008-03-05 silently ignore commands for removing restrictions if they are not active anyway //~ 2008-06-24 fix of loophole in ask-mode by Felis Darwin //~ 2008-09-01 changed llSay to llShout, increased distance check (MK)

//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy, //~ completeness or performance. It may only be distributed in its full source code, //~ this header and disclaimer and is not to be sold.

//~ * Possible improvements //~ Do some error checking //~ Handle more than one object //~ Periodically check that the in-world objects are still around, when one is missing purge its restrictions //~ Manage an access list //~ Reject some commands if not on access list (force remove clothes, force remove attachments...) //~ and much more...


// --------------------------------------------------- // Constants // ---------------------------------------------------

integer RLVRS_PROTOCOL_VERSION = 1014; // version of the protocol, stated on the specification page

string PREFIX_RL_COMMAND = "@"; string PREFIX_METACOMMAND = "!";

integer RLVRS_CHANNEL = -1812221819; // RLVRS in numbers integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers

integer MAX_OBJECT_DISTANCE = 100; // 100m is llShout distance integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 300; // 300 is 5 minutes

integer PERMISSION_DIALOG_TIMEOUT = 30;

integer LOGIN_DELAY_WAIT_FOR_PONG = 10; integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;

integer MODE_OFF = 0; integer MODE_ASK = 1; integer MODE_AUTO = 2;


// --------------------------------------------------- // Variables // ---------------------------------------------------

integer nMode;

list lRestrictions; // restrictions currently applied (without the "=n" part) key kSource; // UUID of the object I'm commanded by, always equal to NULL_KEY if lRestrictions is empty, always set if not

string sPendingName; // name of initiator of pending request (first request of a session in mode 1) key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1) string sPendingMessage; // message of pending request (first request of a session in mode 1) integer sPendingTime;

// used on login integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit) integer loginWaitingForPong; integer loginPendingForceSit;

key lastForceSitDestination; integer lastForceSitTime;

// --------------------------------------------------- // Low Level Communication // ---------------------------------------------------


debug(string x) { // llOwnerSay("DEBUG: " + x); }

// acknowledge or reject ack(string cmd_id, key id, string cmd, string ack) {

   llShout(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);

}

// cmd begins with a '@' sendRLCmd(string cmd) {

   llOwnerSay(cmd);

}

// get current mode as string string getModeDescription() {

   if (nMode == 0) return "RLV Relay is OFF"; 
   else if (nMode == 1) return "RLV Relay is ON (permission needed)"; 
   else return "RLV Relay is ON (auto-accept)"; 

}

// check that this command is for us and not someone else integer verifyWeAreTarget(string message) {

   list tokens = llParseString2List(message, [","], []);
   if (llGetListLength(tokens) == 3) // this is a normal command
   {
     if (llList2String(tokens, 1) == llGetOwner()) // talking to me ?
     {
        return TRUE;
     }
   }
   return FALSE;

}

// --------------------------------------------------- // Permission Handling // ---------------------------------------------------

// are we already under command by this object? integer isObjectKnow(key id) {

   // first some error handling
   if (id == NULL_KEY)
   {
       return FALSE;
   }

   // are we already under command by this object?
   if (kSource == id)
   {
       return TRUE;
   }

   // are we not under command by any object but were we forced to sit on this object recently?
   if ((kSource == NULL_KEY) && (id == lastForceSitDestination))
   {
       debug("on last force sit target");
       if (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime())
       {
           debug("and recent enough to auto accept");
           return TRUE;
       }
   }

   return FALSE;

}


// check whether the object is in llShout distance. It could have moved // before the message is received (chatlag) integer isObjectNear(key id) {

   vector myPosition = llGetRootPosition();
   list temp = llGetObjectDetails(id, ([OBJECT_POS]));
   vector objPostition = llList2Vector(temp,0);
   float distance = llVecDist(objPostition, myPosition);
   return distance <= MAX_OBJECT_DISTANCE;

}

// do a basic check on the identity of the object trying to issue a command integer isObjectIdentityTrustworthy(key id) {

   key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);
   key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);
   key object_owner=llGetOwnerKey(id);
   key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);

   debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
   debug("group= " + (string) parcel_group + " / " + (string) object_group);

   if (object_owner==llGetOwner ()        // IF I am the owner of the object
     || object_owner==parcel_owner        // OR its owner is the same as the parcel I'm on
     || object_group==parcel_group        // OR its group is the same as the parcel I'm on
   )
   {
       return TRUE;
   }
   return FALSE;

}


// Is this a simple request for information or a meta command like !release? integer isSimpleRequest(list list_of_commands) {

   integer len = llGetListLength(list_of_commands);
   integer i;

   // now check every single atomic command
   for (i=0; i < len; ++i)
   {
       string command = llList2String(list_of_commands, i);
       if (!isSimpleAtomicCommand(command))
       {
          return FALSE;
       }
   }

   // all atomic commands passed the test
   return TRUE;

}

// is this a simple atmar command // (a command which only queries some information or releases restrictions) // (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command) integer isSimpleAtomicCommand(string cmd) {

   // check right hand side of the "=" - sign
   integer index = llSubStringIndex (cmd, "=");
   if (index > -1) // there is a "=" 
   {
       // check for a number after the "="
       string param = llGetSubString (cmd, index + 1, -1);
       if ((integer)param!=0 || param=="0") // is it an integer (channel number)?
       {
           return TRUE;
       }

       // removing restriction
       if ((param == "y") || (param == "rem"))
       {
           return TRUE;
       }
   }

   // check for a leading ! (meta command)
   if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
   {
       return TRUE;
   }

   // check for @clear
   // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
   // (but we need this check here because "!release|@clear" is a BROKEN attempt to work around
   // a bug in the first relay implementation. You should refuse to use relay versions < 1013
   // instead.)
   if (cmd == "@clear")
   {
       return TRUE;
   }

   // this one is not "simple".
   return FALSE;

}

// If we already have commands from this object pending // because of a permission request dialog, just add the // new commands at the end. // Note: We use a timeout here because the player may // have "ignored" the dialog. integer tryToGluePendingCommands(key id, string commands) {

   if ((sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
   {
       debug("Gluing " + sPendingMessage + " with " + commands);
       sPendingMessage = sPendingMessage + "|" + commands;
       return TRUE;
   }
   return FALSE;

}

// verifies the permission. This includes mode // (off, permission, auto) of the relay and the // identity of the object (owned by parcel people). integer verifyPermission(key id, string name, string message) {

   // is it switched off?
   if (nMode == MODE_OFF)
   {
       return FALSE;
   }

   // extract the commands-part
   list tokens = llParseString2List (message, [","], []);
   if (llGetListLength (tokens) < 3)
   {
       return FALSE;
   }
   string commands = llList2String(tokens, 2);
   list list_of_commands = llParseString2List(commands, ["|"], []);

   // accept harmless commands silently
   if (isSimpleRequest(list_of_commands))
   {
       return TRUE;
   }

   // if we are already having a pending permission-dialog request for THIS object,
   // just add the new commands at the end of the pending command list.
   if (tryToGluePendingCommands(id, commands))
   {
       return FALSE;
   }

   // check whether this object belongs here
   integer trustworthy = isObjectIdentityTrustworthy(id);
   string warning = "";
   if (!trustworthy)
   {
       warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";
   }

   // ask in permission-request-mode and/OR in case the object identity is suspisous.
   if (nMode == MODE_ASK || !trustworthy)
   {
       sPendingId=id;
       sPendingName=name;
       sPendingMessage=message;
       sPendingTime = llGetUnixTime();
       llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
       debug("Asking for permission");
       return FALSE;
   }
   return TRUE;

}


// --------------------------------------------------- // Executing of commands // ---------------------------------------------------

// execute a non-parsed message // this command could be denied here for policy reasons, (if it were implemenetd) // but this time there will be an acknowledgement execute(string name, key id, string message) {

   list tokens=llParseString2List (message, [","], []);
   if (llGetListLength (tokens)==3) // this is a normal command
   {
       string cmd_id=llList2String (tokens, 0); // CheckAttach
       key target=llList2Key (tokens, 1); // UUID
       if (target==llGetOwner ()) // talking to me ?
       {
           list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);
           integer len=llGetListLength (list_of_commands);
           integer i;
           string command;
           string prefix;
           for (i=0; i<len; ++i) // execute every command one by one
           {
               // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
               command=llList2String (list_of_commands, i);
               prefix=llGetSubString (command, 0, 0);

               if (prefix==PREFIX_RL_COMMAND) // this is a RL command
               {
                   executeRLVCommand(cmd_id, id, command);
               }
               else if (prefix==PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
               {
                   executeMetaCommand(cmd_id, id, command);
               }
           }
       }
   }

}

// executes a command for the restrained life viewer // with some additinal magic like book keeping executeRLVCommand(string cmd_id, string id, string command) {

   // we need to know whether whether is a rule or a simple command
   list tokens_command=llParseString2List (command, ["="], []);
   string behav=llList2String (tokens_command, 0); // @getattach:skull
   string param=llList2String (tokens_command, 1); // 2222
   integer ind=llListFindList (lRestrictions, [behav]);

   if (param=="n" || param=="add") // add to lRestrictions
   {
       if (ind<0) lRestrictions+=[behav];
       kSource=id; // we know that kSource is either NULL_KEY or id already
   }
   else if (param=="y" || param=="rem") // remove from lRestrictions
   {
       if (ind > -1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind);
       if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY;
   }

   workaroundForAtClear(command);
   rememberForceSit(command);
   sendRLCmd(command); // execute command
   ack(cmd_id, id, command, "ok"); // acknowledge

}

// check for @clear // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login // (but we need this check here because "!release|@clear" is a BROKEN attempt to work around // a bug in the first relay implementation. You should refuse to use relay versions < 1013 // instead.) workaroundForAtClear(string command) {

   if (command == "@clear")
   {
       releaseRestrictions();
   }

}

// remembers the time and object if this command is a force sit rememberForceSit(string command) {

   list tokens_command=llParseString2List (command, ["="], []);
   string behav=llList2String (tokens_command, 0); // @sit:<uuid>
   string param=llList2String (tokens_command, 1); // force
   if (param != "force")
   {
       return;
   }

   tokens_command=llParseString2List(behav, [":"], []);
   behav=llList2String (tokens_command, 0); // @sit
   param=llList2String (tokens_command, 1); // <uuid>
   debug("'force'-command:" + behav + "/" + param);
   if (behav != "@sit")
   {
       return;
   }
   lastForceSitDestination = (key) param;
   lastForceSitTime = llGetUnixTime();
   debug("remembered force sit");

}

// executes a meta command which is handled by the relay itself executeMetaCommand(string cmd_id, string id, string command) {

   if (command==PREFIX_METACOMMAND+"version") // checking relay version
   {
       ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
   }
   else if (command==PREFIX_METACOMMAND+"release") // release all the restrictions (end session)
   {
       releaseRestrictions();
       ack(cmd_id, id, command, "ok");
   }

}

// lift all the restrictions (called by !release and by turning the relay off) releaseRestrictions () {

   kSource=NULL_KEY;
   integer i;
   integer len=llGetListLength (lRestrictions);
   for (i=0; i<len; ++i)
   {
       sendRLCmd(llList2String (lRestrictions, i)+"=y");
   }
   lRestrictions = [];
   loginPendingForceSit = FALSE;

}


// --------------------------------------------------- // initialisation and login handling // ---------------------------------------------------

init() {

   nMode=1;
   kSource=NULL_KEY;
   lRestrictions=[];
   sPendingId=NULL_KEY;
   sPendingName="";
   sPendingMessage="";
   llListen (RLVRS_CHANNEL, "", "", "");
   llListen (DIALOG_CHANNEL, "", llGetOwner(), "");
   llOwnerSay (getModeDescription());

}

// sends the known restrictions (again) to the RL-viewer // (call this functions on login) reinforceKnownRestrictions() {

   integer i;
   integer len=llGetListLength(lRestrictions);
   string restr;
   debug("kSource=" + (string) kSource);
   for (i=0; i<len; ++i)
   {
       restr=llList2String(lRestrictions, i);
       debug("restr=" + restr);
       sendRLCmd(restr+"=n");
       if (restr=="@unsit")
       {
           loginPendingForceSit = TRUE;
       }
   }

}

// send a ping request and start a timer pingWorldObjectIfUnderRestrictions() {

   loginWaitingForPong = FALSE;
   if (kSource != NULL_KEY)
   {
       ack("ping", kSource, "ping", "ping");
       timerTickCounter = 0;
       llSetTimerEvent(1.0);
       loginWaitingForPong = TRUE;
   }

}

default {

   state_entry()
   {
       init();
   }

   on_rez(integer start_param)
   {
       // relogging, we must refresh the viewer and ping the object if any
       // if mode is not OFF, fire all the stored restrictions
       if (nMode)
       {
           reinforceKnownRestrictions();
           pingWorldObjectIfUnderRestrictions();
       }
       // remind the current mode to the user
       llOwnerSay(getModeDescription());
   }


   timer()
   {
       timerTickCounter++;
       debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
       if (loginWaitingForPong && (timerTickCounter == LOGIN_DELAY_WAIT_FOR_PONG))
       {
           llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available.");
           loginWaitingForPong = FALSE;
           loginPendingForceSit = FALSE;
           releaseRestrictions();
       }

       if (loginPendingForceSit)
       {
           integer agentInfo = llGetAgentInfo(llGetOwner());
           if (agentInfo & AGENT_SITTING)
           {
               loginPendingForceSit = FALSE;
               debug("is sitting now");
           }
           else if (timerTickCounter == LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
           {
               llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
               loginPendingForceSit = FALSE;
               releaseRestrictions();
           }
           else
           {
                sendRLCmd ("@sit:"+(string)kSource+"=force");
           }
       }

       if (!loginPendingForceSit && !loginWaitingForPong)
       {
           llSetTimerEvent(0.0);
       }
   }

   listen(integer channel, string name, key id, string message)
   {
       if (channel==RLVRS_CHANNEL)
       {
           if (!verifyWeAreTarget(message))
           {
              return;
           }

           if (nMode== MODE_OFF)
           {
               debug("deactivated - ignoring commands");
               return; // mode is 0 (off) => reject
           }

//~ if (!isObjectNear(id)) return;

           debug("Got message (active world object " + (string) kSource + "): name=" + name+ "id=" + (string) id + " message=" + message);

           if (kSource != NULL_KEY && kSource != id)
           {
               debug("already used by another object => reject");
               return;
           }

           loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request

           if (!isObjectKnow(id))
           {
               debug("asking for permission because kSource is NULL_KEY");
               if (!verifyPermission(id, name, message))
               {
                   return;
               }
           }

           debug("Executing: " + (string) kSource);
           execute(name, id, message);
       }
       else if (channel==DIALOG_CHANNEL)
       {
           if (id != llGetOwner())
           {
               return; // only accept dialog responses from the owner
           }
           if (sPendingId!=NULL_KEY)
           {
               if (message=="Yes") // pending request authorized => process it
               {
                   execute(sPendingName, sPendingId, sPendingMessage);
               }

               // clear pending request
               sPendingName="";
               sPendingId=NULL_KEY;
               sPendingMessage="";
           }
       }
   }

   touch_start(integer num_detected)
   {
       // touched by user => cycle through OFF/ON_PERMISSION/ON_ALWAYS modes
       key toucher=llDetectedKey(0);
       if (toucher==llGetOwner())
       {
           if (kSource != NULL_KEY)
           {
               llOwnerSay("Sorry, you cannot change the relay mode while it is locked.");
               return;
           }
           ++nMode;
           if (nMode>2) nMode=0;
           if (nMode==MODE_OFF) releaseRestrictions ();
           llOwnerSay (getModeDescription());
       }
   }

   changed(integer change)
   {
       if (change & CHANGED_OWNER) 
       {
            llResetScript();
       }
   }

}

</lsl>