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

From Second Life Wiki
Jump to navigation Jump to search
m (→‎Reference Implementation: changed "if (ind>-1)" to "if (ind > -1)" to work around a parser issue in lslplus)
m (<lsl> tag to <source>)
 
(19 intermediate revisions by 5 users not shown)
Line 1: Line 1:
{{Restrained Life Relay Specs TOC}}
{{Restrained Life Relay Specs TOC}}


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.
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]].




Line 32: Line 32:


* 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.
* 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==
==Reference Implementation==
Line 38: Line 39:
<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>
<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>


<lsl>
<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">
//~ RestrainedLife Viewer Relay Script example code
//~ RestrainedLife Viewer Relay Script example code
//~ By Marine Kelley
//~ By Marine Kelley
//~ 2008-02-03
//~ 2008-02-03
//~ 2008-02-03
//~ v1.1
//~ v1.1
Line 49: Line 54:
//~ 2008-03-05 silently ignore commands for removing restrictions if they are not active anyway  
//~ 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-06-24 fix of loophole in ask-mode by Felis Darwin
 
//~ 2008-09-01 changed llSay to llShout, increased distance check (MK)
//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy,
//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy,
//~ completeness or performance. It may only be distributed in its full source code,
//~ completeness or performance. It may only be distributed in its full source code,
Line 61: Line 67:
//~ Reject some commands if not on access list (force remove clothes, force remove attachments...)
//~ Reject some commands if not on access list (force remove clothes, force remove attachments...)
//~ and much more...
//~ and much more...
 
 
// ---------------------------------------------------
// ---------------------------------------------------
//                    Constants
//                    Constants
// ---------------------------------------------------
// ---------------------------------------------------
   
   
integer RLVRS_PROTOCOL_VERSION = 1014; // version of the protocol, stated on the specification page
integer RLVRS_PROTOCOL_VERSION = 1020; // version of the protocol, stated on the specification page
   
   
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 MAX_OBJECT_DISTANCE = 20;    // 20m is llSay distance
integer MAX_OBJECT_DISTANCE = 100;    // 100m is llShout distance
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 300; // 300 is 5 minutes
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 300; // 300 is 5 minutes
 
integer PERMISSION_DIALOG_TIMEOUT = 30;
integer PERMISSION_DIALOG_TIMEOUT = 30;
 
integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
 
integer MODE_OFF = 0;
integer MODE_OFF = 0;
integer MODE_ASK = 1;
integer MODE_ASK = 1;
integer MODE_AUTO = 2;
integer MODE_AUTO = 2;
 
 
// ---------------------------------------------------
// ---------------------------------------------------
//                      Variables
//                      Variables
Line 93: Line 99:
   
   
integer nMode;
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 sPendingTime;
integer sPendingTime;
 
// used on login
// used on login
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)
integer loginWaitingForPong;
integer loginWaitingForPong;
integer loginPendingForceSit;
integer loginPendingForceSit;
 
key    lastForceSitDestination;
key    lastForceSitDestination;
integer lastForceSitTime;
integer lastForceSitTime;
 
// ---------------------------------------------------
// ---------------------------------------------------
//              Low Level Communication
//              Low Level Communication
// ---------------------------------------------------
// ---------------------------------------------------
   
   
 
debug(string x)
debug(string x)
{
{
Line 123: Line 129:
ack(string cmd_id, key id, string cmd, string ack)
ack(string cmd_id, key id, string cmd, string ack)
{
{
     llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
     llShout(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
}
}
 
// cmd begins with a '@'  
// cmd begins with a '@'  
sendRLCmd(string cmd)
sendRLCmd(string cmd)
Line 131: Line 137:
     llOwnerSay(cmd);
     llOwnerSay(cmd);
}
}
 
// get current mode as string
// get current mode as string
string getModeDescription()
string getModeDescription()
Line 139: Line 145:
     else return "RLV Relay is ON (auto-accept)";  
     else return "RLV Relay is ON (auto-accept)";  
}
}
 
// check that this command is for us and not someone else
// check that this command is for us and not someone else
integer verifyWeAreTarget(string message)
integer verifyWeAreTarget(string message)
Line 153: Line 159:
     return FALSE;
     return FALSE;
}
}
 
// ---------------------------------------------------
// ---------------------------------------------------
//              Permission Handling
//              Permission Handling
// ---------------------------------------------------
// ---------------------------------------------------
 
