Jump to content

RLV Relay


Quistess Alpha
 Share

You are about to reply to a thread that has been inactive for 434 days.

Please take a moment to consider if this thread is worth bumping.

Recommended Posts

It turns out that LinksetData functions make implementing a RLV relay a good deal easier:

Assistant script and config:

string gSatOn;
key OWNER;
default
{   state_entry()
    {   // listed "DOM"s accept or deny RLV devices on your behalf.
        llLinksetDataWrite("@_DOM",""); // supports a comma separated list, or empty value.
  		// above "DOM"s can use this safeword to free you. (reset the other script after changing this setting)
  		// To free yourself in case of emergency, click and hold on the HUD for 5 seconds then release.
        llLinksetDataWrite("@_SAFEWORD","Free Tessa!");
  		// If you have any "DOM"s listed above, they must approve your safeword. 
  		// When @_AloneSafewordAllowed is 1, your click-and-hold safeword will work if none of your "DOM"s are nearby.
        llLinksetDataWrite("@_AloneSafewordAllowed","1");
    
        OWNER = llGetOwner();
        integer CHAN_ALLOW = llAbs((integer)("0x"+(string)OWNER));
        llListen(CHAN_ALLOW,"",OWNER,"Allow");
        llListen(CHAN_ALLOW,"",OWNER,"Deny");
        string DOM = llLinksetDataRead("@_DOM");
        if(DOM)
        {   list DOM = llCSV2List(DOM);
            integer i = llGetListLength(DOM);
            while(~--i)
            {   string DOM = llList2String(DOM,i); // yes, lsl supports confusing name overloading per-scope!
                llListen(CHAN_ALLOW,"",DOM,"Allow");
                llListen(CHAN_ALLOW,"",DOM,"Deny");
            }
        }
        llSetTimerEvent(20.0);
    }
    timer()
    {   gSatOn=llList2String(llGetObjectDetails(OWNER,[OBJECT_ROOT]),0);
    }
    attach(key ID)
    {   if(ID) if(gSatOn!=(string)llGetOwner()) llOwnerSay("@sit:"+gSatOn+"=force"); 
        //     ^^ second conditional isn't really neccessary, just avoids a 'no room to sit' error when attempting to sit on oneself.
    }
    listen(integer Channel, string Name, key ID, string Text)
    {   llLinksetDataWrite("@_Allow",(string)(Text=="Allow"));
    }
    changed(integer c)
    {   if(c&CHANGED_OWNER) llResetScript(); // mildly inefficient, but avoids listen handles.
    }
}

Main script:

integer MAX_RESTRICTORS = 6; // can handle up to 31, but that's gratuitous.
string SPEC_VERSION = "1100"; // Retrieved Feb 2023: https://wiki.secondlife.com/wiki/LSL_Protocol/Restrained_Love_Relay/Specification
string IMPL_VERSION = "LSDR:0001"; // this can be whatever we want?

string SAFEWORD = "SAFEWORD!";

float PING_DELAY = 20.0; // how long to wait for responses to log-in ping. no restrictions are applied before this delay.  

//integer FULL_MASK; // 0b111111 for MAX_RESTRICTORS==6.
list gRestrictors;
integer gMaskRestrictors;

integer CHAN_RLV = -1812221819;

integer CHAN_ALLOW; // = llAbs((integer)("0x"+"(string)OWNER"));

integer gOpenPing = FALSE; // are we listening for a pong response?


// Trivial performance enhancement:
list PIPE = ["|"]; 
list EMPTY_LIST = [];
key OWNER; // the wearer of this relay. if D/s features are added, call the other party DOM or TRUSTED.

integer check_rlv(string rlv,string suffix)
{   // check whether we like certain RLV commands or not.
    if(~llSubStringIndex(rlv,",")) return FALSE; // multiple RLV in one rlv_command seems out-of-spec.
    if( ("=y"==suffix) || ("=rem"==suffix) ) return TRUE; // undoing restrictions seems benign.
    if( "@failme"==rlv ) return FALSE; // for debugging and example.
    return TRUE; 
}

