Welcome to the Second Life Forums Archive

These forums are CLOSED. Please visit the new forums HERE

Scripting Restrained Life (an exploration)

Soen Eber
Registered User
Join date: 3 Aug 2006
Posts: 428
07-18-2009 15:17
No, I'm not a Restrained Life scripting guru. Just the opposite - I'm just starting out and recording what will hopefully be an ongoing series of notes and experiments. A week ago I knew nothing about Restrained Life other then what it could generally do. I did take a good look at the viewer source code a few months ago and I glanced at the API, but haven't used it in-world.

1. Getting the viewer: google found it at erestraint.com. Knowing the viewer was developed by Marine Kelley I checked her blog and found a corresponding link to erestraint.com so I had a good sense I could trust the code there. I downloaded and followed the instructions.

2. Getting the relay: I'm pretty sure open collar has a built in relay, but Mistress already had a collar on me so swapping out wasn't a real option. I instead googled for restrained life wiki (I knew there was a good wiki site even though the link from Marine Kelley's blog was broken). It took me to the API which wasn't quite what I was looking for, so I followed the navigation bar back to "Second Life Wiki > LSL Protocol" and clicked on the Restrained Life relay spec instead. A note there said the code had gotten somewhat outdated, and the link to a recommended relay implementation looked good but also pointed me to an in-world location to get it - only to discover the author had been banned. Not knowing how far out of date the relay had fallen I went to other implementations and eventually chose the Think Kink version based on their having both documentation and multiple coders on the implementation team (both very good signs).

I know the above isn't really scripting, but maybe I can save someone else the hour I spent on the above.

3. First bit of scripting - version checking. I wanted to start off with a simple version check, just to make sure everything was running smooth. Put the following into a newly minted prim and click to turn on/off (it wasn't smooth at first - I think I need to wear the relay, log off, and then log back in to hit its rez event or something...or I was just tired, which I was, and I missed something)

CODE

integer rlv_test;
integer ch_obj = 5;
string rlv_viewer;
string rlv_version;
integer rlv_ok = FALSE;

default
{
state_entry()
{
rlv_test = TRUE;
rlv_ok = FALSE;
llListen(ch_obj, "", NULL_KEY, "");
llOwnerSay("on");
llOwnerSay("Sending Version request to viewer");
llOwnerSay("@version="+(string)ch_obj);
llSetTimerEvent(5);
}
touch_start(integer total_number)
{
if (rlv_test == TRUE) {
llOwnerSay("off");
rlv_test = FALSE;
}
else {
llOwnerSay("on");
rlv_test = TRUE;
llSetTimerEvent(5);
llOwnerSay("Sending Version request to viewer");
llOwnerSay("@version="+(string)ch_obj);
}
}
listen(integer ch, string name, key id, string msg)
{
list l=llParseString2List(msg, [" "],[]);

llOwnerSay("heard "+msg);
if (llList2String(l,0)+" "+llList2String(l,1) == "RestrainedLife viewer") {
rlv_viewer = llList2String(l,0);
if (llList2String(l,2) == "v1.19") {
rlv_version = "v1.19";
rlv_ok = TRUE;
llOwnerSay("Version "+rlv_viewer+" Version "+(string)rlv_version);
llSetTimerEvent(0);
}
}
}
timer()
{
llOwnerSay("RLV Version check FAILED");
rlv_ok = FALSE;
llSetTimerEvent(0);
}
}


I also had a simple chat relay in a separate prim just to listen in:
CODE

integer ch_listen;

default
{
state_entry()
{
ch_listen = -1;
llOwnerSay("Say /1 ch # to listen to channel #");
llListen(1,"",NULL_KEY,"");
}
listen(integer ch, string name, key id, string msg)
{
if (ch == 1) {
list l = llParseString2List(msg,[" "],[]);
if (llList2String(l,0) == "ch") {
ch_listen = (integer)llList2String(l,1);
llListen(ch_listen, "", NULL_KEY, "");
llOwnerSay("Listening to channel "+(string)ch_listen);
}
}
else {
llOwnerSay((string)ch_listen + " (" + llKey2Name(id) + "): " + msg);
}
}
on_rez(integer start_parameter)
{
llResetScript();
}
}



...to be continued...feel free to comment
Soen Eber
Registered User
Join date: 3 Aug 2006
Posts: 428
07-18-2009 18:44
OK, now I can enable/disable the ability to remove an attachment. Put the following script in a new prim and wear it on your hand or other body part.

I'm trying to be good about paying attention to resources, clearing states when necessary, paying attention to stuff the API mentions needs to be paid attention to, etc, since the code will probably get copied, modded, and put into stuff for sale by other users - let me know if you spot anything amiss before someone puts 10,000 copies of my buggy code on the market, thanks.

CODE

integer rlv_test;
integer ch_obj = 5;
integer ch_handle;
string rlv_viewer;
string rlv_version;
integer rlv_ok = FALSE;
integer rez_utime;
integer vers_utime;

// We need to give up to 60 seconds for version checking on rez (as per the API) so we need to capture when
// this occurs so we don't time out prematurely. Since script reset would blow out the rez time we're saving
// move the state_entry code to an init function called in both state_entry and on_rez.

init()
{
rlv_ok = FALSE;
rlv_viewer = "UNKNOWN";
ch_handle = llListen(ch_obj, "", NULL_KEY, "");
vers_utime = llGetUnixTime();
llSetTimerEvent(1);
llOwnerSay("@version="+(string)ch_obj);
}
cleanup()
{
llSetTimerEvent(0);
llListenRemove(ch_handle);
}
on()
{
rlv_test = TRUE;
llOwnerSay("Enabling restriction on removing attachments");
llOwnerSay("@detach=n");
}
off()
{
rlv_test = FALSE;
llOwnerSay("Disabling restriction on removing attachments");
llOwnerSay("@detach=y");
cleanup();
}
default
{
state_entry()
{
init();
}
on_rez(integer start_param)
{
rez_utime = llGetUnixTime();
init();
}
attach(key id)
{
// cleanup when detached
if (id == NULL_KEY) {
llOwnerSay("Clearing Restrained Life Settings");
llOwnerSay("@clear");
}
else {
init();
}
}
touch_start(integer total_number)
{
if (rlv_test == TRUE) {
off();
}
else {
if (rlv_version == "") {
llOwnerSay("Restrained Life Viewer not Determined - retesting (please wait)");
init();
}
else if (rlv_version == "UNKNOWN") {
llOwnerSay("Please wait while obtaining Restrained Life Viewer");
}
else {
on();
}
}
}
listen(integer ch, string name, key id, string msg)
{
list l=llParseString2List(msg, [" "],[]);

llOwnerSay("heard "+msg);
if (llList2String(l,0)+" "+llList2String(l,1) == "RestrainedLife viewer") {
rlv_viewer = llList2String(l,0);
if (llList2String(l,2) == "v1.19") {
rlv_version = "v1.19";
rlv_ok = TRUE;
llOwnerSay("Version "+rlv_viewer+" Version "+(string)rlv_version);
llSetTimerEvent(0);
on();
}
}
cleanup();
}
timer()
{
// wait a minute post rez or 3 seconds after a version request before failing
if (
((llGetUnixTime() - rez_utime) > 60) &&
((llGetUnixTime() - vers_utime) > 3)
) {
llOwnerSay("RLV Version check FAILED");
rlv_version = "";
rlv_ok = FALSE;
cleanup();
}
}
}
Soen Eber
Registered User
Join date: 3 Aug 2006
Posts: 428
07-19-2009 07:09
Next project will be a force sit. What I've been doing so far can be done with attachments using llOwnerSay, but how could I get a friend to force sit in a chair I make? Obviously the chair can't llOwnerSay to him/her. Well the answer is there is a relay specification. The chair says something that the relay then picks up, and the relay (after suitable security checks) repeats what the chair says as llOwnerSay. I knew from earlier the relay was important so I made sure to pick one up -- now I have to get down and understand how it works.

From http://wiki.secondlife.com/wiki/LSL_Protocol there is a link to the LSL Protocol/Restrained Life Relay Specs, and another for the open relay group. I'll start with the former -- there is going to be some decent RTFM time devoted to both before proceeding with my force sit chair script. I will also need to look at the documentation that came with the relay as well.
Soen Eber
Registered User
Join date: 3 Aug 2006
Posts: 428
07-19-2009 18:48
Got it working.

There's some overhead that needs to be accounted for when working with the relay, and you really need to include it if you want to be a good citizen. Here's the code, and as usual just dump it into a freshly rezzed prim.

(note: the script is pretty "talky" from debugging)

CODE

integer rlv_test;
integer ch_rlv_relay = -1812221819;
integer hdl_rlv_relay;
integer trigger_utime;
key agent_key;
string relay_version;
string implementation;
string trigger_event;

init()
{
hdl_rlv_relay = llListen(ch_rlv_relay, "", NULL_KEY, "");
agent_key = NULL_KEY;
rlv_test = TRUE;
}
cleanup()
{
llSetTimerEvent(0);
}
on()
{
init();
llOwnerSay("Enabling");
}
off()
{
rlv_test = FALSE;
llOwnerSay("Disabling");
trigger("release");
}
trigger(string s_in)
{
if (agent_key == NULL_KEY) {
return;
}
trigger_event = s_in;
trigger_utime = llGetUnixTime();
string rlv_prefix = (string)llGetKey()+","+(string)agent_key+",";
llSetTimerEvent(1.0);

if (trigger_event == "version check") {
llSay(ch_rlv_relay,rlv_prefix+"!version");
}
if (trigger_event == "implementation") {
llSay(ch_rlv_relay,rlv_prefix+"!implversion");
}
if (trigger_event == "release") {
llSay(ch_rlv_relay,rlv_prefix+"!release");
}
if (trigger_event == "force sit") {
llSay(ch_rlv_relay,rlv_prefix+"@sit:"+(string)llGetKey()+"=force");
}
}
default
{
state_entry()
{
init();
}
on_rez(integer start_param)
{
init();
}
touch_start(integer total_number)
{
if (rlv_test == TRUE) {
off();
}
else {
on();
}
}
listen(integer ch, string name, key id, string msg)
{
list l=llParseString2List(msg, [","],[]);
string tgt = llList2String(l,0);
string cmd = llList2String(l,2);
string rsp = llList2String(l,3);

if (tgt == (string)llGetKey()) {
llOwnerSay("It is for me");
if (cmd == "!version") {
llOwnerSay("It is a response to !version");
relay_version = rsp;
llOwnerSay("version = "+rsp);
trigger("implementation");
}
// we don't really need this but its nice to have around
if (cmd == "!implversion") {
llOwnerSay("It is a response to !implversion");
implementation = rsp;
llOwnerSay("implementation = "+implementation);
trigger("force sit");
}
if (llGetSubString(cmd, 0, 4) == "@sit:") {
llOwnerSay("It is a response to @sit");
llOwnerSay("response is "+rsp);
cleanup();
}
if (cmd == "!release") {
llOwnerSay("It is a response to !release");
llOwnerSay(rsp);
llSetTimerEvent(0);
cleanup();
llListenRemove(ch_rlv_relay);
}
if (cmd == "ping") {
llSay(ch_rlv_relay, (string)llGetKey()+","+(string)agent_key+",!pong");
}
}
}
collision_start(integer num_detected)
{
if (rlv_test == TRUE) {
agent_key = llDetectedKey(0);
llOwnerSay("Detected "+llKey2Name(agent_key)+" ("+(string)agent_key+") on collision");
trigger("version check");
}
}
timer()
{
if ((llGetUnixTime() - trigger_utime) > 3) {
llOwnerSay("Trigger event "+trigger_event+" timed out");
cleanup();
}
}
}


It also needs a pose script in the prim.
Soen Eber
Registered User
Join date: 3 Aug 2006
Posts: 428
07-19-2009 18:49
Here's the chat spy I'm using to read relay messages:

CODE

default
{
state_entry()
{
llListen(-1812221819,"","","");
}

listen(integer channel, string name, key id, string message)
{
llSay(0,message);
integer i;
string words = "";
list lMsg = llParseString2List(message, [","], []);
for (i = 0; i < llGetListLength(lMsg); i++) {
string word = llList2String(lMsg,i);
string name = llKey2Name(word);
if (word == (string)NULL_KEY) name = "null_key";
if (name == "") words = words + word + " ";
else words = words + "("+name+") ";
}
llSay(0,llKey2Name(id)+" said: "+words);
}
}
Beverly Ultsch
Registered User
Join date: 6 Sep 2007
Posts: 229
07-20-2009 02:34
Just a quick tip here on version checking.

Don't rely on !version, that only returns the version of the RELAY, it does not confirm that they are using the restrained life viewer.

It's much safer to use @version and check the version of the viewer, if you get a response you know there using the viewer, you also know there is an active relay (of course there is nothing wrong in checking both).

This will save hours of confusion when you try and work out why the relay appears to happily carry out all the commands, but you see no results, only for you to discover they weren't running the viewer :)
Soen Eber
Registered User
Join date: 3 Aug 2006
Posts: 428
07-20-2009 22:52
I got the extra version checking in -- and thanks for the heads up :-) I will post the code once I've cleaned it up a bit. I'm a bit surprised that that sending @version to the relay only produces an ok and not the restrained life version (maybe I missed something so I'll look again in the morning, or maybe its a privacy thing - you can only get your own restrained life version #?). Also - what does "ko" signify? I couldn't find anything in the API that differentiated it from "ok"? That's pretty confusing.

This is going to be a fun project - I've put the forced sit script in a bouncing physics ball I created some time back and am launched it with the classic watermelon launcher. Its not capturing people I point it at though [well, I really mean when people point it at me] because the balls bounce and fly past out of range before the commo handshake wraps up. I'll need to put a llMove2Target or something in the collision event I guess. Also these things are temp rez so I'll have to figure out if the detach event fires off on that so I can send a !clear message. Garbage collection seems pretty slow but I guess its not expecting 30 some bouncing balls rezzing and derezzing all over the place.

Still puzzling over the "ko"...
Beverly Ultsch
Registered User
Join date: 6 Sep 2007
Posts: 229
07-21-2009 03:56
They key to understanding this is to remember that communications using a relay are one way.

In other words the in world object talks to the relay and the relay talks to the viewer, there is no communication from the viewer to the relay.

Commands to the relay take one of two forms, those staring with a “!” and those starting with a “@”.

Commands starting with “!”, eg !version, !clear are commands to the RELAY ONLY and are not passed on to the viewer.

Commands starting with “@” indicate that the relay should pass the command on to the viewer, these commands have two formats.

Commands that do not require a response from the viewer eg @unsit=n (to prevent standing)

Commands that do require a response fro m the viewer eg @version=< channel> (where channel is a positive integer) In this case the viewer will respond on channel with the response.

So if we consider the case of checking versions.

The in world object sends “!version” to the relay - this is a command to the relay and the relay responds with its own version, the command does not get passed to the viewer.

The in world object sends “@version=555” to the relay – this is a command to the viewer so the relay sends the command to the viewer. The viewer sends it’s response on channel 555, the in world object needs to be listening on this channel to receive the response, it is not passed back via the relay.

With regards to “ok” and “ko”

The relay does some checking on all commands before carrying them out, a response of “ok” indicates that it was a valid command, a response of “ko” indicates an invalid command (or a command that was refused by the relay).

So “@version” will produce “ok” from the relay, “ #version” (which is not a valid command) will produce” ko” and the command will not be executed.

What’s considered valid and invalid may be different depending on the relay implementation in use.

I'm not sure if it will make any difference to your project, but since version 1.015 of the relay, the spec changed to allow a llShout to the relay instead of an llSay, this may just give the time required for the coms to work for you.

Another thing to look at is that the spec allows for lists of commands, so typically in your situation you would send "@sit:< UUID>= force|@unsit =n", to first do the force sit and then prevent standing.

I hope this clarifies things a bit and hasn't further added to the confusion
Soen Eber
Registered User
Join date: 3 Aug 2006
Posts: 428
07-22-2009 19:02
Yes, that was a great help, thankyou :-)

Pretty sure this is final form now - I'm only doing the force sit for now since any collision reapplies the force sit and its fun to watch the sub struggle to get up :-) Also I'm pretty much focused on the surrounding architecture for all this. I think I have a good base now for future development here and I think I'm crossing all the t's here...

I got the ping/pong working, but I'm not seeing the relay reapply the force sit. The collision event kicks off as soon as I log in so the force sit comes from that, maybe its overriding. I'll have to find a way to test for that with a non-collision-based sit.

Some general notes, not related (for others getting comfy with restrained life). The Think Geek relay updated and there was some good info in their release notes:

"We highly recommend Boy Lane's version of the Cool Viewer"
"We also give HIGH Marks to the GreenLife Emerald Viewer ... for it's exceptional features and it's implementation of Kitty Barnett's RLVa"

RLVa? What's that? Some googling turned up the blog for it at

So some other choices to look into. Also, one of the reasons I felt comfortable with restrained life scripting now was the release of the snow globe viewer. All the restrained life compatible viewers (that I'm aware of) make changes to the core SL client, so it was good to have an alternate official viewer as a backup.

More general notes: COOLNESS! I just discovered the RestrainedLife debug settings under the advanced menu. You can toggle RestrainedLife features on/off (needs a restart), show Restrained Life debug messages, and disallow anything messing with with your environment settings. I guess some objects put you into something super-blurry/pixelated and I can see the utility if you're just checking out various gadgets at a store or something.

Anyways, the code:

CODE

integer script_active;
integer handshake_complete = FALSE;

integer ch_relay = -1812221819;
integer lc_relay;
integer ch_object;
integer lc_object;

string viewer_name;
float viewer_version;
string relay_protocol;
string relay_implementation;

integer trigger_utime;
key agent_key;
string trigger_event;

cleanup()
{
llSetTimerEvent(0);
llListenRemove(lc_relay);
llListenRemove(lc_object);
}
on()
{
lc_relay = llListen(ch_relay, "", NULL_KEY, "");
agent_key = NULL_KEY;
script_active = TRUE;
llOwnerSay("Enabling");
}
off()
{
script_active = FALSE;
llOwnerSay("Disabling");
cleanup(); // not going to listen for release ack since avatar may have logged
trigger("release");
}
integer is_avatar(key id)
{
// hacky test if key is an avatar and in the sim
if (llGetAgentSize(id) == ZERO_VECTOR) return FALSE;
else return TRUE;
}
relay_msg(string cmd_label, string cmd_list)
{
llSay(
ch_relay,
(string)llGetKey()+":"+cmd_label+","
+(string)agent_key+","
+cmd_list
);
}
trigger(string s_in)
{
if (agent_key == NULL_KEY) {
return;
}
trigger_event = s_in;
trigger_utime = llGetUnixTime();
llSetTimerEvent(1.0);

// Relay and Viewer Handshakes
if (trigger_event == "relay version") {
relay_msg("", "!version");
}
if (trigger_event == "relay implementation") {
relay_msg("", "!implversion");
}
if (trigger_event == "viewer version") {
lc_object = llListen(ch_object, "", NULL_KEY, "");
relay_msg("", "@version="+(string)ch_object);
}

// Relay Release
if (trigger_event == "release") {
relay_msg("", "!release");
}

// Actions
if (trigger_event == "force sit") {
relay_msg("force sit", "@sit:"+(string)llGetKey()+"=force");
}
}
default
{
state_entry()
{
// Establish a sufficiently unique channel # based on our object key
ch_object = (integer)("0x"+llGetSubString((string)llGetKey(),-8,-2))*10+0;
on();
}
on_rez(integer start_param)
{
on();
}
touch_end(integer total_number)
{
// multiple people may be touching the object - loop through and look for owner
integer i;
for (i=0; i < total_number; i++) {
if (is_avatar(llDetectedKey(i))) {
if (llDetectedKey(i) == llGetOwner()) {
if (script_active == TRUE) {
off();
}
else {
on();
}
}
}
}
}
listen(integer ch, string name, key id, string msg)
{
if (ch == ch_object) {
list l=llParseString2List(msg, [" "],[]);

// Viewer Handshake
if (llList2String(l,0) == "RestrainedLife") {
viewer_name = llList2String(l,0)+" "+llList2String(l,1);
viewer_version = (float)llGetSubString(llList2String(l,2),1,-1);
handshake_complete = TRUE;
trigger("force sit");
}
}

else if (ch == ch_relay) {
list l=llParseString2List(msg, [","],[]);

// Handle relay ping requests
if (llList2String(l,0) == "ping") {
llSay(ch_relay, (string)llGetKey()+","+(string)agent_key+",!pong");
}

// Handle relay responses
else {

// Implement relay "command" as a two part field with owner key and command label
// This helps tie relay responses to our actual object
key obj_key = llList2Key(llParseString2List(llList2String(l,0),[":"],[]),0);
string obj_cmd_label = llList2String(llParseString2List(llList2String(l,0),[":"],[]),1);
string cmd = llList2String(l,2);
string rsp = llList2String(l,3);

// constrain to our messages only
if (obj_key == llGetKey()) {
if (rsp == "ko") {
llOwnerSay("*** Relay did not recognize message or had a problem with it");
llOwnerSay("*** "+msg);
}
else {
// Catch Handshake messages
if (cmd == "!version") {
relay_protocol = rsp;
trigger("relay implementation");
}
if (cmd == "!implversion") {
relay_implementation = rsp;
trigger("viewer version");
}
// Force sit action has been acknowledged - cleanup
if (obj_cmd_label == "force sit") {
llSetTimerEvent(0);
llListenRemove(lc_object);
}
}
}
}
}
}
collision_start(integer num_detected)
{
integer i;

if (script_active == TRUE) {
for (i=0; i < num_detected; i++) {
if (is_avatar(llDetectedKey(i))) {

// An avatar has bumped into us, so start the process for forcing them to sit
// We start with a handshake process with the avatar's relay and viewer to determine if
// they are actively running restrained life, and to get protocol and version information
agent_key = llDetectedKey(i);
handshake_complete = FALSE;
trigger("relay version");
}
}
}
}
timer()
{
// 3 second timeout
if ((llGetUnixTime() - trigger_utime) > 3) {
llSetTimerEvent(0);
llListenRemove(lc_object);
}
}
}