// are we already under command by this object?
// are we already under command by this object?
integer isObjectKnow(key id)
integer isObjectKnow(key id)
Line 166: Line 172:
         return FALSE;
         return FALSE;
     }
     }
 
     // are we already under command by this object?
     // are we already under command by this object?
     if (kSource == id)
     if (kSource == id)
Line 172: Line 178:
         return TRUE;
         return TRUE;
     }
     }
 
     // are we not under command by any object but were we forced to sit on this object recently?
     // are we not under command by any object but were we forced to sit on this object recently?
     if ((kSource == NULL_KEY) && (id == lastForceSitDestination))
     if ((kSource == NULL_KEY) && (id == lastForceSitDestination))
Line 183: Line 189:
         }
         }
     }
     }
 
     return FALSE;
     return FALSE;
}
}
 
 
// check whether the object is in llSay distance.
// check whether the object is in llShout distance. It could have moved
// The specification requires llSay instead of llShout or llRegionSay
// before the message is received (chatlag)
// to be used to limit the range. But this has to be checked here again
// because the objects are not trustworthy.
integer isObjectNear(key id)
integer isObjectNear(key id)
{
{
Line 200: Line 204:
     return distance <= MAX_OBJECT_DISTANCE;
     return distance <= MAX_OBJECT_DISTANCE;
}
}
 
// do a basic check on the identity of the object trying to issue a command
// do a basic check on the identity of the object trying to issue a command
integer isObjectIdentityTrustworthy(key id)
integer isObjectIdentityTrustworthy(key id)
Line 208: Line 212:
     key object_owner=llGetOwnerKey(id);
     key object_owner=llGetOwnerKey(id);
     key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
     key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
 
     debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
     debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
     debug("group= " + (string) parcel_group + " / " + (string) object_group);
     debug("group= " + (string) parcel_group + " / " + (string) object_group);
 
     if (object_owner==llGetOwner ()        // IF I am the owner of the object
     if (object_owner==llGetOwner ()        // IF I am the owner of the object
       || object_owner==parcel_owner        // OR its owner is the same as the parcel I'm on
       || object_owner==parcel_owner        // OR its owner is the same as the parcel I'm on
Line 221: Line 225:
     return FALSE;
     return FALSE;
}
}
 
 
// Is this a simple request for information or a meta command like !release?
// Is this a simple request for information or a meta command like !release?
integer isSimpleRequest(list list_of_commands)  
integer isSimpleRequest(list list_of_commands)  
Line 228: Line 232:
     integer len = llGetListLength(list_of_commands);
     integer len = llGetListLength(list_of_commands);
     integer i;
     integer i;
 
     // now check every single atomic command
     // now check every single atomic command
     for (i=0; i < len; ++i)
     for (i=0; i < len; ++i)
Line 238: Line 242:
         }
         }
     }
     }
 
     // all atomic commands passed the test
     // all atomic commands passed the test
     return TRUE;
     return TRUE;
}
}
 
// is this a simple atmar command
// is this a simple atmar command
// (a command which only queries some information or releases restrictions)
// (a command which only queries some information or releases restrictions)
Line 258: Line 262:
             return TRUE;
             return TRUE;
         }
         }
 
         // removing restriction
         // removing restriction
         if ((param == "y") || (param == "rem"))
         if ((param == "y") || (param == "rem"))
Line 265: Line 269:
         }
         }
     }
     }
 
     // check for a leading ! (meta command)
     // check for a leading ! (meta command)
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
Line 271: Line 275:
         return TRUE;
         return TRUE;
     }
     }
 
     // check for @clear
     // check for @clear
     // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
     // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
Line 281: Line 285:
         return TRUE;
         return TRUE;
     }
     }
 
     // this one is not "simple".
     // this one is not "simple".
     return FALSE;
     return FALSE;
}
}
 
// If we already have commands from this object pending
// If we already have commands from this object pending
// because of a permission request dialog, just add the
// because of a permission request dialog, just add the
Line 301: Line 305:
     return FALSE;
     return FALSE;
}
}
 
// verifies the permission. This includes mode  
// verifies the permission. This includes mode  
// (off, permission, auto) of the relay and the
// (off, permission, auto) of the relay and the
Line 312: Line 316:
         return FALSE;
         return FALSE;
     }
     }
 
     // extract the commands-part
     // extract the commands-part
     list tokens = llParseString2List (message, [","], []);
     list tokens = llParseString2List (message, [","], []);