integer check_restrictor(key ID,string cmd,list rlv_list) // adds if new and passes security; returns 1+ the restrictor index.
{   integer i;
    for(;i<MAX_RESTRICTORS;++i)
    {   key r = llList2Key(gRestrictors,i);
        if(r==ID)
        {   //llSay(0,"Debug: found old restrictor in index "+(string)i);
            return i+1;
        }
    }
    
    // before checking for an open slot, allow some commands without adding as an approved restrictor:
    if(1==llGetListLength(rlv_list))
    {   string rlv = llList2String(rlv_list,0);
        if((integer)llDeleteSubString(rlv,0,llSubStringIndex(rlv,"="))) // =<channel> commands.
        {   llOwnerSay(rlv);
            llSay(CHAN_RLV,cmd+","+(string)ID+","+rlv+",ok"); // probably a bit verbose; the object should get a response from the viewer as well.
            return -1; // skip sendking ko response.
        }else if("!version"==rlv) //also handled by meta_command routine.
        {   llSay(CHAN_RLV,cmd+","+(string)ID+",!version,"+SPEC_VERSION);
            return -1;
        }else if("!implversion"==rlv) // also handled by meta_command routine.
        {   llSay(CHAN_RLV,cmd+","+(string)ID+",!implversion,"+IMPL_VERSION);
            return -1;
        }else if("!release"==rlv) // avoid giving out an error message when an untrusted object is trying to release. just do nothing.
        // this meta command is why it wouldn't be elegant to embed meta_command() up here.
        {   return -1;
        }else if( ("@unsit=force"==rlv) && (ID==llList2Key(llGetObjectDetails(OWNER,[OBJECT_ROOT]),0)) ) // because some objects are nice and ask to kick us off after we safeword, that shouldn't count as 're-grabbing'.
        {   llOwnerSay(rlv);
            llSay(CHAN_RLV,cmd+","+(string)ID+","+rlv+",ok"); // probably a bit verbose, does the object really care they successfully unsat us?
            return -1;
        }
    }
    
    if(gOpenPing==1) return FALSE; // do not add new restrictors in the opening moments after adding the relay/logging in.
    
    // check for an open slot:
    for(i=0;i<MAX_RESTRICTORS;++i)
    {   if(llList2Key(gRestrictors,i)=="")
        {   /* place any security check against the restrictor here, return FALSE if we don't like the restrictor.
               // for example, if(not a member of land group) return FALSE;.
            */
            // ask owner (or dominant(s)) via dialog and slave script synchronicity:
            llLinksetDataDelete("@_Allow");
            integer pass=1;
            string DOM = llLinksetDataRead("@_DOM");
            if(DOM)
            {   list DOM = llCSV2List(DOM);
                integer i = llGetListLength(DOM);
                while(~--i)
                {   string DOM = llList2String(DOM,i);
                    if(llGetAgentSize(DOM))
                    {   if(llVecDist(llGetPos(),llList2Vector(llGetObjectDetails(DOM,[OBJECT_POS]),0))<20.0)
                        {   pass=0; // found a dominant, don't ask wearer's permission.
                            llDialog(DOM,llKey2Name(ID)+" owned by secondlife:///app/agent/"+
                                (string)llGetOwnerKey(ID)+"/inspect would like to interface with secondlife:///app/agent/"+
                                (string)OWNER+"/inspect 's relay.",
                                ["Allow","Deny"],CHAN_ALLOW);
                        }
                    }
                }
            }
            if(pass)
            {   llDialog(OWNER,llKey2Name(ID)+" owned by secondlife:///app/agent/"+
                    (string)llGetOwnerKey(ID)+"/inspect would like to interface via the RLV-relay protocol.",
                    ["Allow","Deny"],CHAN_ALLOW);
            }
            
            llResetTime();
            while( (llGetTime()<20.0) && (llLinksetDataRead("@_Allow")=="") ){llSleep(0.5);}
            pass = (integer)llLinksetDataRead("@_Allow");
            llLinksetDataDelete("@_Allow");
            if(!pass) return FALSE;
            
            llOwnerSay("Debug: Adding new restrictor at index "+(string)i+" "+(string)ID+" : "+llKey2Name(ID));
            gRestrictors = llListReplaceList(gRestrictors,[ID],i,i);
            return i+1;
        }
    }
    // we're full up. (could try a ping test here.)
    ping();
    return FALSE;
}
rem_restrictor(key ID,integer index,string cmd) // if not via a !release command, let cmd = "safeword" or "not-found"
{   integer mask = 1<<index;
    gRestrictors = llListReplaceList(gRestrictors,[""],index,index);
    // we could take this in chunks, but I think this script is low-memory enough in general to handle a large number of restrictions.
    list restrictions = llLinksetDataFindKeys("^@[^_]",0,0); // find all keys starting with "@" but not "@_"
    integer i = llGetListLength(restrictions);
    while(~--i)
    {   string prefix = llList2String(restrictions,i);
        apply_mask(prefix,mask,FALSE);
    }
    llSay(CHAN_RLV,cmd+","+(string)ID+",!release,ok"); // the spec says we have to say this even if there was no release request from the object.
}
rem_restrictors(list IDs,integer mask,string cmd) // more efficient for boot-up.
{   // expect calling location to clean up gRestrictors.
    integer i = llGetListLength(IDs);
    while(~--i)
    {   string restrictor = llList2String(IDs,i);
        if(restrictor) // empty slots don't respond to ping, so are set to be removed. we don't need to talk to "" though.
            llSay(CHAN_RLV,cmd+","+restrictor+",!release,ok");
    }
    list restrictions = llLinksetDataFindKeys("^@[^_]",0,0); // find all keys starting with "@" but not "@_"
    i = llGetListLength(restrictions);
    while(~--i)
    {   string prefix = llList2String(restrictions,i);
        apply_mask(prefix,mask,FALSE);
    }
}
parse_message(string msg, key ID)
{   list msg = llCSV2List(msg);
    if((key)llList2String(msg,1) != OWNER) return; // message was not for us.
    
    string cmd = llList2String(msg,0);
    //if(cmd=="ping") return; // don't listen to pings from relays (superfluous)
    list rlv_list = llParseString2List(llList2String(msg,2),PIPE,EMPTY_LIST);
    integer index_restrictor = check_restrictor(ID,cmd,rlv_list);
    if(index_restrictor==-1) return;
    if(index_restrictor)
    {   // check each restriction:
        integer i = llGetListLength(rlv_list);
        while(~--i)
        {   string rlv = llList2String(rlv_list,i);
            integer ind_eq = llSubStringIndex(rlv,"=");
            string prefix = llDeleteSubString(rlv,ind_eq,-1);
            string suffix =    llGetSubString(rlv,ind_eq,-1);
            if(check_rlv(rlv,suffix))
            {   if(llGetSubString(rlv,0,0)=="!")
                {   meta_command(rlv, cmd, ID, index_restrictor-1);
                }else
                {   llSay(CHAN_RLV,cmd+","+(string)ID+","+rlv+",ok");
                    llOwnerSay(rlv);
                    add_rlv(prefix,suffix,index_restrictor-1); 
                }
            }else
            {   //llSay(0,"Debug: "+rlv+" didn't pass check!");
                llSay(CHAN_RLV,cmd+","+(string)ID+","+rlv+",ko");
            }
        }
    }else
    {   // we don't like the restrictor, reply 'ko' to each rlv.
        llOwnerSay("Warning: "+llList2CSV(rlv_list)+" from a disliked restrictor!"); // mildly innefficient because of variable masking, oh well.
        integer i = llGetListLength(rlv_list);
        while(~--i)
        {   string rlv = llList2String(rlv_list,i);

            llSay(CHAN_RLV,cmd+","+(string)ID+","+rlv+",ko");
        }
    }
}

