Difference between revisions of "LSL Protocol/Restrained Living Relay/Other Implementations/Maike Short's Relay"

From Second Life Wiki
Jump to navigation Jump to search
m (change log)
(version 1.040)
Line 4: Line 4:


== Changes ==
== Changes ==
=== 1.040 ===
* added support for !who which allows world objects to tell who controls them
* added support for !handover to support inter-sim kidnappers leading directly into a trap and processing facilities that hand over the victim from one step to the next
* added support for [[LSL_Protocol/Restrained_Life_Relay/Other_Implementations/The_Think_Kink_PBA#The_GPU|!vision script]] from the Think Kink PBA by Ilana Debevec and Chloe1982 Constantine)


=== 1.030.c ===
=== 1.030.c ===
Line 9: Line 16:
* fixed group check on parcels not set to a group (which matched objects set to (none), too (reported by Kitty Barnett)
* fixed group check on parcels not set to a group (which matched objects set to (none), too (reported by Kitty Barnett)
* fixed <0, 0, 0> pseudo object position in distance check (reported by Kitty Barnett).
* fixed <0, 0, 0> pseudo object position in distance check (reported by Kitty Barnett).


=== 1.030.b ===
=== 1.030.b ===
Line 44: Line 50:


<lsl>
<lsl>
//~ RestrainedLife Viewer Relay Script example code
// RestrainedLife Viewer Relay Script
//~ By Marine Kelley, Maike Short, Felis Darwin
//
//~ Thanks to Chorazin Allen, Azoth Amat, Gregor Mougin, Nano Siemens,
// By Maike Short
//~ Cerdita Piek, Satomi Ahn, Marissa Mistwallow, Kitty Barnett
//
 
// While this script used to be based on Marine's sample implementations it was
//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy,
// so heavily modified and extended that there is little of the original code
//~ completeness or performance. It may only be distributed in its full source code,
// remaining. So please do not bother Marine with any problems but report them to
//~ this header and disclaimer and is not to be sold.
// https://wiki.secondlife.com/wiki/LSL_Protocol/Restrained_Life_Relay/Bugs_and_Pendings_Features
//
// Thanks to Felis Darwin, Chorazin Allen, Azoth Amat, Gregor Mougin,
// Nano Siemens, Cerdita Piek, Satomi Ahn, Marissa Mistwallow,
// Chloe1982 Constantine, Ilana Debevec, Kitty Barnett and many others.
//
// Many thanks to she who started it all ... Marine Kelley.
//
// This script is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy, completeness
// or performance. It may be distributed in its full source code with this header and
// disclaimer and is not to be sold without permission. Optionally it may be distributed
// in a 'no mod' form within Second Life™ PROVIDED that either a link to the full source
// IS provided (ie. this page or a .zip or .rar) within the object the code is contained
// AND/OR a off-world contact point (ie. email) that the source code may be requested from.
// This somewhat unusual (no-mod) distribution is allowed to maintain the integrity and
// reliability of the creator reference of in-world objects (scripts). Changes and
// improvement to this code must be shared with the community so that we ALL benefit.


integer DEBUG = FALSE;
integer DEBUG = FALSE;
// make cheating (adding exceptions) a bit more difficult by not allowing
// attachment to control the viewer as they are not subjected to land building
// restrictions
integer ALLOW_CONTROL_BY_ATTACHMENTS = TRUE;




Line 68: Line 85:
//                    Constants
//                    Constants
// ---------------------------------------------------
// ---------------------------------------------------
 
integer RLVRS_PROTOCOL_VERSION = 1030; // version of the protocol, stated on the specification page
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page
string RLVRS_IMPL_VERSION = "Based on Maike's Implemenation 1.030.c"; // version of the implementation for debugging
string RLVRS_IMPL_VERSION = "Based on Maike's Implemenation 1.040"; // 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 STATUS_LINK_CHANNEL = -1373421300;
Line 82: Line 99:
integer DIALOG_LINK_CHANNEL = -1373421303;
integer DIALOG_LINK_CHANNEL = -1373421303;
integer ASK_LINK_CHANNEL = -1373421304;
integer ASK_LINK_CHANNEL = -1373421304;
integer GPU_CHANNEL      = -4360493;


integer MAX_OBJECT_DISTANCE = 100;    // 100m is llShout distance
integer MAX_OBJECT_DISTANCE = 100;    // 100m is llShout distance
Line 87: Line 106:


integer PERMISSION_DIALOG_TIMEOUT = 30;
integer PERMISSION_DIALOG_TIMEOUT = 30;
integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;


Line 102: Line 121:
//                      Variables
//                      Variables
// ---------------------------------------------------
// ---------------------------------------------------
 
integer mode;
integer mode;


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


string pendingName; // name of initiator of pending request (first request of a session in mode 1)
string pendingName; // name of initiator of pending request (first request of a session in ask mode)
key pendingId;      // UUID 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 ask mode)
string pendingMessage; // message of pending request
string pendingMessage; // message of pending request
integer pendingTime;
integer pendingTime;
Line 122: Line 142:


integer isNotEmbedded = TRUE;
integer isNotEmbedded = TRUE;
// current message
// using global variables because the content can get huge and uses up a lot stack memory otherwise
string currentMessage = "";
list currentMessageToken = [];
string currentMessageCommands;
list currentMessageCommandList;


// ---------------------------------------------------
// ---------------------------------------------------
Line 134: Line 162:
         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));
}
}