Line 321: Line 325:
     string commands = llList2String(tokens, 2);
     string commands = llList2String(tokens, 2);
     list list_of_commands = llParseString2List(commands, ["|"], []);
     list list_of_commands = llParseString2List(commands, ["|"], []);
 
     // accept harmless commands silently
     // accept harmless commands silently
     if (isSimpleRequest(list_of_commands))
     if (isSimpleRequest(list_of_commands))
Line 327: Line 331:
         return TRUE;
         return TRUE;
     }
     }
 
     // if we are already having a pending permission-dialog request for THIS object,
     // if we are already having a pending permission-dialog request for THIS object,
     // just add the new commands at the end of the pending command list.
     // just add the new commands at the end of the pending command list.
Line 334: Line 338:
         return FALSE;
         return FALSE;
     }
     }
 
     // check whether this object belongs here
     // check whether this object belongs here
     integer trustworthy = isObjectIdentityTrustworthy(id);
     integer trustworthy = isObjectIdentityTrustworthy(id);
Line 342: Line 346:
         warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";
         warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";
     }
     }
 
     // ask in permission-request-mode and/OR in case the object identity is suspisous.
     // ask in permission-request-mode and/OR in case the object identity is suspisous.
     if (nMode == MODE_ASK || !trustworthy)
     if (nMode == MODE_ASK || !trustworthy)
Line 356: Line 360:
     return TRUE;
     return TRUE;
}
}
 
 
// ---------------------------------------------------
// ---------------------------------------------------
//              Executing of commands
//              Executing of commands
// ---------------------------------------------------
// ---------------------------------------------------
 
// execute a non-parsed message
// execute a non-parsed message
// this command could be denied here for policy reasons, (if it were implemenetd)
// this command could be denied here for policy reasons, (if it were implemenetd)
Line 397: Line 401:
     }
     }
}
}
 
// 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
Line 407: Line 411:
     string param=llList2String (tokens_command, 1); // 2222
     string param=llList2String (tokens_command, 1); // 2222
     integer ind=llListFindList (lRestrictions, [behav]);
     integer ind=llListFindList (lRestrictions, [behav]);
 
     if (param=="n" || param=="add") // add to lRestrictions
     if (param=="n" || param=="add") // add to lRestrictions
     {
     {
Line 418: Line 422:
         if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY;
         if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY;
     }
     }
 
     workaroundForAtClear(command);
     workaroundForAtClear(command);
     rememberForceSit(command);
     rememberForceSit(command);
Line 424: Line 428:
     ack(cmd_id, id, command, "ok"); // acknowledge
     ack(cmd_id, id, command, "ok"); // acknowledge
}
}
 
// check for @clear
// check for @clear
// Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
// Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
Line 437: Line 441:
     }
     }
}
}
 
// remembers the time and object if this command is a force sit
// remembers the time and object if this command is a force sit
rememberForceSit(string command)
rememberForceSit(string command)
Line 448: Line 452:
         return;
         return;
     }
     }
 
     tokens_command=llParseString2List(behav, [":"], []);
     tokens_command=llParseString2List(behav, [":"], []);
     behav=llList2String (tokens_command, 0); // @sit
     behav=llList2String (tokens_command, 0); // @sit
Line 461: Line 465:
     debug("remembered force sit");
     debug("remembered force sit");
}
}
 
// executes a meta command which is handled by the relay itself
// executes a meta command which is handled by the relay itself
executeMetaCommand(string cmd_id, string id, string command)
executeMetaCommand(string cmd_id, string id, string command)
Line 489: Line 493:
     loginPendingForceSit = FALSE;
     loginPendingForceSit = FALSE;
}
}
 
   
   
// ---------------------------------------------------
// ---------------------------------------------------
Line 506: Line 510:
     llOwnerSay (getModeDescription());
     llOwnerSay (getModeDescription());
}
}
 
// sends the known restrictions (again) to the RL-viewer
// sends the known restrictions (again) to the RL-viewer
// (call this functions on login)
// (call this functions on login)
Line 526: Line 530:
     }
     }
}
}
 