meta_command(string rlv, string cmd, key restrictor, integer index_res)
{   if("!release"==rlv)
    {   rem_restrictor(restrictor,index_res,cmd);
    }else if("!version"==rlv)
    {   llSay(CHAN_RLV,cmd+","+(string)restrictor+",!version,"+SPEC_VERSION);
    }else if("!pong"==rlv)
    {   if(gOpenPing)
        {   gMaskRestrictors = gMaskRestrictors | (1<<index_res);
            // we don't reply to !pong with an ok.
            //llSay(0,"Debug: Got pong from "+(string)index_res+" "+(string)restrictor);
        }else
        {   // it didn't respond promptly; tell it to release:
            llSay(CHAN_RLV,"ping,"+(string)restrictor+"!release,ok");
            //llSay(0,"Debug: The pong was too tardy!");
        }
    }else if("!implversion"==rlv)
    {   llSay(CHAN_RLV,cmd+","+(string)restrictor+",!implversion,"+IMPL_VERSION);
    }
}
add_rlv(string prefix, string suffix, integer index_restrictor)
{   if("=y"==suffix || "=rem"==suffix)
    {   apply_mask(prefix, (1<<index_restrictor),FALSE);
    }else if("=n"==suffix || "=add"==suffix)
    {   apply_mask(prefix, (1<<index_restrictor),TRUE);
    } // last 2 cases aren't strictly neccessary:
    else if("=force"==suffix || (integer)llDeleteSubString(suffix,0,0)) // 
    {   // do nothing, llOwnerSay was already executed.
    }else
    {   llSay(0,"Error! RLV with erroneous suffix: '"+prefix+suffix+"'");
    }
}
apply_mask(string rlv, integer mask, integer add)
{
    if(add)
    {   mask = mask | (integer)llLinksetDataRead(rlv);
    }else // remove
    {   mask = ~mask & (integer)llLinksetDataRead(rlv);
    }
    if(mask)
    {   llLinksetDataWrite(rlv,(string)mask);
        llOwnerSay(rlv+"=add");
    }else
    {   llLinksetDataDelete(rlv);
        llOwnerSay(rlv+"=rem");
    }
}