Line 150: Line 169:
// ---------------------------------------------------
// ---------------------------------------------------


 
// 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)
{
{
     llShout(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)
{
{
Line 170: Line 189:
}
}


// splits the relay message into tokens
splitMessage()
{
    currentMessageToken = llParseString2List(currentMessage, [","], []);
    currentMessageCommands = llList2String(currentMessageToken, 2);
    currentMessageCommandList = llParseString2List(currentMessageCommands, ["|"], []);
}


// 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()
{
{
    list tokens = llParseString2List(message, [","], []);
     if (llGetListLength(currentMessageToken) != 3) // this is not a normal command
     if (llGetListLength(tokens) != 3) // this is not a normal command
     {
     {
         return FALSE;
         return FALSE;
     }
     }


     return (llList2String(tokens, 1) == llGetOwner()); // talking to me ?
     return (llList2String(currentMessageToken, 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);
}


// ---------------------------------------------------
// ---------------------------------------------------
Line 209: Line 224:


     // 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?
    debug("source=" + (string) source + " id=" + (string) id + " lastForceSitDestination=" + (string) lastForceSitDestination);
     if ((source == NULL_KEY) && (id == lastForceSitDestination))
     if ((source == NULL_KEY) && (id == lastForceSitDestination))
     {
     {
Line 224: Line 240:


// check whether the object is in llShout distance.
// check whether the object is in llShout distance.
integer isObjectNear(key id)
integer isObjectNear(key id)
{
{
    vector myPosition = llGetRootPosition();
    vector myPosition = llGetRootPosition();
    list temp = llGetObjectDetails(id, ([OBJECT_POS]));
    list temp = llGetObjectDetails(id, ([OBJECT_POS]));
    vector objPosition = llList2Vector(temp,0);
    vector objPosition = llList2Vector(temp,0);
    if (objPosition == <0, 0, 0>)
    if (objPosition == <0, 0, 0>)
    {
    {
        // object is not in this sim nor in one of the adjacent sims
        // object is not in this sim nor in one of the adjacent sims
        return FALSE;
        return FALSE;
    }
    }
    float distance = llVecDist(objPosition, myPosition);
    float distance = llVecDist(objPosition, myPosition);
    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
Line 249: Line 265:
     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
       || (object_group==parcel_group && parcel_group != NULL_KEY)   // OR its group is the same as the parcel I'm on
       || (object_group==parcel_group && parcel_group != NULL_KEY)       // OR its group is the same as the parcel I'm on
     )
     )
     {
     {
Line 261: Line 277:


// 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()  
{
{
     integer len = llGetListLength(list_of_commands);
     integer len = llGetListLength(currentMessageCommandList);
     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)
     {
     {
        string command = llList2String(list_of_commands, i);
         if (!isSimpleAtomicCommand(i))
         if (!isSimpleAtomicCommand(command))
         {
         {
           return FALSE;
           return FALSE;
Line 283: Line 297:
// (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)
integer isSimpleAtomicCommand(string cmd)
integer isSimpleAtomicCommand(integer i)
{
{
    string cmd = llList2String(currentMessageCommandList, i);
    cmd = llToLower(llStringTrim(cmd, STRING_TRIM));
     // check right hand side of the "=" - sign
     // check right hand side of the "=" - sign
     integer index = llSubStringIndex (cmd, "=");
     integer index = llSubStringIndex (cmd, "=");
     if (index > -1) // there is a "="
     if (index > -1) // there is a "="  
     {
     {
         // check for a number after the "="
         // check for a number after the "="
Line 306: Line 322:
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
     {
     {
         return TRUE;
         return llGetSubString(cmd, 0, 11) == "!visionclear"
            || llGetSubString(cmd, 0, 6) != "!vision";
     }
     }


Line 318: Line 335:
         return TRUE;
         return TRUE;
     }
     }
 
 
     // this one is not "simple".
     // this one is not "simple".
     return FALSE;
     return FALSE;
Line 328: Line 345:
// Note: We use a timeout here because the player may
// Note: We use a timeout here because the player may
// have "ignored" the dialog.
// have "ignored" the dialog.
integer tryToGluePendingCommands(key id, string commands)
integer tryToGluePendingCommands(key id)
{
{
     if (llStringLength(pendingMessage) > 4000)
     if (llStringLength(pendingMessage) > 2000)
     {
     {
         llSay(0, llKey2Name(id) + " is flooding commands. Releasing restrictions.");
         llSay(0, llKey2Name(id) + " is flooding commands. Releasing restrictions.");
Line 338: Line 355:
     if ((pendingId == id) && (pendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
     if ((pendingId == id) && (pendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
     {
     {
         debug("Gluing " + (string) pendingMessage + " with " + commands);
         debug("Gluing ");
         pendingMessage = pendingMessage + "|" + commands;
         pendingMessage = pendingMessage + "|" + currentMessageCommands;
         return TRUE;
         return TRUE;
     }
     }
Line 347: Line 364:


// accept !release even if out of range
// accept !release even if out of range
handleCommandsWhichAreAcceptedOutOfRange(key id, string message)
handleCommandsWhichAreAcceptedOutOfRange(key id)
{
{
     if (id != source)
     if (id != source)
Line 354: Line 371:
     }
     }


    list tokens = llParseString2List (message, [","], []);
     if (llGetListLength (currentMessageToken) < 3)
     if (llGetListLength (tokens) < 3)
     {
     {
         return;
         return;
     }
     }
    string commands = llList2String(tokens, 2);
     if (llListFindList(currentMessageCommandList, ["!release"]) > -1)
    list list_of_commands = llParseString2List(commands, ["|"], []);
     if (llListFindList(list_of_commands, ["!release"]) > -1)
     {
     {
         debug("accepted !release although it was out of range");
         debug("accepted !release although it was out of range");
Line 369: Line 383:




// 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
// identity of the object (owned by parcel people).
// identity of the object (owned by parcel people).
integer verifyPermission(key id, string name, string message)
integer verifyPermission(key id, string name)
{
{
     // is it switched off?
     // is it switched off?
Line 380: Line 394:
     }
     }


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


     // 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.
     if (tryToGluePendingCommands(id, commands))
     if (tryToGluePendingCommands(id))
     {
     {
         return FALSE;
         return FALSE;
Line 396: Line 407:


     // accept harmless commands silently
     // accept harmless commands silently
     if (isSimpleRequest(list_of_commands))
     if (isSimpleRequest())
     {
     {
         return TRUE;
         return TRUE;
Line 412: Line 423:
     if (mode == MODE_ASK || !trustworthy)
     if (mode == MODE_ASK || !trustworthy)
     {
     {
        debug("asking");
         pendingId=id;
         pendingId=id;
         pendingName=name;
         pendingName=name;
         pendingMessage = message;
 
        currentMessageToken = [];
        currentMessageCommands = "";
        currentMessageCommandList = [];
 
         pendingMessage = currentMessage;
        currentMessage = "";
 
         pendingTime = llGetUnixTime();
         pendingTime = llGetUnixTime();
         if (llKey2Name(llGetOwnerKey(id)) != "")
         if (llKey2Name(llGetOwnerKey(id)) != "")
Line 423: Line 442:
         list temp = llGetObjectDetails(id, [OBJECT_POS]);
         list temp = llGetObjectDetails(id, [OBJECT_POS]);
         vector pos = llList2Vector(temp, 0);
         vector pos = llList2Vector(temp, 0);
 
        string controller = getControllerName();
         string text = name
       
         string text = name  
             + " at <" + (string) ((integer) pos.x)
             + " at <" + (string) ((integer) pos.x)
             + ", " + (string) ((integer) pos.y)
             + ", " + (string) ((integer) pos.y)
             + ", " + (string) ((integer) pos.z)
             + ", " + (string) ((integer) pos.z)
             + "> (" + (string) ((integer) llVecDist(pos, llGetRootPosition())) + "m) "
             + "> (" + (string) ((integer) llVecDist(pos, llGetRootPosition())) + "m)";
             + "would like to control your viewer." + warning + ".\n\nDo you accept ?";
        if (controller != "")
        {
             text = text + " operated by " + controller;
        }       
        text = text + " would like to control your viewer." + warning + "\n\nDo you accept ?";
 
         debug("Asking for permission isNotEmbedded=" + (string) isNotEmbedded);
         debug("Asking for permission isNotEmbedded=" + (string) isNotEmbedded);
         if (isNotEmbedded)
         if (isNotEmbedded)
Line 442: Line 467:
}
}


// gets the name of the last controller or ""
// if there is either no !who-command, it is for NULL_KEY,
// or the avatar is not in the sim
string getControllerName()
{
    string controller = "";
    integer i;
    integer count = llGetListLength(currentMessageCommandList);
    for (i = 0; i < count; i++)
    {
        string command = llList2String(currentMessageCommandList, i);
        list commandTokens = llParseString2List(command, ["/"], []);
        if (llList2String(commandTokens, 0) == "!who")
        {
            controller = llKey2Name(llList2String(commandTokens, 1));
        }
    }
    return controller;
}


// ---------------------------------------------------
// ---------------------------------------------------
Line 448: Line 492:


// 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
// but this time there will be an acknowledgement
execute(string name, key id)
execute(string name, key id, string message)
{
{
     list tokens = llParseString2List(message, [","], []);
     currentMessage = "";
     string cmd_id = llList2String(tokens, 0); // CheckAttach
    currentMessageCommands = "";
     list list_of_commands = llParseString2List(llList2String(tokens, 2), ["|"], []);
     string cmd_id = llList2String(currentMessageToken, 0); // CheckAttach
     currentMessageToken = [];


     integer len = llGetListLength (list_of_commands);
     integer len = llGetListLength (currentMessageCommandList);
     integer i;
     integer i;
     string command;
     string command;
Line 463: Line 507:
     {
     {
         // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
         // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
         command = llList2String(list_of_commands, i);
         command = llList2String(currentMessageCommandList, i);
         prefix = llGetSubString(command, 0, 0);
         prefix = llGetSubString(command, 0, 0);


Line 472: Line 516:
         else if (prefix == PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
         else if (prefix == PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
         {
         {
             executeMetaCommand(cmd_id, id, command);
             integer continue = executeMetaCommand(cmd_id, id, command);
            if (!continue)
            {
                return;
            }
         }
         }
     }
     }
}
}


// 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)
Line 491: Line 539:
     string behavName = llList2String (tokens, 0);  // @sit
     string behavName = llList2String (tokens, 0);  // @sit
     string option = llList2String (tokens, 1);    // <uuid>
     string option = llList2String (tokens, 1);    // <uuid>
 
   
     debug("executeRLVCommand: behav=!" + behav + "! param=!" + param + "!");
     debug("executeRLVCommand: behav=!" + behav + "! param=!" + param + "!");


Line 507: Line 555:
         if (ind < 0)
         if (ind < 0)
         {
         {
             if (llGetListLength(restrictions) == 0)
             if ((llGetListLength(restrictions) == 0) && !visionRestricted)
             {
             {
                 sendRLCmd(RVL_COMMAND_START);
                 sendRLCmd(RVL_COMMAND_START);
                 llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
                 llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
             }
             }  
             restrictions += [behav];
             restrictions += [behav];
         }
         }
Line 523: Line 571:
             return;
             return;
         }
         }
 
       
         if (ind > -1)
         if (ind > -1)  
         {
         {
             restrictions = llDeleteSubList (restrictions, ind, ind);
             restrictions = llDeleteSubList (restrictions, ind, ind);
         }
         }
         if (llGetListLength(restrictions) == 0)
         if ((llGetListLength(restrictions) == 0) && !visionRestricted)
         {
         {
             source = NULL_KEY;
             releaseRestrictions();
            sendRLCmd(RVL_COMMAND_END);
         }
         }
         removeFromPendingList(behav);
         removeFromPendingList(behav);
Line 562: Line 609:
integer isQuery(string behav)
integer isQuery(string behav)
{
{
     if (llGetSubString(behav, 0, 3) == "@get")
     return ((llGetSubString(behav, 0, 3) == "@get") ||
    {
            (llGetSubString(behav, 0, 7) == "@version") ||
        return TRUE;
            (llGetSubString(behav, 0, 10) == "@findfolder"));
    }
    if (llGetSubString(behav, 0, 7) == "@version")
    {
        return TRUE;
    }
    if (llGetSubString(behav, 0, 10) == "@findfolder")
    {
        return TRUE;
    }
    return FALSE;
}
}


Line 607: Line 644:
         }
         }
     }
     }
 
   
     if (param != "force")
     if (param != "force")
     {
     {
Line 624: Line 661:


// 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)
integer executeMetaCommand(string cmd_id, string id, string commandString)
{
{
    list tokens = llParseString2List(commandString, ["/"], []);
    string command = llList2String(tokens, 0);
   
     if (command == PREFIX_METACOMMAND + "version") // checking relay protocol version
     if (command == PREFIX_METACOMMAND + "version") // checking relay protocol version
     {
     {
Line 639: Line 679:
         ack(cmd_id, id, command, "ok");
         ack(cmd_id, id, command, "ok");
     }
     }
     else if (command == PREFIX_METACOMMAND + "pong")
    else if (command == PREFIX_METACOMMAND + "handover")
    {
        handleHandOver(id, tokens);
        return FALSE;
    }
     else if (command == PREFIX_METACOMMAND + "pong") // object is still available for us
     {
     {
         loginWaitingForPong = FALSE;
         loginWaitingForPong = FALSE;
     }
     }
    else if (llGetSubString(command, 0, 11) == PREFIX_METACOMMAND + "visionclear")
    {
        visionRestricted = FALSE;
        ack(cmd_id, id, command, "ok");
        if (llGetListLength(restrictions) == 0)
        {
            releaseRestrictions();
      }
      llMessageLinked(LINK_SET, GPU_CHANNEL, commandString, source);
    }
    else if (llGetSubString(command, 0, 6) == PREFIX_METACOMMAND + "vision")
    {
        integer attached = llGetAttached();
        if (attached >= ATTACH_HUD_CENTER_2 || attached < ATTACH_HUD_BOTTOM_RIGHT)
        {
            if (llGetListLength(restrictions) == 0)
            {
                llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
                sendRLCmd(RVL_COMMAND_START);
            }
            llMessageLinked(LINK_SET, GPU_CHANNEL, commandString, source);
            ack(cmd_id, id, command, "ok");
            visionRestricted = TRUE;
            source = id;
        }
        else
        {
            ack(cmd_id, id, command, "ko");
        }
    }
    else
    {
        ack(cmd_id, id, command, "ko");
    }
    return TRUE;
}
handleHandOver(string id, list tokens)
{
    debug("handover");
    key targetObject = (key) llList2String(tokens, 1);
    integer keepRestrictions = (integer) llList2String(tokens, 2);
    if (!keepRestrictions)
    {
        releaseRestrictions();
    }
    source = targetObject;
    if (llGetListLength(restrictions) == 0)
    {
        sendRLCmd(RVL_COMMAND_START);
        llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
    }
    pingWorldObjectIfUnderRestrictions();
}
}


Line 650: Line 749:
     string search = behav + "=";
     string search = behav + "=";


     // don't do the expensive parsing (string operations are very slow in pre-
     // 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.
     // mono LSL) in case we can detect fast that this one is not in the list.
     if (llSubStringIndex(pendingMessage, search) < 0)
     if (llSubStringIndex(pendingMessage, search) < 0)
Line 672: Line 771:
                 commands = llDeleteSubList(commands, i, i);
                 commands = llDeleteSubList(commands, i, i);
                 modified = TRUE;
                 modified = TRUE;
             }
             }  
         }
         }
     }
     }
Line 690: Line 789:
}
}


 
// 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()
Line 705: Line 804:
     loginPendingForceSit = FALSE;
     loginPendingForceSit = FALSE;
     clearPendingMessages();
     clearPendingMessages();
    visionRestricted = FALSE;
    llMessageLinked(LINK_SET, GPU_CHANNEL, "!visionclear", source);
     llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "release", source);
     llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "release", source);
     source = NULL_KEY;
     source = NULL_KEY;
Line 732: Line 833:


// processes a message send on the relay channel
// processes a message send on the relay channel
processRelayMessage(string name, key id, string message)
processRelayMessage(string name, key id)
{
{
     if (mode== MODE_OFF)
     if (mode== MODE_OFF)
Line 740: Line 841:
     }
     }


     debug("LISTEN: " + message);
     debug("LISTEN: " + currentMessage);
    splitMessage();


     if (!verifyWeAreTarget(message))
     if (!verifyWeAreTarget())
     {
     {
       return;
       return;
     }
     }


    if (!verifySourceType(id))
     debug("Got message (active world object " + (string) source + "): name=" + name+ " id=" + (string) id);
    {
        return;
    }
 
     debug("Got message (active world object " + (string) source + "): name=" + name+ " id=" + (string) id + " message=" + message);
 
     if (source)
     if (source)
     {
     {
Line 771: Line 868:
     }
     }


     if (!isObjectNear(id))
     if (!isObjectNear(id))  
     {
     {
         handleCommandsWhichAreAcceptedOutOfRange(id, message);
         handleCommandsWhichAreAcceptedOutOfRange(id);
         return;
         return;
     }
     }
Line 780: Line 877:
     {
     {
         debug("asking for permission because source is NULL_KEY");
         debug("asking for permission because source is NULL_KEY");
         if (!verifyPermission(id, name, message))
         if (!verifyPermission(id, name))
         {
         {
             return;
             return;
Line 787: Line 884:


     debug("Executing: " + (string) source);
     debug("Executing: " + (string) source);
     execute(name, id, message);
     execute(name, id);
}
}


Line 814: Line 911:
         if (message == "Yes") // pending request authorized => process it
         if (message == "Yes") // pending request authorized => process it
         {
         {
             execute(pendingName, pendingId, pendingMessage);
            debug("recovering pendingMessage");
            currentMessage = pendingMessage;
            debug("splitting");
            splitMessage();
            debug("executing");
             execute(pendingName, pendingId);
         }
         }
         clearPendingMessages();
         clearPendingMessages();
Line 821: Line 923:




 
// ---------------------------------------------------
// ---------------------------------------------------
//            initialisation and login handling
//            initialisation and login handling
// ---------------------------------------------------
// ---------------------------------------------------
 
init() {
init() {
     sendRLCmd("@clear");
     sendRLCmd("@clear");
Line 965: Line 1,067:
             return;
             return;
         }
         }
         mode = ((integer) ((string) id)) % 3;
         mode = ((integer) ((string) id)) % 3;  
         showModeDescription();
         showModeDescription();
         return;
         return;
Line 990: Line 1,092:
         init();
         init();
     }
     }
 
     on_rez(integer start_param)
     on_rez(integer start_param)
     {
     {
Line 1,003: Line 1,105:
         showModeDescription();
         showModeDescription();
     }
     }
 
   
     attach(key id)
     attach(key id)
     {
     {
Line 1,020: Line 1,122:
     listen(integer channel, string name, key id, string message)
     listen(integer channel, string name, key id, string message)
     {
     {
        currentMessageToken = [];
        currentMessageCommands = "";
        currentMessageCommandList = [];
        currentMessage = message;
        message = ""; // free memory
         if (channel==RLVRS_CHANNEL)
         if (channel==RLVRS_CHANNEL)
         {
         {
             processRelayMessage(name, id, message);
             processRelayMessage(name, id);
         }
         }
         else if (channel==DIALOG_CHANNEL)
         else if (channel==DIALOG_CHANNEL)
Line 1,029: Line 1,138:
         }
         }
     }
     }
 
   
     link_message(integer sender, integer channel, string message, key id)
     link_message(integer sender, integer channel, string message, key id)
     {
     {
        currentMessageToken = [];
        currentMessageCommands = "";
        currentMessageCommandList = [];
         if (channel == CMD_LINK_CHANNEL)
         if (channel == CMD_LINK_CHANNEL)
         {
         {
Line 1,062: Line 1,175:
         }
         }
         mode = (mode + 1) % 3;
         mode = (mode + 1) % 3;
         if (mode == MODE_OFF) {
         if (mode == MODE_OFF)
        {
             releaseRestrictions();
             releaseRestrictions();
         }
         }
Line 1,070: Line 1,184:
     changed(integer change)
     changed(integer change)
     {
     {
         if (change & (CHANGED_OWNER | CHANGED_INVENTORY | CHANGED_ALLOWED_DROP))
         if (change & (CHANGED_OWNER | CHANGED_INVENTORY | CHANGED_ALLOWED_DROP))  
         {
         {
             llResetScript();
             llResetScript();
        }
        if (change & (CHANGED_REGION | CHANGED_TELEPORT))
        {
            if (loginWaitingForPong)
            {
                if (source)
                {
                    ack("ping", source, "ping", "ping");
                }
            }
         }
         }
     }
     }
}
}
</lsl>
</lsl>

Revision as of 08:21, 6 March 2009

You can get a free relay object at the MSM Restraints shop in "Stonehaven". http://slurl.com/secondlife/Stonehaven%20Island/155/79/301

Changes

1.040

  • added support for !who which allows world objects to tell who controls them
  • added support for !handover to support inter-sim kidnappers leading directly into a trap and processing facilities that hand over the victim from one step to the next
  • added support for !vision script from the Think Kink PBA by Ilana Debevec and Chloe1982 Constantine)


1.030.c

  • Diff
  • fixed group check on parcels not set to a group (which matched objects set to (none), too (reported by Kitty Barnett)
  • fixed <0, 0, 0> pseudo object position in distance check (reported by Kitty Barnett).

1.030.b

  • Diff
  • fixed two faked avatar chat problems

1.030.a

  • Diff
  • Fixed the compatibility code for world objects that send @clear instead of !release broken by the security fix in 1.020.b

1.030

  • Diff
  • added "Temp Mute" in the ask dialog which will mute the object until the next time you login (in the extended controller script)
  • tell world objects if an active session is canceled by the relay.


1.020.c

  • Diff
  • Display position and distance in permission dialog

1.020.b

  • Diff
  • This is based on 1.015 and has all the fixed made there missing in Marine's 1.020 which is based on a very early version of 1.014
  • Filter automatic RLV replies on public chat channel 0 so that people cannot be tricked to say some foreign text out loud
  • Added "this-is-a-script-generated-message-beyond-the-control-of-the-agent/" at the beginning of @getstatus-replies on all channel
  • Note: Relays which do not add any restriction on their own (like @detach=n) may be abused using @gestatus to trigger dialog responses, gag talk or other scripts like "to buy as gift, say the name of the receiver on channel /x". Any version of the reference implementation smaller than (not including) 1.015 (but including 1.020 which is based on 1.014) are affected by this.

1.015

Code

<lsl> // RestrainedLife Viewer Relay Script // // By Maike Short // // While this script used to be based on Marine's sample implementations it was // so heavily modified and extended that there is little of the original code // remaining. So please do not bother Marine with any problems but report them to // https://wiki.secondlife.com/wiki/LSL_Protocol/Restrained_Life_Relay/Bugs_and_Pendings_Features // // Thanks to Felis Darwin, Chorazin Allen, Azoth Amat, Gregor Mougin, // Nano Siemens, Cerdita Piek, Satomi Ahn, Marissa Mistwallow, // Chloe1982 Constantine, Ilana Debevec, Kitty Barnett and many others. // // Many thanks to she who started it all ... Marine Kelley. // // This script is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy, completeness // or performance. It may be distributed in its full source code with this header and // disclaimer and is not to be sold without permission. Optionally it may be distributed // in a 'no mod' form within Second Life™ PROVIDED that either a link to the full source // IS provided (ie. this page or a .zip or .rar) within the object the code is contained // AND/OR a off-world contact point (ie. email) that the source code may be requested from. // This somewhat unusual (no-mod) distribution is allowed to maintain the integrity and // reliability of the creator reference of in-world objects (scripts). Changes and // improvement to this code must be shared with the community so that we ALL benefit.

integer DEBUG = FALSE;


// list of refused commands. Use [] to allow everything or // ["@detach", "@remoutfit"] to prevent striping for example list refusedForceCommands = [];

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

integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page string RLVRS_IMPL_VERSION = "Based on Maike's Implemenation 1.040"; // version of the implementation for debugging

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

integer RLVRS_CHANNEL = -1812221819; // RLVRS in numbers integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers integer STATUS_LINK_CHANNEL = -1373421300; integer RLV_LINK_CHANNEL = -1373421301; integer CMD_LINK_CHANNEL = -1373421302; integer DIALOG_LINK_CHANNEL = -1373421303; integer ASK_LINK_CHANNEL = -1373421304; integer GPU_CHANNEL = -4360493;


integer MAX_OBJECT_DISTANCE = 100; // 100m is llShout distance integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds

integer PERMISSION_DIALOG_TIMEOUT = 30; integer LOGIN_DELAY_WAIT_FOR_PONG = 20; integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;

integer MODE_OFF = 0; integer MODE_ASK = 1; 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 = "@this-is-a-script-generated-message-beyond-the-control-of-the-agent=n,detach=n"; string RVL_COMMAND_END = "@detach=y";

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

integer mode;

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

string pendingName; // name of initiator of pending request (first request of a session in ask mode) key pendingId; // UUID of initiator of pending request (first request of a session in ask mode) string pendingMessage; // message of pending request integer pendingTime;

// 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;

integer isNotEmbedded = TRUE;


// current message // using global variables because the content can get huge and uses up a lot stack memory otherwise string currentMessage = ""; list currentMessageToken = []; string currentMessageCommands; list currentMessageCommandList;

// --------------------------------------------------- // Helper functions // ---------------------------------------------------


debug(string x) {

   if (DEBUG)
   {
       llOwnerSay("DEBUG: " + x);
   }

}


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


// 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) {

   if (cmd != "")
   {
       if (isNotEmbedded)
       {
           llOwnerSay(cmd);
       }
       llMessageLinked(LINK_SET, RLV_LINK_CHANNEL, cmd, source);
   }

}

// splits the relay message into tokens splitMessage() {

   currentMessageToken = llParseString2List(currentMessage, [","], []);
   currentMessageCommands = llList2String(currentMessageToken, 2);
   currentMessageCommandList = llParseString2List(currentMessageCommands, ["|"], []);

}

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

   if (llGetListLength(currentMessageToken) != 3) // this is not a normal command
   {
       return FALSE;
   }
   return (llList2String(currentMessageToken, 1) == llGetOwner()); // talking to me ?

}


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


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

   // are we already under command by this object?
   if (source == id)
   {
       return TRUE;
   }
   // are we not under command by any object but were we forced to sit on this object recently?
   debug("source=" + (string) source + " id=" + (string) id + " lastForceSitDestination=" + (string) lastForceSitDestination);
   if ((source == 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.

integer isObjectNear(key id)
{
    vector myPosition = llGetRootPosition();
    list temp = llGetObjectDetails(id, ([OBJECT_POS]));
    vector objPosition = llList2Vector(temp,0);
    if (objPosition == <0, 0, 0>)
    {
        // object is not in this sim nor in one of the adjacent sims
        return FALSE;
    }
    float distance = llVecDist(objPosition, 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 && parcel_group != NULL_KEY)        // 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() {

   integer len = llGetListLength(currentMessageCommandList);
   integer i;
   // now check every single atomic command
   for (i=0; i < len; ++i)
   {
       if (!isSimpleAtomicCommand(i))
       {
          return FALSE;
       }
   }
   // all atomic commands passed the test
   return TRUE;

}

// is this a simple atomar 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(integer i) {

   string cmd = llList2String(currentMessageCommandList, i);
   cmd = llToLower(llStringTrim(cmd, STRING_TRIM)); 
   // 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 llGetSubString(cmd, 0, 11) == "!visionclear"
            || llGetSubString(cmd, 0, 6) != "!vision";
   }
   // 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) {

   if (llStringLength(pendingMessage) > 2000)
   {
       llSay(0, llKey2Name(id) + " is flooding commands. Releasing restrictions.");
       releaseRestrictions();
       return TRUE;
   }
   if ((pendingId == id) && (pendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
   {
       debug("Gluing ");
       pendingMessage = pendingMessage + "|" + currentMessageCommands;
       return TRUE;
   }
   return FALSE;

}


// accept !release even if out of range handleCommandsWhichAreAcceptedOutOfRange(key id) {

   if (id != source)
   {
       return;
   }
   if (llGetListLength (currentMessageToken) < 3)
   {
       return;
   }
   if (llListFindList(currentMessageCommandList, ["!release"]) > -1)
   {
       debug("accepted !release although it was out of range");
       releaseRestrictions();
   }

}


// 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) {

   // is it switched off?
   if (mode == MODE_OFF)
   {
       return FALSE;
   }
   if (llGetListLength (currentMessageToken) < 3)
   {
       return FALSE;
   }
   // 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))
   {
       return FALSE;
   }
   // accept harmless commands silently
   if (isSimpleRequest())
   {
       return TRUE;
   }
   // 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 (mode == MODE_ASK || !trustworthy)
   {
       debug("asking");
       pendingId=id;
       pendingName=name;
       currentMessageToken = [];
       currentMessageCommands = "";
       currentMessageCommandList = [];
       pendingMessage = currentMessage;
       currentMessage = "";
       pendingTime = llGetUnixTime();
       if (llKey2Name(llGetOwnerKey(id)) != "")
       {
           name += " (owned by " + llKey2Name(llGetOwnerKey(id)) + ")";
       }
       list temp = llGetObjectDetails(id, [OBJECT_POS]);
       vector pos = llList2Vector(temp, 0);
       string controller = getControllerName();
       
       string text = name 
           + " at <" + (string) ((integer) pos.x)
           + ", " + (string) ((integer) pos.y)
           + ", " + (string) ((integer) pos.z)
           + "> (" + (string) ((integer) llVecDist(pos, llGetRootPosition())) + "m)";
       if (controller != "")
       {
           text = text + " operated by " + controller;
       }        
       text = text + " would like to 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");
       return FALSE;
   }
   return TRUE;

}

// gets the name of the last controller or "" // if there is either no !who-command, it is for NULL_KEY, // or the avatar is not in the sim string getControllerName() {

   string controller = "";
   integer i;
   integer count = llGetListLength(currentMessageCommandList);
   for (i = 0; i < count; i++)
   {
       string command = llList2String(currentMessageCommandList, i);
       list commandTokens = llParseString2List(command, ["/"], []);
       if (llList2String(commandTokens, 0) == "!who")
       {
           controller = llKey2Name(llList2String(commandTokens, 1));
       }
   }
   return controller;

}

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

// execute a non-parsed message // this command could be denied here for policy reasons execute(string name, key id) {

   currentMessage = "";
   currentMessageCommands = "";
   string cmd_id = llList2String(currentMessageToken, 0); // CheckAttach
   currentMessageToken = [];
   integer len = llGetListLength (currentMessageCommandList);
   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(currentMessageCommandList, i);
       prefix = llGetSubString(command, 0, 0);
       if (prefix == PREFIX_RL_COMMAND) // this is a RLV command
       {
           executeRLVCommand(cmd_id, id, command);
       }
       else if (prefix == PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
       {
           integer continue = executeMetaCommand(cmd_id, id, command);
           if (!continue)
           {
               return;
           }
       }
   }

}

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

   command = llToLower(command);
   // we need to know whether whether is a rule or a simple command
   list tokens = llParseString2List(command, ["="], []);
   string behav = llList2String(tokens, 0); // @getattach:skull
   string param = llList2String(tokens, 1); // 2222
   integer ind = llListFindList(restrictions, [behav]);
   tokens = llParseString2List(behav, [":"], []); // @sit:<uuid>
   string behavName = llList2String (tokens, 0);  // @sit
   string option = llList2String (tokens, 1);     // <uuid>
   
   debug("executeRLVCommand: behav=!" + behav + "! param=!" + param + "!");
   if (isQuery(behavName) && ((integer) param <= 0))
   {
       // if this is a query command, don't accept public chat, 
       // negative numbers, or pseudo channels like
       // "n", "add", "y", "rem", "force",  ...
       ack(cmd_id, id, command, "ko");
       return;
   }
   if (param=="n" || param=="add") // add to restrictions
   {
       if (ind < 0)
       {
           if ((llGetListLength(restrictions) == 0) && !visionRestricted)
           {
               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
   {
       if (behavName == "@this-is-a-script-generated-message-beyond-the-control-of-the-agent")
       {
           ack(cmd_id, id, command, "ko");
           return;
       }
       
       if (ind > -1) 
       {
           restrictions = llDeleteSubList (restrictions, ind, ind);
       }
       if ((llGetListLength(restrictions) == 0) && !visionRestricted)
       {
           releaseRestrictions();
       }
       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;
       }
   }
   else if (((integer) param <= 0) && (behavName != "@clear"))
   {
       // this is either an unknown param (not "n", "add", "y", "rem", "force")
       // or a query which should be answered on the public chat channel 0.
       // note negative channels are displayed as /-x on public chat.
       ack(cmd_id, id, command, "ko");
       return;
   }
   workaroundForAtClear(command);
   rememberForceSit(behavName, option, param);
   sendRLCmd(command); // execute command
   ack(cmd_id, id, command, "ok"); // acknowledge

}

// is this a query? integer isQuery(string behav) {

   return ((llGetSubString(behav, 0, 3) == "@get") ||
           (llGetSubString(behav, 0, 7) == "@version") ||
           (llGetSubString(behav, 0, 10) == "@findfolder"));

}

// 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 behavName, string option, string param) {

   // clear lastForceSitDestination in case we are now prevented from standing up and
   // the force sit was long ago. Note: restrictions is checked to prevent the
   // clearance in case @unsit is just send again on login
   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")
   {
       return;
   }
   debug("'force'-command:" + behavName + "/" + option);
   if (behavName != "@sit")
   {
       return;
   }
   lastForceSitDestination = (key) option;
   lastForceSitTime = llGetUnixTime();
   debug("remembered force sit");

}

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

   list tokens = llParseString2List(commandString, ["/"], []);
   string command = llList2String(tokens, 0);
   
   if (command == PREFIX_METACOMMAND + "version") // checking relay protocol version
   {
       ack(cmd_id, id, command, (string) RLVRS_PROTOCOL_VERSION);
   }
   else if (command == PREFIX_METACOMMAND + "implversion") // checking relay version
   {
       ack(cmd_id, id, command, RLVRS_IMPL_VERSION);
   }
   else if (command == PREFIX_METACOMMAND + "release") // release all the restrictions (end session)
   {
       releaseRestrictions();
       ack(cmd_id, id, command, "ok");
   }
   else if (command == PREFIX_METACOMMAND + "handover")
   {
       handleHandOver(id, tokens);
       return FALSE;
   }
   else if (command == PREFIX_METACOMMAND + "pong") // object is still available for us 
   {
       loginWaitingForPong = FALSE;
   }
   else if (llGetSubString(command, 0, 11) == PREFIX_METACOMMAND + "visionclear")
   {
       visionRestricted = FALSE;
       ack(cmd_id, id, command, "ok");
       if (llGetListLength(restrictions) == 0)
       {
           releaseRestrictions();
      } 
      llMessageLinked(LINK_SET, GPU_CHANNEL, commandString, source);
   }
   else if (llGetSubString(command, 0, 6) == PREFIX_METACOMMAND + "vision")
   {
       integer attached = llGetAttached(); 
       if (attached >= ATTACH_HUD_CENTER_2 || attached < ATTACH_HUD_BOTTOM_RIGHT)
       {
           if (llGetListLength(restrictions) == 0)
           {
               llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
               sendRLCmd(RVL_COMMAND_START);
           } 
           llMessageLinked(LINK_SET, GPU_CHANNEL, commandString, source);
           ack(cmd_id, id, command, "ok");
           visionRestricted = TRUE;
           source = id;
       }
       else
       {
           ack(cmd_id, id, command, "ko");
       }
   }
   else
   {
       ack(cmd_id, id, command, "ko");
   }
   return TRUE;

}


handleHandOver(string id, list tokens) {

   debug("handover");
   key targetObject = (key) llList2String(tokens, 1);
   integer keepRestrictions = (integer) llList2String(tokens, 2);
   if (!keepRestrictions)
   {
       releaseRestrictions();
   }
   source = targetObject;
   if (llGetListLength(restrictions) == 0)
   {
       sendRLCmd(RVL_COMMAND_START);
       llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
   } 
   pingWorldObjectIfUnderRestrictions();

}

// 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) releaseRestrictions() {

   integer i;
   integer len = llGetListLength (restrictions);
   for (i = 0; i < len; ++i)
   {
       sendRLCmd(llList2String (restrictions, i) + "=y");
   }
   sendRLCmd(RVL_COMMAND_END);
   lastForceSitDestination = NULL_KEY;
   restrictions = [];
   loginPendingForceSit = FALSE;
   clearPendingMessages();
   visionRestricted = FALSE;
   llMessageLinked(LINK_SET, GPU_CHANNEL, "!visionclear", source);
   llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "release", source);
   source = NULL_KEY;

}

// sends an !release,ok to the world object if we are in an active session tellWorldObjectAboutCanceledSession() {

   if (source != NULL_KEY)
   {
       ack("release", source, "!release", "ok");
   }

}


// 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) {

   if (mode== MODE_OFF)
   {
       debug("deactivated - ignoring commands");
       return; // mode is 0 (off) => reject
   }
   debug("LISTEN: " + currentMessage);
   splitMessage();
   if (!verifyWeAreTarget())
   {
      return;
   }
   debug("Got message (active world object " + (string) source + "): name=" + name+ " id=" + (string) id);

   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);
       return;
   }
   if (!isObjectKnow(id))
   {
       debug("asking for permission because source is NULL_KEY");
       if (!verifyPermission(id, name))
       {
           return;
       }
   }
   debug("Executing: " + (string) source);
   execute(name, id);

}

// --------------------------------------------------- // 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
       {
           debug("recovering pendingMessage");
           currentMessage = pendingMessage;
           debug("splitting");
           splitMessage();
           debug("executing");
           execute(pendingName, pendingId);
       }
       clearPendingMessages();
   }

}


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

init() {

   sendRLCmd("@clear");
   llOwnerSay("Waiting for plugins to initialize...");
   llSleep(5);
   mode = MODE_ASK;
   llListen(RLVRS_CHANNEL, "", "", "");
   llListen(DIALOG_CHANNEL, "", llGetOwner(), "");
   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", (key) ((string) RLVRS_PROTOCOL_VERSION));
   llOwnerSay("Initialized and ready");
   debug("Free Memory: " + (string) llGetFreeMemory());

}


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

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

}

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

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

}

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.");
       tellWorldObjectAboutCanceledSession();
       releaseRestrictions();
       mode = MODE_OFF;
       showModeDescription();
       return;
   }
   if (message == "unlock")
   {
       if (id)
       {
           llSay(0, llKey2Name(id) + " freed " + llKey2Name(llGetOwner()) + " by unlocking the relay.");
           tellWorldObjectAboutCanceledSession();
       }
       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 {

   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 (mode)
       {
           reinforceKnownRestrictions();
           pingWorldObjectIfUnderRestrictions();
       }
       // remind the current mode to the user
       showModeDescription();
   }
   
   attach(key id)
   {
       if (id == NULL_KEY)
       {
           tellWorldObjectAboutCanceledSession();
           releaseRestrictions();
       }
   }
   timer()
   {
       processTimer();
   }
   listen(integer channel, string name, key id, string message)
   {
       currentMessageToken = [];
       currentMessageCommands = "";
       currentMessageCommandList = [];
       currentMessage = message;
       message = ""; // free memory
       if (channel==RLVRS_CHANNEL)
       {
           processRelayMessage(name, id);
       }
       else if (channel==DIALOG_CHANNEL)
       {
           processDialogResponse(id, message);
       }
   }
   
   link_message(integer sender, integer channel, string message, key id)
   {
       currentMessageToken = [];
       currentMessageCommands = "";
       currentMessageCommandList = [];
       if (channel == CMD_LINK_CHANNEL)
       {
           processLinkMessage(id, message);
       }
       else if (channel == DIALOG_LINK_CHANNEL)
       {
           processDialogResponse(id, message);
       }
   }
   touch_start(integer num_detected)
   {
       if (!isNotEmbedded)
       {
           return;
       }
       // touched by user => cycle through OFF/ON_PERMISSION/ON_ALWAYS modes
       key toucher = llDetectedKey(0);
       if (toucher != llGetOwner())
       {
           return;
       }
       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)
   {
       if (change & (CHANGED_OWNER | CHANGED_INVENTORY | CHANGED_ALLOWED_DROP)) 
       {
            llResetScript();
       }
       if (change & (CHANGED_REGION | CHANGED_TELEPORT)) 
       {
           if (loginWaitingForPong)
           {
               if (source)
               {
                   ack("ping", source, "ping", "ping");
               }
           }
       }
   }

} </lsl>