// send a ping request and start a timer
// send a ping request and start a timer
pingWorldObjectIfUnderRestrictions()
pingWorldObjectIfUnderRestrictions()
Line 539: Line 543:
     }
     }
}
}
 
default
default
{
{
Line 559: Line 563:
         llOwnerSay(getModeDescription());
         llOwnerSay(getModeDescription());
     }
     }
 
 
     timer()
     timer()
     {
     {
Line 572: Line 576:
             releaseRestrictions();
             releaseRestrictions();
         }
         }
 
         if (loginPendingForceSit)
         if (loginPendingForceSit)
         {
         {
Line 589: Line 593:
             else
             else
             {
             {
                 sendRLCmd ("@sit:"+(string)kSource+"=force");
                 sendRLCmd ("@sit:"+(string)lastForceSitDestination+"=force");
             }
             }
         }
         }
 
         if (!loginPendingForceSit && !loginWaitingForPong)
         if (!loginPendingForceSit && !loginWaitingForPong)
         {
         {
Line 607: Line 611:
               return;
               return;
             }
             }
       
             if (nMode== MODE_OFF)
             if (nMode== MODE_OFF)
             {
             {
Line 614: Line 618:
             }
             }
             if (!isObjectNear(id)) return;
             if (!isObjectNear(id)) return;
 
             debug("Got message (active world object " + (string) kSource + "): name=" + name+ "id=" + (string) id + " message=" + message);
             debug("Got message (active world object " + (string) kSource + "): name=" + name+ "id=" + (string) id + " message=" + message);
   
   
Line 622: Line 626:
                 return;
                 return;
             }
             }
 
             loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request
             loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request
 
             if (!isObjectKnow(id))
             if (!isObjectKnow(id))
             {
             {
Line 633: Line 637:
                 }
                 }
             }
             }
 
             debug("Executing: " + (string) kSource);
             debug("Executing: " + (string) kSource);
             execute(name, id, message);
             execute(name, id, message);
Line 649: Line 653:
                     execute(sPendingName, sPendingId, sPendingMessage);
                     execute(sPendingName, sPendingId, sPendingMessage);
                 }
                 }
 
                 // clear pending request
                 // clear pending request
                 sPendingName="";
                 sPendingName="";
Line 675: Line 679:
         }
         }
     }
     }
 
     changed(integer change)
     changed(integer change)
     {
     {
Line 684: Line 688:
     }
     }
}
}
</lsl>
==History==
1.014a
* fix of loophole in ask-mode by Felis Darwin
1.014
* improved compatibility with existing world objects and simplified the world-object coding
** commands to remove non-existing restrictions must be ignored silently by the relay (without spamming the user with pointless request-for-permission dialog)
** simple (harmless) commands can now be joined in one single message without triggering the permission dialog.
** multiple pending messages from the same object are now stored over the permission dialog.
1.013e: no changes in the specificiation, just in the sample code
* Verified how far away the object is that is trying to control you. The specification says that the object must use llSay for 20meters max range. But as the object is not trustworth this must be checked in the relay again.Previously llShout (for 100 meters) and llRegionSay (for the complete sim) did work, too.
* the permission request dialog was shown even if the command was for another person
* the ping/pong on login did not verify whether the pong event was for us and not some other person.
* fixed a problem which caused additional questions for permission dialogs
* now automatically accepts commands from an object you were forced to sit on by the relay (so you only have to confirm that once)
* fixed force sit on re-login which could fail if the login was very slow
* code cleanup
1.013
* fixed force-sit on login (by delaying it for 10 seconds)
* allow meta commands without asking for permission
* fixed a vulnerability which allowed faked responses for the permission dialog
* extended object identity check for the object/parcel owner (before it checked only the group but there is groupless personal property out there)
* prevent turning off of the relay when it is locked
1.012
Fixed a bug in !release which caused the relay to reapply those restrictions on login for the object NULL_KEY. But as there is no ping/pong for NULL_KEY the wearer was stuck.
1.011
Precision on the ping-pong routine : relay standard message would be "ping,<object_uuid>,ping,ping" so objects can keep a listener with a static filter, to reduce lag. Thank you again Monica Jewell for the suggestion.
1.010
Added the ping-pong routine as a way to ensure the object is still available when the user relogs. Also updated the sample code to handle the timeout. Thank you Monica Jewell for pointing that possible problem out.


1.000
</source>
First release

Latest revision as of 12: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();
        }
    }
}