string mask_to_list(integer n)
{   string ret;
    integer i = MAX_RESTRICTORS;
    while(~--i)
    {   if(n&(1<<i))
        {   ret+=(string)i;
        }else
        {   ret+="_";
        }
    }
    return ret;
}

ping()
{   integer i = MAX_RESTRICTORS;
    while(~--i)
    {   key res = llList2Key(gRestrictors,i);
        if(res)
        {   llSay(CHAN_RLV,"ping,"+(string)res+",ping,ping");
            //llSay(0,"Debug: pinging "+(string)i+" "+(string)res);
            gOpenPing=TRUE;
            gMaskRestrictors =0;
        }
    }
    llSetTimerEvent(gOpenPing*PING_DELAY);
}

default
{
    state_entry()
    {   OWNER= llGetOwner();
        CHAN_ALLOW = llAbs((integer)("0x"+(string)OWNER));
        string s = llLinksetDataRead("@_SAFEWORD");
        if(s) SAFEWORD = s;
        // mildly innefficient, but it's done rarely:
        while(llGetListLength(gRestrictors)<MAX_RESTRICTORS) gRestrictors+="";
        
        llListen(CHAN_RLV,"","","");
        string DOM = llLinksetDataRead("@_DOM");
        if(DOM)
        {   list DOM = llCSV2List(DOM);
            integer i = llGetListLength(DOM);
            while(~--i)
            {   llListen(0,"",llList2String(DOM,i),SAFEWORD);
            }
        }
    }
    touch_start(integer n)
    {   llResetTime();
    }
    changed(integer c)
    {   if(c&CHANGED_TELEPORT)
        {   ping();
        }
        if(c&CHANGED_OWNER)
        {   llResetScript(); //only need to change CHAN_ALLOW, but in an abundance of caution.
        }
    }
    touch_end(integer n)
    {   //if(llDetectedTouchFace(0)!=2) return;
        if(llGetTime()>3.0)
        {   llOwnerSay("Attempting Safeword.");
            string DOM = llLinksetDataRead("@_DOM");
            integer allowSafeword= (integer)llLinksetDataRead("@_AloneSafewordAllowed");
            if(DOM)
            {   list DOM= llCSV2List(DOM);
                integer i = llGetListLength(DOM);
                while(~--i)
                {   string DOM = llList2String(DOM,i);
                    if(llVecDist(llGetPos(),llList2Vector(llGetObjectDetails(DOM,[OBJECT_POS]),0))<20.0)
                    {   allowSafeword=FALSE;
                        llDialog(DOM,"secondlife:///app/agent/"+(string)OWNER+"/inspect is attempting safeword. To let them, say: '"+SAFEWORD+"'.",[SAFEWORD,"ignore"],0);
                    }else
                    {   llInstantMessage(DOM,"secondlife:///app/agent/"+(string)OWNER+"/inspect is attempting safeword.");
                    }
                }
            }
            if(allowSafeword)
            {   llOwnerSay("Safeword Allowed.");
                rem_restrictors(gRestrictors,-1,"safeword");
                llOwnerSay("@detach=y,unsit=force");
            }else
            {   llOwnerSay("Safeword Denied.");
            }
        }else
        { // list restrictors and restrictions.
            integer i = MAX_RESTRICTORS;
            while(~--i)
            {   llOwnerSay("Restrictor "+(string)i+" is "+llKey2Name(llList2Key(gRestrictors,i)));
            }
            list restrictions = llLinksetDataFindKeys("^@[^_]",0,0); // starts with "@" as first character, but "_" is not the second character.
            i = llGetListLength(restrictions);
            while(~--i)
            {   string restriction = llList2String(restrictions,i);
                string by = mask_to_list((integer)llLinksetDataRead(restriction));
                llOwnerSay("> "+restriction+" is restricted by "+by+".");
            }
        }
    }
    attach(key ID)
    {   if(ID)
        {   OWNER = ID;
            llOwnerSay("@detach=n,touchme=add");
            ping();
        }
    }
    timer()
    {   llSetTimerEvent(0);
        gOpenPing=FALSE;
        list remove = gRestrictors;
        integer i = MAX_RESTRICTORS;
        while(~--i)
        {   if((1<<i)&gMaskRestrictors)
            {   remove = llDeleteSubList(remove,i,i);
            }else
            {   gRestrictors = llListReplaceList(gRestrictors,[""],i,i);
            }
        }
        rem_restrictors(remove,~gMaskRestrictors,"not-found");
    }
    listen(integer Channel, string Name, key ID, string Text)
    {   //llOwnerSay(Text);
        if(Text==SAFEWORD)
        {   llOwnerSay("Safeword Allowed.");
            rem_restrictors(gRestrictors,-1,"safeword");
            llOwnerSay("@detach=y,unsit=force");
        }
        parse_message(Text,ID);
    }
}

