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

From Second Life Wiki
Jump to navigation Jump to search
(moved code to subpages, published version 1.040.a with support for multiple object)
Line 1: Line 1:
{{Restrained Life Relay Specs TOC}}
{{Restrained Life Relay Specs TOC}}


You can get a free relay object at the MSM Restraints shop in "Stonehaven". http://slurl.com/secondlife/Stonehaven%20Island/155/79/301
__NOTOC__
 
== Introduction ==
 
You can get this relay in MSM Restraints shop in Stonehaven: http://slurl.com/secondlife/Stonehaven%20Island/155/79/301
 
== Code ==
 
=== Multi Object Support ===
 
* Root Prim
** [[LSL Protocol/Restrained Life Relay/Other Implementations/Maike Short's Relay/Relay Manager|Relay Manager]] (delegates to the relay units)
** [[LSL Protocol/Restrained Life Relay/Other Implementations/Maike Short's Relay/Extended Controller|Extended Controller]] (optional additional functionality)
* Child Prims named "Relay Unit ''n''"
** [[LSL Protocol/Restrained Life Relay/Other Implementations/Maike Short's Relay/Relay Unit|Relay Units]]
* Clickable Child Prims
** [[LSL Protocol/Restrained Life Relay/Other Implementations/Maike Short's Relay/Toucher|Toucher]]
* !vision Prim
** [[LSL_Protocol/Restrained_Life_Relay/Other_Implementations/Think_Kink_Restrained_Life_PBA#The_GPU|!vision]] (by Chloe1982 Constantine and Ilana Debevec)
 
 
=== Old Single Object, Single Script version ===
 
* [[LSL Protocol/Restrained Life Relay/Other Implementations/Maike Short's Relay/Relay Simple|Relay Simple]]
 


== Changes ==
== Changes ==
=== 1.040.a ===
* added support for multiple world objects
* show world map with tp destination if force-tp is disabled


=== 1.040 ===
=== 1.040 ===
Line 46: Line 74:


* see [[LSL Protocol/Restrained Life Relay/Change_History#1.014_to_1.015.a|Changes to the reference implemenation 1.014 to 1.015.a]]
* see [[LSL Protocol/Restrained Life Relay/Change_History#1.014_to_1.015.a|Changes to the reference implemenation 1.014 to 1.015.a]]
== Code ==
[[LSL Protocol/Restrained Life Relay/Other Implementations/Maike Short's Relay/Alpha Version|Experimental Multi Object Support]]
<lsl>
// RestrainedLife Viewer Relay Script
//
// By Maike Short
//
// While this script used to be based on Marine's sample implementations it was
// so heavily modified and extended that there is little of the original code
// remaining. So please do not bother Marine with any problems but report them to
// https://wiki.secondlife.com/wiki/LSL_Protocol/Restrained_Life_Relay/Bugs_and_Pendings_Features
//
// Thanks to Felis Darwin, Chorazin Allen, Azoth Amat, Gregor Mougin,
// Nano Siemens, Cerdita Piek, Satomi Ahn, Marissa Mistwallow,
// Chloe1982 Constantine, Ilana Debevec, Kitty Barnett and many others.
//
// Many thanks to she who started it all ... Marine Kelley.
//
// This script is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy, completeness
// or performance. It may be distributed in its full source code with this header and
// disclaimer and is not to be sold without permission. Optionally it may be distributed
// in a 'no mod' form within Second Life™ PROVIDED that either a link to the full source
// IS provided (ie. this page or a .zip or .rar) within the object the code is contained
// AND/OR a off-world contact point (ie. email) that the source code may be requested from.
// This somewhat unusual (no-mod) distribution is allowed to maintain the integrity and
// reliability of the creator reference of in-world objects (scripts). Changes and
// improvement to this code must be shared with the community so that we ALL benefit.
integer DEBUG = FALSE;
// list of refused commands. Use [] to allow everything or
// ["@detach", "@remoutfit"] to prevent striping for example
list refusedForceCommands = [];
// ---------------------------------------------------
//                    Constants
// ---------------------------------------------------
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page
string RLVRS_IMPL_VERSION = "Based on Maike's Implemenation 1.040"; // version of the implementation for debugging
string PREFIX_RL_COMMAND = "@";
string PREFIX_METACOMMAND = "!";
integer RLVRS_CHANNEL  = -1812221819;  // RLVRS in numbers
integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers
integer STATUS_LINK_CHANNEL = -1373421300;
integer RLV_LINK_CHANNEL = -1373421301;
integer CMD_LINK_CHANNEL = -1373421302;
integer DIALOG_LINK_CHANNEL = -1373421303;
integer ASK_LINK_CHANNEL = -1373421304;
integer GPU_CHANNEL      = -4360493;
integer MAX_OBJECT_DISTANCE = 100;    // 100m is llShout distance
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds
integer PERMISSION_DIALOG_TIMEOUT = 30;
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
integer MODE_OFF = 0;
integer MODE_ASK = 1;
integer MODE_AUTO = 2;
list MODE_DESCRIPTIONS = ["RLV Relay is OFF", "RLV Relay is ON (permission needed)", "RLV Relay is ON (auto-accept)"];
string RVL_COMMAND_START = "@this-is-a-script-generated-message-beyond-the-control-of-the-agent=n,detach=n";
string RVL_COMMAND_END = "@detach=y";
// ---------------------------------------------------
//                      Variables
// ---------------------------------------------------
integer mode;
list restrictions; // restrictions currently applied (without the "=n" part)
integer visionRestricted;
key source;        // UUID of the object I'm commanded by, always equal to NULL_KEY if restrictions is empty, always set if not
string pendingName; // name of initiator of pending request (first request of a session in ask mode)
key pendingId;      // UUID of initiator of pending request (first request of a session in ask mode)
string pendingMessage; // message of pending request
integer pendingTime;
// used on login
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)
integer loginWaitingForPong;
integer loginPendingForceSit;
key    lastForceSitDestination;
integer lastForceSitTime;
integer isNotEmbedded = TRUE;
// current message
// using global variables because the content can get huge and uses up a lot stack memory otherwise
string currentMessage = "";
list currentMessageToken = [];
string currentMessageCommands;
list currentMessageCommandList;
// ---------------------------------------------------
//              Helper functions
// ---------------------------------------------------
debug(string x)
{
    if (DEBUG)
    {
        llOwnerSay("DEBUG: " + x);
    }
}
// ---------------------------------------------------
//              Low Level Communication
// ---------------------------------------------------
// acknowledge or reject
ack(string cmd_id, key id, string cmd, string ack)
{
    llShout(RLVRS_CHANNEL, cmd_id + "," + (string) id + "," + cmd + "," + ack);
}
// cmd begins with a '@'
sendRLCmd(string cmd)
{
    if (cmd != "")
    {
        if (isNotEmbedded)
        {
            llOwnerSay(cmd);
        }
        llMessageLinked(LINK_SET, RLV_LINK_CHANNEL, cmd, source);
    }
}
// splits the relay message into tokens
splitMessage()
{
    currentMessageToken = llParseString2List(currentMessage, [","], []);
    currentMessageCommands = llList2String(currentMessageToken, 2);
    currentMessageCommandList = llParseString2List(currentMessageCommands, ["|"], []);
}
// check that this command is for us and not someone else
integer verifyWeAreTarget()
{
    if (llGetListLength(currentMessageToken) != 3) // this is not a normal command
    {
        return FALSE;
    }
    return (llList2String(currentMessageToken, 1) == llGetOwner()); // talking to me ?
}
// ---------------------------------------------------
//              Permission Handling
// ---------------------------------------------------
// are we already under command by this object?
integer isObjectKnow(key id)
{
    // are we already under command by this object?
    if (source == id)
    {
        return TRUE;
    }
    // are we not under command by any object but were we forced to sit on this object recently?
    debug("source=" + (string) source + " id=" + (string) id + " lastForceSitDestination=" + (string) lastForceSitDestination);
    if ((source == NULL_KEY) && (id == lastForceSitDestination))
    {
        debug("on last force sit target");
        if (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime())
        {
            debug("and recent enough to auto accept");
            return TRUE;
        }
    }
    return FALSE;
}
// check whether the object is in llShout distance.
integer isObjectNear(key id)
{
    vector myPosition = llGetRootPosition();
    list temp = llGetObjectDetails(id, ([OBJECT_POS]));
    vector objPosition = llList2Vector(temp,0);
    if (objPosition == <0, 0, 0>)
    {
        // object is not in this sim nor in one of the adjacent sims
        return FALSE;
    }
    float distance = llVecDist(objPosition, myPosition);
    return distance <= MAX_OBJECT_DISTANCE;
}
// do a basic check on the identity of the object trying to issue a command
integer isObjectIdentityTrustworthy(key id)
{
    key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);
    key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);
    key object_owner=llGetOwnerKey(id);
    key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
    debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
    debug("group= " + (string) parcel_group + " / " + (string) object_group);
    if (object_owner==llGetOwner ()        // IF I am the owner of the object
      || object_owner==parcel_owner        // OR its owner is the same as the parcel I'm on
      || (object_group==parcel_group && parcel_group != NULL_KEY)        // OR its group is the same as the parcel I'm on
    )
    {
        return TRUE;
    }
    return FALSE;
}
// Is this a simple request for information or a meta command like !release?
integer isSimpleRequest()
{
    integer len = llGetListLength(currentMessageCommandList);
    integer i;
    // now check every single atomic command
    for (i=0; i < len; ++i)
    {
        if (!isSimpleAtomicCommand(i))
        {
          return FALSE;
        }
    }
    // all atomic commands passed the test
    return TRUE;
}
// is this a simple atomar command
// (a command which only queries some information or releases restrictions)
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)
integer isSimpleAtomicCommand(integer i)
{
    string cmd = llList2String(currentMessageCommandList, i);
    cmd = llToLower(llStringTrim(cmd, STRING_TRIM));
    // check right hand side of the "=" - sign
    integer index = llSubStringIndex (cmd, "=");
    if (index > -1) // there is a "="
    {
        // check for a number after the "="
        string param = llGetSubString (cmd, index + 1, -1);
        if ((integer)param!=0 || param=="0") // is it an integer (channel number)?
        {
            return TRUE;
        }
        // removing restriction
        if ((param == "y") || (param == "rem"))
        {
            return TRUE;
        }
    }
    // check for a leading ! (meta command)
    if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
    {
        return llGetSubString(cmd, 0, 11) == "!visionclear"
            || llGetSubString(cmd, 0, 6) != "!vision";
    }
    // check for @clear
    // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
    // (but we need this check here because "!release|@clear" is a BROKEN attempt to work around
    // a bug in the first relay implementation. You should refuse to use relay versions < 1013
    // instead.)
    if (cmd == "@clear")
    {
        return TRUE;
    }
 
    // this one is not "simple".
    return FALSE;
}
// If we already have commands from this object pending
// because of a permission request dialog, just add the
// new commands at the end.
// Note: We use a timeout here because the player may
// have "ignored" the dialog.
integer tryToGluePendingCommands(key id)
{
    if (llStringLength(pendingMessage) > 2000)
    {
        llSay(0, llKey2Name(id) + " is flooding commands. Releasing restrictions.");
        releaseRestrictions();
        return TRUE;
    }
    if ((pendingId == id) && (pendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
    {
        debug("Gluing ");
        pendingMessage = pendingMessage + "|" + currentMessageCommands;
        return TRUE;
    }
    return FALSE;
}
// accept !release even if out of range
handleCommandsWhichAreAcceptedOutOfRange(key id)
{
    if (id != source)
    {
        return;
    }
    if (llGetListLength (currentMessageToken) < 3)
    {
        return;
    }
    if (llListFindList(currentMessageCommandList, ["!release"]) > -1)
    {
        debug("accepted !release although it was out of range");
        releaseRestrictions();
    }
}
// verifies the permission. This includes mode
// (off, permission, auto) of the relay and the
// identity of the object (owned by parcel people).
integer verifyPermission(key id, string name)
{
    // is it switched off?
    if (mode == MODE_OFF)
    {
        return FALSE;
    }
    if (llGetListLength (currentMessageToken) < 3)
    {
        return FALSE;
    }
    // if we are already having a pending permission-dialog request for THIS object,
    // just add the new commands at the end of the pending command list.
    if (tryToGluePendingCommands(id))
    {
        return FALSE;
    }
    // accept harmless commands silently
    if (isSimpleRequest())
    {
        return TRUE;
    }
    // check whether this object belongs here
    integer trustworthy = isObjectIdentityTrustworthy(id);
    string warning = "";
    if (!trustworthy)
    {
        warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";
    }
    // ask in permission-request-mode and/OR in case the object identity is suspisous.
    if (mode == MODE_ASK || !trustworthy)
    {
        debug("asking");
        pendingId=id;
        pendingName=name;
        currentMessageToken = [];
        currentMessageCommands = "";
        currentMessageCommandList = [];
        pendingMessage = currentMessage;
        currentMessage = "";
        pendingTime = llGetUnixTime();
        if (llKey2Name(llGetOwnerKey(id)) != "")
        {
            name += " (owned by " + llKey2Name(llGetOwnerKey(id)) + ")";
        }
        list temp = llGetObjectDetails(id, [OBJECT_POS]);
        vector pos = llList2Vector(temp, 0);
        string controller = getControllerName();
       
        string text = name
            + " at <" + (string) ((integer) pos.x)
            + ", " + (string) ((integer) pos.y)
            + ", " + (string) ((integer) pos.z)
            + "> (" + (string) ((integer) llVecDist(pos, llGetRootPosition())) + "m)";
        if (controller != "")
        {
            text = text + " operated by " + controller;
        }       
        text = text + " would like to control your viewer." + warning + "\n\nDo you accept ?";
        debug("Asking for permission isNotEmbedded=" + (string) isNotEmbedded);
        if (isNotEmbedded)
        {
            llDialog (llGetOwner(), text, ["Yes", "No"], DIALOG_CHANNEL);
        }
        llMessageLinked(LINK_SET, ASK_LINK_CHANNEL, text, id);
        debug("Asking for permission");
        return FALSE;
    }
    return TRUE;
}
// gets the name of the last controller or ""
// if there is either no !who-command, it is for NULL_KEY,
// or the avatar is not in the sim
string getControllerName()
{
    string controller = "";
    integer i;
    integer count = llGetListLength(currentMessageCommandList);
    for (i = 0; i < count; i++)
    {
        string command = llList2String(currentMessageCommandList, i);
        list commandTokens = llParseString2List(command, ["/"], []);
        if (llList2String(commandTokens, 0) == "!who")
        {
            controller = llKey2Name(llList2String(commandTokens, 1));
        }
    }
    return controller;
}
// ---------------------------------------------------
//              Executing of commands
// ---------------------------------------------------
// execute a non-parsed message
// this command could be denied here for policy reasons
execute(string name, key id)
{
    currentMessage = "";
    currentMessageCommands = "";
    string cmd_id = llList2String(currentMessageToken, 0); // CheckAttach
    currentMessageToken = [];
    integer len = llGetListLength (currentMessageCommandList);
    integer i;
    string command;
    string prefix;
    for (i=0; i<len; ++i) // execute every command one by one
    {
        // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
        command = llList2String(currentMessageCommandList, i);
        prefix = llGetSubString(command, 0, 0);
        if (prefix == PREFIX_RL_COMMAND) // this is a RLV command
        {
            executeRLVCommand(cmd_id, id, command);
        }
        else if (prefix == PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
        {
            integer continue = executeMetaCommand(cmd_id, id, command);
            if (!continue)
            {
                return;
            }
        }
    }
}
// executes a command for the restrained life viewer
// with some additinal magic like book keeping
executeRLVCommand(string cmd_id, string id, string command)
{
    command = llToLower(command);
    // we need to know whether whether is a rule or a simple command
    list tokens = llParseString2List(command, ["="], []);
    string behav = llList2String(tokens, 0); // @getattach:skull
    string param = llList2String(tokens, 1); // 2222
    integer ind = llListFindList(restrictions, [behav]);
    tokens = llParseString2List(behav, [":"], []); // @sit:<uuid>
    string behavName = llList2String (tokens, 0);  // @sit
    string option = llList2String (tokens, 1);    // <uuid>
   
    debug("executeRLVCommand: behav=!" + behav + "! param=!" + param + "!");
    if (isQuery(behavName) && ((integer) param <= 0))
    {
        // if this is a query command, don't accept public chat,
        // negative numbers, or pseudo channels like
        // "n", "add", "y", "rem", "force",  ...
        ack(cmd_id, id, command, "ko");
        return;
    }
    if (param=="n" || param=="add") // add to restrictions
    {
        if (ind < 0)
        {
            if ((llGetListLength(restrictions) == 0) && !visionRestricted)
            {
                sendRLCmd(RVL_COMMAND_START);
                llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
            }
            restrictions += [behav];
        }
        source = id; // we know that source is either NULL_KEY or id already
    }
    else if (param == "y" || param == "rem") // remove from restrictions
    {
        if (behavName == "@this-is-a-script-generated-message-beyond-the-control-of-the-agent")
        {
            ack(cmd_id, id, command, "ko");
            return;
        }
       
        if (ind > -1)
        {
            restrictions = llDeleteSubList (restrictions, ind, ind);
        }
        if ((llGetListLength(restrictions) == 0) && !visionRestricted)
        {
            releaseRestrictions();
        }
        removeFromPendingList(behav);
    }
    else if (param == "force")
    {
        if (llListFindList(refusedForceCommands, [behavName]) >= 0)
        {
            debug("rejected force-command: behav=!" + behav + "! behavName=!" + behavName + "!");
            ack(cmd_id, id, command, "ko");
            return;
        }
    }
    else if (((integer) param <= 0) && (behavName != "@clear"))
    {
        // this is either an unknown param (not "n", "add", "y", "rem", "force")
        // or a query which should be answered on the public chat channel 0.
        // note negative channels are displayed as /-x on public chat.
        ack(cmd_id, id, command, "ko");
        return;
    }
    workaroundForAtClear(command);
    rememberForceSit(behavName, option, param);
    sendRLCmd(command); // execute command
    ack(cmd_id, id, command, "ok"); // acknowledge
}
// is this a query?
integer isQuery(string behav)
{
    return ((llGetSubString(behav, 0, 3) == "@get") ||
            (llGetSubString(behav, 0, 7) == "@version") ||
            (llGetSubString(behav, 0, 10) == "@findfolder"));
}
// check for @clear
// Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
// (but we need this check here because "!release|@clear" is a BROKEN attempt to work around
// a bug in the first relay implementation. You should refuse to use relay versions < 1013
// instead.)
workaroundForAtClear(string command)
{
    if (command == "@clear")
    {
        releaseRestrictions();
    }
}
// remembers the time and object if this command is a force sit
rememberForceSit(string behavName, string option, string param)
{
    // clear lastForceSitDestination in case we are now prevented from standing up and
    // the force sit was long ago. Note: restrictions is checked to prevent the
    // clearance in case @unsit is just send again on login
    if (behavName == "@unsit")
    {
        if (llListFindList(restrictions, ["@unsit"]) < 0)
        {
            if (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT < llGetUnixTime())
            {
                debug("clearing lastForceSitDestination");
                lastForceSitDestination = NULL_KEY;
            }
        }
    }
   
    if (param != "force")
    {
        return;
    }
    debug("'force'-command:" + behavName + "/" + option);
    if (behavName != "@sit")
    {
        return;
    }
    lastForceSitDestination = (key) option;
    lastForceSitTime = llGetUnixTime();
    debug("remembered force sit");
}
// executes a meta command which is handled by the relay itself
integer executeMetaCommand(string cmd_id, string id, string commandString)
{
    list tokens = llParseString2List(commandString, ["/"], []);
    string command = llList2String(tokens, 0);
   
    if (command == PREFIX_METACOMMAND + "version") // checking relay protocol version
    {
        ack(cmd_id, id, command, (string) RLVRS_PROTOCOL_VERSION);
    }
    else if (command == PREFIX_METACOMMAND + "implversion") // checking relay version
    {
        ack(cmd_id, id, command, RLVRS_IMPL_VERSION);
    }
    else if (command == PREFIX_METACOMMAND + "release") // release all the restrictions (end session)
    {
        releaseRestrictions();
        ack(cmd_id, id, command, "ok");
    }
    else if (command == PREFIX_METACOMMAND + "handover")
    {
        handleHandOver(id, tokens);
        return FALSE;
    }
    else if (command == PREFIX_METACOMMAND + "pong") // object is still available for us
    {
        loginWaitingForPong = FALSE;
    }
    else if (llGetSubString(command, 0, 11) == PREFIX_METACOMMAND + "visionclear")
    {
        visionRestricted = FALSE;
        ack(cmd_id, id, command, "ok");
        if (llGetListLength(restrictions) == 0)
        {
            releaseRestrictions();
      }
      llMessageLinked(LINK_SET, GPU_CHANNEL, commandString, source);
    }
    else if (llGetSubString(command, 0, 6) == PREFIX_METACOMMAND + "vision")
    {
        integer attached = llGetAttached();
        if (attached >= ATTACH_HUD_CENTER_2 || attached < ATTACH_HUD_BOTTOM_RIGHT)
        {
            if (llGetListLength(restrictions) == 0)
            {
                llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
                sendRLCmd(RVL_COMMAND_START);
            }
            llMessageLinked(LINK_SET, GPU_CHANNEL, commandString, source);
            ack(cmd_id, id, command, "ok");
            visionRestricted = TRUE;
            source = id;
        }
        else
        {
            ack(cmd_id, id, command, "ko");
        }
    }
    else
    {
        ack(cmd_id, id, command, "ko");
    }
    return TRUE;
}
handleHandOver(string id, list tokens)
{
    debug("handover");
    key targetObject = (key) llList2String(tokens, 1);
    integer keepRestrictions = (integer) llList2String(tokens, 2);
    if (!keepRestrictions)
    {
        releaseRestrictions();
    }
    source = targetObject;
    if (llGetListLength(restrictions) == 0)
    {
        sendRLCmd(RVL_COMMAND_START);
        llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "start", id);
    }
    pingWorldObjectIfUnderRestrictions();
}
// removes a restriction from the list of pending commands
removeFromPendingList(string behav)
{
    string search = behav + "=";
    // don't do the expensive parsing (string operations are very slow in pre-
    // mono LSL) in case we can detect fast that this one is not in the list.
    if (llSubStringIndex(pendingMessage, search) < 0)
    {
        return;
    }
    list tokens = llParseString2List(pendingMessage, [","], []);
    list commands = llParseString2List(llList2String(tokens, 2), ["|"], []);
    integer modified = FALSE;
    integer len = llGetListLength(commands);
    integer i;
    for (i = len - 1; i >= 0; i--)
    {
        string cmd = llList2String(commands, i);
        if (llSubStringIndex(cmd, search) == 0)
        {
            if (llSubStringIndex(cmd, "=n") > -1 || llSubStringIndex(cmd, "=add") > -1)
            {
                commands = llDeleteSubList(commands, i, i);
                modified = TRUE;
            }
        }
    }
    if (modified)
    {
        if (llGetListLength(commands) > 0)
        {
            pendingMessage = llList2String(tokens, 0) + "," + llList2String(tokens, 1) + "," + llDumpList2String(commands, "|");
        }
        else
        {
            clearPendingMessages();
        }
    }
}
// lift all the restrictions (called by !release and by turning the relay off)
releaseRestrictions()
{
    integer i;
    integer len = llGetListLength (restrictions);
    for (i = 0; i < len; ++i)
    {
        sendRLCmd(llList2String (restrictions, i) + "=y");
    }
    sendRLCmd(RVL_COMMAND_END);
    lastForceSitDestination = NULL_KEY;
    restrictions = [];
    loginPendingForceSit = FALSE;
    clearPendingMessages();
    visionRestricted = FALSE;
    llMessageLinked(LINK_SET, GPU_CHANNEL, "!visionclear", source);
    llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "release", source);
    source = NULL_KEY;
}
// sends an !release,ok to the world object if we are in an active session
tellWorldObjectAboutCanceledSession()
{
    if (source != NULL_KEY)
    {
        ack("release", source, "!release", "ok");
    }
}
// deletes the list of pending messsages
clearPendingMessages()
{
    // clear pending request
    pendingName = "";
    pendingId = NULL_KEY;
    pendingMessage = "";
    pendingTime = 0;
}
// processes a message send on the relay channel
processRelayMessage(string name, key id)
{
    if (mode== MODE_OFF)
    {
        debug("deactivated - ignoring commands");
        return; // mode is 0 (off) => reject
    }
    debug("LISTEN: " + currentMessage);
    splitMessage();
    if (!verifyWeAreTarget())
    {
      return;
    }
    debug("Got message (active world object " + (string) source + "): name=" + name+ " id=" + (string) id);
    if (source)
    {
        if (source != id)
        {
            if (isObjectNear(source))
            {
                debug("already used by another object => reject");
                return;
            }
            else
            {
                debug("already used by another object, which is out of range");
                releaseRestrictions();
            }
        }
    }
    if (!isObjectNear(id))
    {
        handleCommandsWhichAreAcceptedOutOfRange(id);
        return;
    }
    if (!isObjectKnow(id))
    {
        debug("asking for permission because source is NULL_KEY");
        if (!verifyPermission(id, name))
        {
            return;
        }
    }
    debug("Executing: " + (string) source);
    execute(name, id);
}
// ---------------------------------------------------
//            mode and dialog handling
// ---------------------------------------------------
// shows current mode as string
showModeDescription()
{
    if (isNotEmbedded)
    {
        llOwnerSay(llList2String(MODE_DESCRIPTIONS, mode));
    }
    llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "show_mode", (key) ((string) mode));
}
// process the Yes/No buttons of the permission dialog
processDialogResponse(key id, string message)
{
    if (pendingId)
    {
        if (message == "Yes") // pending request authorized => process it
        {
            debug("recovering pendingMessage");
            currentMessage = pendingMessage;
            debug("splitting");
            splitMessage();
            debug("executing");
            execute(pendingName, pendingId);
        }
        clearPendingMessages();
    }
}
// ---------------------------------------------------
//            initialisation and login handling
// ---------------------------------------------------
init() {
    sendRLCmd("@clear");
    llOwnerSay("Waiting for plugins to initialize...");
    llSleep(5);
    mode = MODE_ASK;
    llListen(RLVRS_CHANNEL, "", "", "");
    llListen(DIALOG_CHANNEL, "", llGetOwner(), "");
    if (llGetFreeMemory() < 10000)
    {
        llSay(0, llKey2Name(llGetOwner()) + ", your relay is having very little free script memory and is likly to crash during use. Please verify that it was compiled using Mono.");
    }
    llMessageLinked(LINK_SET, STATUS_LINK_CHANNEL, "init", (key) ((string) RLVRS_PROTOCOL_VERSION));
    llOwnerSay("Initialized and ready");
    debug("Free Memory: " + (string) llGetFreeMemory());
}
// sends the known restrictions (again) to the RL-viewer
// (call this functions on login)
reinforceKnownRestrictions()
{
    integer i;
    integer len = llGetListLength(restrictions);
    string restr;
    debug("source=" + (string) source);
    if (len > 0)
    {
        sendRLCmd(RVL_COMMAND_START);
    }
    for (i=0; i < len; ++i)
    {
        restr = llList2String(restrictions, i);
        debug("restr=" + restr);
        sendRLCmd(restr + "=n");
        if (restr == "@unsit")
        {
            loginPendingForceSit = TRUE;
        }
    }
}
// send a ping request and start a timer
pingWorldObjectIfUnderRestrictions()
{
    loginWaitingForPong = FALSE;
    if (source)
    {
        ack("ping", source, "ping", "ping");
        timerTickCounter = 0;
        llSetTimerEvent(1.0);
        loginWaitingForPong = TRUE;
    }
}
sendForceSitDuringLogin()
{
    key sitTarget = source;
    if (lastForceSitDestination)
    {
        sitTarget = lastForceSitDestination;
    }
    debug("Force sit during login on " + (string) sitTarget + " (source=" + (string) source + ", lastForceSitDestination=" + (string) lastForceSitDestination + ")");
    sendRLCmd ("@sit:" + (string) sitTarget + "=force");
}
// processes a timer event
processTimer()
{
    timerTickCounter++;
    debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
    if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))
    {
        llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available.");
        loginWaitingForPong = FALSE;
        loginPendingForceSit = FALSE;
        releaseRestrictions();
    }
    if (loginPendingForceSit)
    {
        integer agentInfo = llGetAgentInfo(llGetOwner());
        if (agentInfo & AGENT_SITTING)
        {
            loginPendingForceSit = FALSE;
            debug("is sitting now");
        }
        else if (timerTickCounter == LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
        {
            llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
            loginPendingForceSit = FALSE;
            releaseRestrictions();
        }
        else
        {
            sendForceSitDuringLogin();
        }
    }
    if (!loginPendingForceSit && !loginWaitingForPong)
    {
        llSetTimerEvent(0.0);
    }
}
processLinkMessage(key id, string message)
{
    if (message == "safeword")
    {
        llSay(0, llKey2Name(llGetOwner()) + " said the safeword. Freeing and deactivating relay.");
        tellWorldObjectAboutCanceledSession();
        releaseRestrictions();
        mode = MODE_OFF;
        showModeDescription();
        return;
    }
    if (message == "unlock")
    {
        if (id)
        {
            llSay(0, llKey2Name(id) + " freed " + llKey2Name(llGetOwner()) + " by unlocking the relay.");
            tellWorldObjectAboutCanceledSession();
        }
        releaseRestrictions();
        return;
    }
    if (message == "mode")
    {
        if (source)
        {
            llOwnerSay("Sorry, you cannot change the relay mode while it is active.");
            return;
        }
        mode = ((integer) ((string) id)) % 3;   
        showModeDescription();
        return;
    }
    if (message == "refusedForceCommands")
    {
        refusedForceCommands = llParseString2List((string) id, [","], []);
        return;
    }
    if (message == "embedded")
    {
        isNotEmbedded = FALSE;
        return;
    }
}
default
{
    state_entry()
    {
        init();
    }
    on_rez(integer start_param)
    {
        // relogging, we must refresh the viewer and ping the object if any
        // if mode is not OFF, fire all the stored restrictions
        if (mode)
        {
            reinforceKnownRestrictions();
            pingWorldObjectIfUnderRestrictions();
        }
        // remind the current mode to the user
        showModeDescription();
    }
   
    attach(key id)
    {
        if (id == NULL_KEY)
        {
            tellWorldObjectAboutCanceledSession();
            releaseRestrictions();
        }
    }
    timer()
    {
        processTimer();
    }
    listen(integer channel, string name, key id, string message)
    {
        currentMessageToken = [];
        currentMessageCommands = "";
        currentMessageCommandList = [];
        currentMessage = message;
        message = ""; // free memory
        if (channel==RLVRS_CHANNEL)
        {
            processRelayMessage(name, id);
        }
        else if (channel==DIALOG_CHANNEL)
        {
            processDialogResponse(id, message);
        }
    }
   
    link_message(integer sender, integer channel, string message, key id)
    {
        currentMessageToken = [];
        currentMessageCommands = "";
        currentMessageCommandList = [];
        if (channel == CMD_LINK_CHANNEL)
        {
            processLinkMessage(id, message);
        }
        else if (channel == DIALOG_LINK_CHANNEL)
        {
            processDialogResponse(id, message);
        }
    }
    touch_start(integer num_detected)
    {
        if (!isNotEmbedded)
        {
            return;
        }
        // touched by user => cycle through OFF/ON_PERMISSION/ON_ALWAYS modes
        key toucher = llDetectedKey(0);
        if (toucher != llGetOwner())
        {
            return;
        }
        if (source)
        {
            llOwnerSay("Sorry, you cannot change the relay mode while it is active.");
            return;
        }
        mode = (mode + 1) % 3;
        if (mode == MODE_OFF)
        {
            releaseRestrictions();
        }
        showModeDescription();
    }
    changed(integer change)
    {
        if (change & (CHANGED_OWNER | CHANGED_INVENTORY | CHANGED_ALLOWED_DROP))
        {
            llResetScript();
        }
        if (change & (CHANGED_REGION | CHANGED_TELEPORT))
        {
            if (loginWaitingForPong)
            {
                if (source)
                {
                    ack("ping", source, "ping", "ping");
                }
            }
        }
    }
}
</lsl>

Revision as of 06:20, 15 March 2009


Introduction

You can get this relay in MSM Restraints shop in Stonehaven: http://slurl.com/secondlife/Stonehaven%20Island/155/79/301

Code

Multi Object Support


Old Single Object, Single Script version


Changes

1.040.a

  • added support for multiple world objects
  • show world map with tp destination if force-tp is disabled

1.040

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


1.030.c

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

1.030.b

  • Diff
  • fixed two faked avatar chat problems

1.030.a

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

1.030

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


1.020.c

  • Diff
  • Display position and distance in permission dialog

1.020.b

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

1.015