Difference between revisions of "LSL Protocol/Restrained Love Relay/Reference Implementation"
Jump to navigation
Jump to search
Maike Short (talk | contribs) |
m (<lsl> tag to <source>) |
||
(24 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== | |||
= | <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 | <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 48: | Line 53: | ||
//~ 2008-03-03 code cleanup 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-03-05 silently ignore commands for removing restrictions if they are not active anyway | ||
//~ 2008-06-24 fix of loophole in ask-mode by Felis Darwin | |||
//~ 2008-09-01 changed llSay to llShout, increased distance check (MK) | |||
//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy, | //~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy, | ||
//~ completeness or performance. It may only be distributed in its full source code, | //~ completeness or performance. It may only be distributed in its full source code, | ||
Line 60: | 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 = | 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 = | 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 92: | 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 122: | Line 129: | ||
ack(string cmd_id, key id, string cmd, string ack) | ack(string cmd_id, key id, string cmd, string ack) | ||
{ | { | ||
llShout(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack); | |||
} | } | ||
// cmd begins with a '@' | // cmd begins with a '@' | ||
sendRLCmd(string cmd) | sendRLCmd(string cmd) | ||
Line 130: | Line 137: | ||
llOwnerSay(cmd); | llOwnerSay(cmd); | ||
} | } | ||
// get current mode as string | // get current mode as string | ||
string getModeDescription() | string getModeDescription() | ||
Line 138: | 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 152: | 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 165: | 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 171: | 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 182: | Line 189: | ||
} | } | ||
} | } | ||
return FALSE; | return FALSE; | ||
} | } | ||
// check whether the object is in | // check whether the object is in llShout distance. It could have moved | ||
// | // before the message is received (chatlag) | ||
integer isObjectNear(key id) | integer isObjectNear(key id) | ||
{ | { | ||
Line 199: | 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 207: | 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 220: | 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 227: | 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 237: | 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 257: | Line 262: | ||
return TRUE; | return TRUE; | ||
} | } | ||
// removing restriction | // removing restriction | ||
if ((param == "y") || (param == "rem")) | if ((param == "y") || (param == "rem")) | ||
Line 264: | 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 270: | 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 280: | 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 300: | 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 311: | Line 316: | ||
return FALSE; | return FALSE; | ||
} | } | ||
// extract the commands-part | // extract the commands-part | ||
list tokens = llParseString2List (message, [","], []); | list tokens = llParseString2List (message, [","], []); | ||
Line 320: | 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 326: | 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. | ||
if (tryToGluePendingCommands(id, commands)) | if (tryToGluePendingCommands(id, commands)) | ||
{ | { | ||
return | return FALSE; | ||
} | } | ||
// check whether this object belongs here | // check whether this object belongs here | ||
integer trustworthy = isObjectIdentityTrustworthy(id); | integer trustworthy = isObjectIdentityTrustworthy(id); | ||
Line 341: | 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 355: | 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 396: | 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 406: | 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 414: | Line 419: | ||
else if (param=="y" || param=="rem") // remove from lRestrictions | else if (param=="y" || param=="rem") // remove from lRestrictions | ||
{ | { | ||
if (ind>-1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind); | if (ind > -1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind); | ||
if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY; | if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY; | ||
} | } | ||
workaroundForAtClear(command); | workaroundForAtClear(command); | ||
rememberForceSit(command); | rememberForceSit(command); | ||
Line 423: | 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 436: | 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 447: | 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 460: | 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 488: | Line 493: | ||
loginPendingForceSit = FALSE; | loginPendingForceSit = FALSE; | ||
} | } | ||
// --------------------------------------------------- | // --------------------------------------------------- | ||
Line 505: | 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 525: | Line 530: | ||
} | } | ||
} | } | ||
// send a ping request and start a timer | // send a ping request and start a timer | ||
pingWorldObjectIfUnderRestrictions() | pingWorldObjectIfUnderRestrictions() | ||
Line 538: | Line 543: | ||
} | } | ||
} | } | ||
default | default | ||
{ | { | ||
Line 558: | Line 563: | ||
llOwnerSay(getModeDescription()); | llOwnerSay(getModeDescription()); | ||
} | } | ||
timer() | timer() | ||
{ | { | ||
Line 571: | Line 576: | ||
releaseRestrictions(); | releaseRestrictions(); | ||
} | } | ||
if (loginPendingForceSit) | if (loginPendingForceSit) | ||
{ | { | ||
Line 588: | Line 593: | ||
else | else | ||
{ | { | ||
sendRLCmd ("@sit:"+(string) | sendRLCmd ("@sit:"+(string)lastForceSitDestination+"=force"); | ||
} | } | ||
} | } | ||
if (!loginPendingForceSit && !loginWaitingForPong) | if (!loginPendingForceSit && !loginWaitingForPong) | ||
{ | { | ||
Line 606: | Line 611: | ||
return; | return; | ||
} | } | ||
if (nMode== MODE_OFF) | if (nMode== MODE_OFF) | ||
{ | { | ||
Line 613: | 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 621: | 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 632: | Line 637: | ||
} | } | ||
} | } | ||
debug("Executing: " + (string) kSource); | debug("Executing: " + (string) kSource); | ||
execute(name, id, message); | execute(name, id, message); | ||
Line 648: | Line 653: | ||
execute(sPendingName, sPendingId, sPendingMessage); | execute(sPendingName, sPendingId, sPendingMessage); | ||
} | } | ||
// clear pending request | // clear pending request | ||
sPendingName=""; | sPendingName=""; | ||
Line 674: | Line 679: | ||
} | } | ||
} | } | ||
changed(integer change) | changed(integer change) | ||
{ | { | ||
Line 683: | Line 688: | ||
} | } | ||
} | } | ||
</source> | |||
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();
}
}
}