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

From Second Life Wiki
Jump to navigation Jump to search
m (<lsl> tag to <source>)
 
(85 intermediate revisions by 7 users not shown)
Line 1: Line 1:
=Restrained Life viewer v1.10 Relay Protocol Specification=
{{Restrained Life Relay Specs TOC}}


By Marine Kelley
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 [[LSL Protocol/Restrained Life Relay/Change History|Change History]].


'''UNDER CONSTRUCTION'''


==Audience==
==What it does==


This document is meant for people who want to create or modify '''in-world''' objects to use the features of someone else's [http://realrestraint.blogspot.com RestrainedLife viewer], typically '''cages''' and '''pieces of furniture''', which per definition are usually not owned by that person.  
* Implements the specification described hereabove.
* Commented to facilitate reading and learning.
* Tested and working with test objects and with real cages.




==Introduction==
==What it doesn't do==


The [http://realrestraint.blogspot.com RestrainedLife viewer] only executes commands issued through [[llOwnerSay]] () messages. Therefore, in order to issue commands to someone using the viewer who does not own the object, that person must wear an attachment that '''relays''' commands after some '''security checks'''.
* 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.




==Why this spec ?==
==How to use it==


Now that [http://realrestraint.blogspot.com RestrainedLife viewer] v1.10 is out, many cages and furniture creators are interested in using its features such as sit, outfit, tp etc. These objects can be found in public places, or owned by friends... but as they are usually not owned by the user, the relay has to implement some basic security in order to avoid [[griefing]]. On top of it, the user does not want to be forced to switch to another relay when going to the next piece of furniture.
* 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.


This is the purpose of this specification : to lay common rules so all the relays implementing it are compatible with all the furnitures implementing it too. Without such a specification, one cage/furniture would talk to the relay specifically made to operate with it and that's all, eventually making the creator stay behind because people rather use standard objects than proprietary closed ones.


==Special thanks==


==Basic principle==
* 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.


Here is a sample use case :
==Reference Implementation==


# User is wearing a Relay
<div style="border: 2px solid green; padding: 1em; color: #000; background-color: #AFA">'''Please add fixes, new features and stuff like that to [[LSL Protocol/Restrained Life Relay/Development & Contribution|Development & Contribution]].'''</div>
# User enters a cage in a public area
# Cage sends a chat message on a known private channel (for instance "@tploc=n")
# Relay receives the message, decides to repeat the command to the user and blocks their ability to teleport from the map with an [[llOwnerSay]] ("@tploc=n");
# User is allowed to get out after some time, the cage issues a "@tploc=y" command, immediately repeated by the relay


Without the relay, the cage could never prevent the user from teleporting since it doesn't belong to them.
<div style="border: 2px solid red; padding: 1em; color: #000; background-color: #FAA">'''This version has a number of known bugs and a vulnerability. Please see [[LSL Protocol/Restrained Life Relay/Bugs and Pendings Features|Bugs and Pendings Features]]'''</div>


<source lang="lsl2">


==Requirements==
 
Here are the informal requirements for a relay (formal requirements below).
 
===Security===
Any object sending commands over the channel the relay listens to is likely to harm the user if there is no security implemented in the former. For instance, one could [[rez]] an object that sends a "@remoutfit=force" command over the chat channel to force an avatar to get naked in front of everyone without a warning. Of course nobody wants that, so basic security is needed.
 
===User-friendliness===
Security often implies control (access lists, switch, permissions...) so the user must be given some basic control over what kind of objects are allowed to issue commands.
 
===Versatility===
Some users will prefer wearing a dedicated attachment that they can unwear any time they want, others will be required to have the relay locked on them so they cannot detach them, others will want the script only... It is important to keep those differences in mind when deciding about the permissions of the relay. However, it is the user's responsibility to choose the relay that suits their needs best.
 
===Licensing===
According to the level of complexity and support of the relay, the creator is allowed to either provide it for free (open/close source) or to sell it, as long as it implements all the formal requirements.
 
 
 
==Common questions==
 
===How hard is it to implement such a specification ?===
That depends on what you do. Furniture/cage makers will find it very easy for it only comes down to send command over a chat channel and getting feedback. Relay makers will find it harder but then again, that depends on the level of security and user-friendliness they wish to offer. But make no mistake, the relay is what does almost all the job (along with the viewer), because there will be many more furnitures than relays around.
 
===Why do other people need to write such a relay ? Couldn't you write it yourself and publish it ?===
Of course I could, and there is even a working code for a basic relay at the end of this page. However :
* The protocol is likely to improve because nobody sees the future
* One object only would not suit everyone's needs
* It would have to implement perfect security and perfect user-friendliness, in all cases
* It would of course have to be perfectly scripted, without any bug whatsoever
 
The security and user-friendliness are the key parts here. Some users will prefer to be safe from [[griefing]], others will prefer a good user interface, others will like a lot of features, others will want to move the script elsewhere... Everyone has their own tastes so there can be no one-size-fits-all relay.
 
==Protocol==
 
In-world objects send chat message over a channel (common to every relay and furniture), that the relay(s) acknowledges or not. If the message is a correct command and passes whatever security checks the relay implements, the latter repeats it as an [[llOwnerSay]] ().
 
When the session ends, possibly after several relogs, the object clears all the restrictions it has put the user under.
 
==Formal requirements==
 
===Common requirements===
* The chat channel is '''-1812221819'''. That's "RLVRS" translated from alphabetical to numbers.
* Messages on this channel are sent with [[llSay]] (), no [[llShout]] () nor [[llWhisper]] () allowed
* Messages coming from the objects are of the form :
** <''cmd_name''>''','''<''user_uuid''>''',@'''<''behav''[:''option'']=''param''>
** Where :
*** "''cmd_name''" is totally free and will be repeated in the acknowledgment if any
*** "''user_uuid''" is the UUID of the user
*** the rest is the exact command to be relayed to the user
* behav, option and param parts are '''lowercase'''
* Messages coming from the relay are of the form :
** <''cmd_name''>''','''<''object_uuid''>''',ok'''|'''ko'''|<''version''>
** Where :
*** "''cmd_name''" is the exact name of the acknowledged command so the object knows which command this message replies to
*** "''object_uuid''" is the UUID of the in-world object
*** "''version''" is the 4-characters version string when needed
 
===Relay requirements===
 
 
===In-world object requirements===
 
==Sample code of a basic working relay==
 
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.
 
<lsl>
 
//~ RestrainedLife Viewer Relay Script example code
//~ RestrainedLife Viewer Relay Script example code
//~ By Marine Kelley
//~ By Marine Kelley
//~ 2008-01-29
//~ 2008-02-03
//~ v1.00
//~ 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 not be sold nor distributed without
//~ completeness or performance. It may only be distributed in its full source code,
//~ this header and disclaimer.
//~ this header and disclaimer and is not to be sold.
 
//~ Requirements for both RLV Relay Script (RLVRS) and in-world objects
//~ * Possible improvements
//~ preset channel with llSay only, no llWhisper no llShout
//~ Do some error checking
//~ preset protocol : cmd_name,user_uuid,@behav=param (behav and param parts are *lowercase*)
//~ Handle more than one object
//~ preset replies (*lowercase*) : ok|ko|<version> (version only when receiving a "version" meta-command)
//~ Periodically check that the in-world objects are still around, when one is missing purge its restrictions
 
//~ Manage an access list
//~ Special requirements for the Relay Script
//~ Reject some commands if not on access list (force remove clothes, force remove attachments...)
//~ send the exact @behav=param part in an llOwnerSay, without any change whatsoever
//~ and much more...
//~ retain list of restrictions and their sources for relog, force sit if unsit is prevented
//~ refresh (purge restrictions of which the source is not around anymore)
//~ name of script contains RLVnnn where nnn is the minimal version it is compatible with (ex : RLV110), the user must have access to that version (dialog, message, object name...) to check everything works correctly
// ---------------------------------------------------
//~ implement some security (group, distance, length of message, deny "force" commands...)
//                     Constants
//~ implement some user-friendliness (authorizations, menus, level of control (accept "force" commands y/n ?)... )
// ---------------------------------------------------
 
//~ Special requirements for the in-world objects
integer RLVRS_PROTOCOL_VERSION = 1020; // version of the protocol, stated on the specification page
//~ never rely on an answer from the RLVRS, requests can be denied silently, the relay can be unworn, the avatar can tp out or crash... => use timeouts
//~ don't poll the dataserver for online status, the RLVRS takes care of the relog part
string PREFIX_RL_COMMAND = "@";
//~ avoid unnecessary messages, do not spam the user if they are not using RLV
string PREFIX_METACOMMAND = "!";
//~ do not rely on RLV to function, as it is only an enhancement not a requirement
//~ try to leave the user's RLVRS clean of restrictions when the game is over (don't make them use Refresh, lift restrictions properly)
integer RLVRS_CHANNEL = -1812221819; // RLVRS in numbers
 
integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers
string RLVRS_PROTOCOL_VERSION = "1000";
 
integer MAX_OBJECT_DISTANCE = 100;    // 100m is llShout distance
integer RLVRS_CHANNEL = -1812221819; //RLVRS in numbers
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 300; // 300 is 5 minutes
integer DIALOG_CHANNEL = -1812220409; //RLVDI in numbers
 
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)
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
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)
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)
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)
string sPendingMessage; // message of pending request (first request of a session in mode 1)
integer nMode; // 0:off, 1:accept on authorization, 2:accept all
integer sPendingTime;
 
 
// used on login
Ack (string cmd_id, key id, string ack) { // acknowledge or reject
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)
  llSay (RLVRS_CHANNEL, cmd_id+","+(string)id+","+ack);
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;
}
}
 
SendRLCmd (string cmd) { // cmd begins with a '@'
// If we already have commands from this object pending
  llOwnerSay (cmd);
// 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;
}
}
 
integer IsSimpleRequest (string cmd) {
// verifies the permission. This includes mode
  // cmd ends with "=" and a number (@version, @getoutfit, @getattach)
// (off, permission, auto) of the relay and the
  integer ind=llSubStringIndex (cmd, "=");
// identity of the object (owned by parcel people).
  if (ind>-1) {
integer verifyPermission(key id, string name, string message)
     string param=llGetSubString (cmd, ind+1, -1);
{
     if ((integer)param!=0 || param=="0") return 1;
    // is it switched off?
  }
    if (nMode == MODE_OFF)
  return 0;
    {
        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;
}
}
 
Execute (string name, key id, string message) {
  // execute a non-parsed command, we are sure the message has been accepted regarding its source (authorization, on/off)
// ---------------------------------------------------
  // this command can still be denied, but this time there will be an acknowledgement
//              Executing of commands
  list tokens=llParseString2List (message, [",", "="], []);
// ---------------------------------------------------
  if (llGetListLength (tokens)==4) { // this is a normal command
     string cmd_id=llList2String (tokens, 0); // CheckAttach
// execute a non-parsed message
    key target=llList2Key (tokens, 1); // UUID
// this command could be denied here for policy reasons, (if it were implemenetd)
    if (target==llGetOwner ()) { // talking to me ?
// but this time there will be an acknowledgement
      string behav=llList2String (tokens, 2); // @getattach:skull
execute(string name, key id, string message)
      string param=llList2String (tokens, 3); // 2222
{
      key my_parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);
    list tokens=llParseString2List (message, [","], []);
      key its_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
    if (llGetListLength (tokens)==3) // this is a normal command
      key owner_key=llGetOwnerKey (id);
     {
      // do the actual check
        string cmd_id=llList2String (tokens, 0); // CheckAttach
      if (owner_key==llGetOwner ()                         // IF I am the owner of the object
        key target=llList2Key (tokens, 1); // UUID
      || its_group==my_parcel_group                        // OR its group is the same as the parcel I'm on
        if (target==llGetOwner ()) // talking to me ?
      ) {
        {
        // command accepted, check the param, add to list if needed, pass to viewer, acknowledge
            list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);
        integer ind=llListFindList (lRestrictions, [behav]);
            integer len=llGetListLength (list_of_commands);
        if (param=="n" || param=="add") { // add to lRestrictions
            integer i;
          if (ind<0) lRestrictions+=[behav];
            string command;
          kSource=id;
            string prefix;
        } else if (param=="y" || param=="rem") { // remove from lRestrictions
            for (i=0; i<len; ++i) // execute every command one by one
          if (ind>-1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind);
            {
          if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY;
                // 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);
                }
            }
         }
         }
        SendRLCmd (behav+"="+param); // execute command
        Ack (cmd_id, id, "ok"); // acknowledge
      } else {
        // command denied, reply "request denied"
        Ack (cmd_id, id, "ko");
      }
     }
     }
  } else if (llGetListLength (tokens)==3) { // meta-command, not relayed to the viewer (release, get protocol version)
}
     string cmd_id=llList2String (tokens, 0); // CheckAttach
     key target=llList2Key (tokens, 1); // UUID
// executes a command for the restrained life viewer
     string behav=llList2String (tokens, 2);
// with some additinal magic like book keeping
     if (target==llGetOwner ()) { // talking to me ?
executeRLVCommand(string cmd_id, string id, string command)
      if (behav=="version") { // checking relay version
{
         Ack (cmd_id, id, RLVRS_PROTOCOL_VERSION);
    // we need to know whether whether is a rule or a simple command
      } else if (behav=="release") { // release all the restrictions (end session)
    list tokens_command=llParseString2List (command, ["="], []);
         ReleaseRestrictions ();
    string behav=llList2String (tokens_command, 0); // @getattach:skull
         Ack (cmd_id, id, "ok");
    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;
}
}
 
ReleaseRestrictions () {
  kSource=NULL_KEY;
// ---------------------------------------------------
  integer i;
//            initialisation and login handling
  integer len=llGetListLength (lRestrictions);
// ---------------------------------------------------
  for (i=0; i<len; ++i) {
     SendRLCmd (llList2String (lRestrictions, i)+"=y");
init() {
  }
    nMode=1;
    kSource=NULL_KEY;
    lRestrictions=[];
    sPendingId=NULL_KEY;
    sPendingName="";
    sPendingMessage="";
    llListen (RLVRS_CHANNEL, "", "", "");
     llListen (DIALOG_CHANNEL, "", llGetOwner(), "");
    llOwnerSay (getModeDescription());
}
}
 
string GetMode () {
// sends the known restrictions (again) to the RL-viewer
  if (nMode==0) return "RLV Relay is OFF";  
// (call this functions on login)
  else if (nMode==1) return "RLV Relay is ON (permission needed)";  
reinforceKnownRestrictions()
  else return "RLV Relay is ON (auto-accept)";  
{
    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;
        }
    }
}
}
 
Init () {
// send a ping request and start a timer
  nMode=1;
pingWorldObjectIfUnderRestrictions()
  kSource=NULL_KEY;
{
  lRestrictions=[];
    loginWaitingForPong = FALSE;
  sPendingId=NULL_KEY;
    if (kSource != NULL_KEY)
  sPendingName="";
    {
  sPendingMessage="";
        ack("ping", kSource, "ping", "ping");
  llListen (RLVRS_CHANNEL, "", "", "");
        timerTickCounter = 0;
  llListen (DIALOG_CHANNEL, "", llGetOwner (), "");
        llSetTimerEvent(1.0);
  llOwnerSay (GetMode ());
        loginWaitingForPong = TRUE;
    }
}
}
 
 
 
 
default
default
{
{
  state_entry () {
    state_entry()
    Init ();
    {
  }
        init();
 
    }
  on_rez(integer start_param) {
    if (nMode) {
    on_rez(integer start_param)
      integer i;
    {
      integer len=llGetListLength (lRestrictions);
        // relogging, we must refresh the viewer and ping the object if any
      string restr;
        // if mode is not OFF, fire all the stored restrictions
      for (i=0; i<len; ++i) {
        if (nMode)
        restr=llList2String (lRestrictions, i);
        {
        SendRLCmd (restr+"=n");
            reinforceKnownRestrictions();
        if (restr=="@unsit") {
            pingWorldObjectIfUnderRestrictions();
          SendRLCmd ("@sit:"+(string)kSource+"=force");
        }
        // 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)lastForceSitDestination+"=force");
            }
        }
        if (!loginPendingForceSit && !loginWaitingForPong)
        {
            llSetTimerEvent(0.0);
         }
         }
      }
     }
     }
     llOwnerSay (GetMode ());
  }
     listen(integer channel, string name, key id, string message)
 
    {
  listen(integer channel, string name, key id, string message) { // CheckAttach,UUID,@getattach:skull=2222
        if (channel==RLVRS_CHANNEL)
    if (channel==RLVRS_CHANNEL) {
        {
      // do a basic check without parsing the command, reject without any acknowledgement when needed
            if (!verifyWeAreTarget(message))
      if (nMode==0) return; // mode is 0 (off) => reject
            {
      if (kSource!=NULL_KEY && kSource!=id) return; // already used by another object => reject
              return;
      if (IsSimpleRequest (message)) { // simple harmless command such as @version, @getoutfit or @getattach
            }
        Execute (name, id, message);
         return;
            if (nMode== MODE_OFF)
      }
            {
      if (nMode==1) {
                debug("deactivated - ignoring commands");
         if (kSource==NULL_KEY) {
                return; // mode is 0 (off) => reject
          if (sPendingId==NULL_KEY) { // not under operation yet, prompt the user, delay reply until they accept or reject
            }
             sPendingId=id;
            if (!isObjectNear(id)) return;
             sPendingName=name;
             sPendingMessage=message;
            debug("Got message (active world object " + (string) kSource + "): name=" + name+ "id=" + (string) id + " message=" + message);
            llDialog (llGetOwner (), name+" would like to connect to your RLV Relay and send commands to your viewer.\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
          }
            if (kSource != NULL_KEY && kSource != id)
        } else if (kSource==id) { // already operated by this object, accept automatically
            {
          Execute (name, id, message);
                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="";
            }
         }
         }
      } else if (nMode==2 && (kSource==NULL_KEY || kSource==id)) { // accept automatically
    }
         Execute (name, id, message);
      }
    touch_start(integer num_detected)
    } else if (channel==DIALOG_CHANNEL) {
    {
      if (sPendingId!=NULL_KEY) {
        // touched by user => cycle through OFF/ON_PERMISSION/ON_ALWAYS modes
        if (message=="Yes") { // pending request authorized => process it
         key toucher=llDetectedKey(0);
          Execute (sPendingName, sPendingId, sPendingMessage);
        if (toucher==llGetOwner())
        } else if (sPendingId!=NULL_KEY && message=="No") { // denied => do nothing at all
        {
         
            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());
         }
         }
        // clear pending request
        sPendingName="";
        sPendingId=NULL_KEY;
        sPendingMessage="";
      }
     }
     }
  }
 
    changed(integer change)
  touch_start(integer num_detected) {
     {
     key toucher=llDetectedKey (0);
        if (change & CHANGED_OWNER)  
    if (toucher==llGetOwner ()) {
        {
      ++nMode;
            llResetScript();
      if (nMode>2) nMode=0;
        }
      if (nMode==0) ReleaseRestrictions ();
      llOwnerSay (GetMode ());
     }
     }
  }
  changed(integer change) {
    if (change & CHANGED_OWNER) llResetScript ();
  }
}
}


</lsl>
</source>

Latest revision as of 13:21, 25 January 2015

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.


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.
This version has a number of known bugs and a vulnerability. Please see Bugs and Pendings Features
 
//~ 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 = 1020; // 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)lastForceSitDestination+"=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();
        }
    }
}