Complex and nitpicky, but it could be much worse. I have tested it a little bit and it should work in most situations, but YMMV.

It should be noted that "safewording" this relay is a bit idiosyncratic. to safeword, either touch and hold the relay for 5 seconds, or have a listed dominant (see config) say your safeword. after a successful safeword, the HUD will be detachable; detaching it removes any restrictions it may have added.

Edited by Quistess Alpha
  • Thanks 1
Link to comment
Share on other sites

  • 1 month later...

Oooh nice!

I'm also in the midst of writing my RLV Relay that leverages LSD for the all-important "restore restraint state upon login".

The main benefit of using LSD is that the Relay script can be "stateless" in a way, so one can regularly Reset it, upon which it'll retrieve (and validate) previous restraints from LSD on-the-fly.

 

  • Like 1
Link to comment
Share on other sites

1 hour ago, primerib1 said:

The main benefit of using LSD is that the Relay script can be "stateless" in a way, so one can regularly Reset it, upon which it'll retrieve (and validate) previous restraints from LSD on-the-fly.

While that is a side benefit, I'd argue the main selling point is that it's Strictly better than a global JSON string. Being able to associate restriction-> restrictor and being able to iterate over all restrictions, is more efficient than say, having an individual list for each restrictor, then needing to iterate over every other restrictor to check if that RLV was also applied by something else when removing a restriction.

I'll probably post an updated script after making some minor changes, and reasonable testing, but on my todo list:

  • @clear needs to translate to !release somewhere in the interpretation pipeline. I'm a bit torn whether to do it early or wait until right before it fails the 'has an = sign' check, or perhaps change the detection of  meta-commands to 'lacks a =' rather than 'starts with a !'.
  • Associate restriction messages with the root prim of their linkset. One object shouldn't be able to take up multiple slots.
  • More of a 'fun' change: add an 'AFK Vulnerable' mode which if active, accepts any request if there is no @_DOM around, and the wearer has the 'away' status on. (Yes I know 'away' can be turned off and the time is viewer-dependent, but for a voluntary gimmick it's more reasonable than constant dialog boxes)
  • Several debug/error messages are reported using llSay. This is useful for testing when I'm not the one wearing the relay, but I should probably add an option to use llOwnerSay instead to avoid spam.

 

Edited by Quistess Alpha
  • Like 1
Link to comment
Share on other sites

6 hours ago, Quistess Alpha said:

Several debug/error messages are reported using llSay. This is useful for testing when I'm not the one wearing the relay, but I should probably add an option to use llOwnerSay instead to avoid spam.

To prevent annoying spam, I think the messages should default to being said to wearer only using llOwnerSay, but the option (in a notecard, maybe?) can change that to use llSay.

  • Like 1
Link to comment
Share on other sites

You are about to reply to a thread that has been inactive for 434 days.

Please take a moment to consider if this thread is worth bumping.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...