Table of Contents

Change Log

30 September 2013

  • Removed restart on region change.

17 August 2013

  • Thanks to TO, a bug on newer viewers has been fixed: it seems that firestorm has 42 attachment points, and that RLV has only 41 of them documented.

11 August 2013

  • Fixed Masters notecard handling and FFA toggle thanks to a bug report from TO.
  • Bumped minor version to .1

7 August 2013

  • Fixed trap handling code - the relay !release command should now work properly (master object was not cleared after a !release meta).
  • Renamed _relayMaster to _relayObject since it holds the key of an object, not an avatar.

14 July 2013

  • Changed the chat trigger to the first two letters of the avatar name.
  • Decorated using UTF8 symbols.
  • Code maintenance and bug fixes.

13 July 2013

  • Added RLV Trap relay.
  • Added panic button.
  • Added local chat menu trigger ( /1q ), thank you Allison Lynagh.
  • More code maintenance.

12 July 2013

12 April 2012

3 June 2012

llTakeControls(0, FALSE, FALSE);

should be able to properly lock an avatar down.

4 March 2012

  • Bugfixes.
  • Added leash and lock options.

Motivation

This is a typical RLV collar that does not use off-world databases and allows the manipulation of clothes, sitting, unsitting, moving the avatar around, locking avatars into place, releasing, leashing avatars to objects or other avatars.

Features

Setup

By default, the collar reacts only to the owner if RLV is turned on. There is also a switch accessible to the one controlling as well as the owner called FFA which enables anybody to control the wearer of the collar. By default FFA is turned off and just the wearer. Additionally, a notecard named Masters placed alongside the script into the same primitive allows a key-based list of avatars to be specified that will become permanent masters (until they are manually removed from the notecard).

29586384-8b41-4136-9591-5afd84233235
d5b7b715-f179-4f22-bad8-04f84335fedf

remember to leave a blank line at the end of the notecard.

Click the collar to trigger the menu or type /1q on local chat (and press enter) in order to open the menu.

Code

quickcollar.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2013 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string var, string kvp) {
    list dVars = llParseString2List(kvp, ["&"], []);
    do {
        list data = llParseString2List(llList2String(dVars, 0), ["="], []);
        string k = llList2String(data, 0);
        if(k != var) jump continue;
        return llList2String(data, 1);
@continue;
        dVars = llDeleteSubList(dVars, 0, 0);
    } while(llGetListLength(dVars));
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueSet(string var, string val, string kvp) {
    list dVars = llParseString2List(kvp, ["&"], []);
    if(llGetListLength(dVars) == 0) return var + "=" + val;
    list result = [];
    do {
        list data = llParseString2List(llList2String(dVars, 0), ["="], []);
        string k = llList2String(data, 0);
        if(k == "") jump continue;
        if(k == var && val == "") jump continue;
        if(k == var) {
            result += k + "=" + val;
            val = "";
            jump continue;
        }
        string v = llList2String(data, 1);
        if(v == "") jump continue;
        result += k + "=" + v;
@continue;
        dVars = llDeleteSubList(dVars, 0, 0);
    } while(llGetListLength(dVars));
    if(val != "") result += var + "=" + val;
    return llDumpList2String(result, "&");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasMenuIndex = 0;
list wasEndlessMenu(list input, list actions, string direction) {
    integer cut = 11-wasListCountExclude(actions, [""]);
    if(direction == ">" &&  (wasMenuIndex+1)*cut+wasMenuIndex+1 < llGetListLength(input)) {
        ++wasMenuIndex;
        jump slice;
    }
    if(direction == "<" && wasMenuIndex-1 >= 0) {
        --wasMenuIndex;
        jump slice;
    }
@slice;
    integer multiple = wasMenuIndex*cut;
    input = llList2List(input, multiple+wasMenuIndex, multiple+cut+wasMenuIndex);
    input = wasListMerge(input, actions, "");
    return input;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasListCountExclude(list input, list exclude) {
    if(llGetListLength(input) == 0) return 0;
    if(llListFindList(exclude, (list)llList2String(input, 0)) == -1) 
        return 1 + wasListCountExclude(llDeleteSubList(input, 0, 0), exclude);
    return wasListCountExclude(llDeleteSubList(input, 0, 0), exclude);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasListMerge(list l, list m, string merge) {
    if(llGetListLength(l) == 0 && llGetListLength(m) == 0) return [];
    string a = llList2String(m, 0);
    if(a != merge) return [ a ] + wasListMerge(l, llDeleteSubList(m, 0, 0), merge);
    return [ llList2String(l, 0) ] + wasListMerge(llDeleteSubList(l, 0, 0), llDeleteSubList(m, 0, 0), merge);
}
 
//pragma inline
moveTo(vector target, integer bb) {
    llStopMoveToTarget();
    llTargetRemove(_aMoveTarget);
    vector pointTo = target - llGetPos();
    llOwnerSay("@setrot:" + (string)llAtan2(pointTo.x, pointTo.y) + "=force");
    _aMoveTarget = llTarget(target, 2 + bb);
    llMoveToTarget(target, 2);
}
 
//pragma inline
stopMove() {
    llSensorRemove();
    llStopMoveToTarget();
    llTargetRemove(_aMoveTarget);
    _leashedKey = NULL_KEY;
    llParticleSystem([]);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
//      Original by: By Marine Kelley, Maike Short and Felis Darwin      //
///////////////////////////////////////////////////////////////////////////
wasTrapCommand(key id, string message) {
    list tokens = llParseString2List (message, [","], []);
    if (llGetListLength (tokens) != 3) return;
    string cmd_id = llList2String(tokens, 0);
    if (llList2Key(tokens, 1) != llGetOwner ()) return;
    list commands = llParseString2List(llList2String(tokens, 2), ["|"], []);
    do {
        string command = llList2String(commands, 0);
 
        // relay metacommands
        if(command == "!release") {
            do {
                string restriction = llList2String(_relayRestrictions, 0);
                if(llGetSubString(restriction, 0, 0) != "@") jump next;
                llOwnerSay(restriction + "=y");
@next;
                _relayRestrictions = llDeleteSubList(_relayRestrictions, 0, 0);
            } while(llGetListLength(_relayRestrictions) != 0);
            llShout(-1812221819, cmd_id + "," + (string)id + "," + command + "," + "ok");
            return;
        }
 
        if (command == "!version") {
            llShout(-1812221819, cmd_id + "," + (string)id + "," + command + "," + "1100");
            jump continue;
        }
 
        // standard commands
        if (llGetSubString(command, 0, 0) != "@") jump continue;
 
        list commandTokens = llParseString2List (command, ["="], [""]);
        string behav = llList2String(commandTokens, 0);
        string param = llList2String(commandTokens, 1);
        integer idx = llListFindList(_relayRestrictions, [behav]);
 
        if ((param == "n" || param == "add") && idx == -1) {
            llOwnerSay(behav + "=n");
            _relayRestrictions += [behav];
            jump acknowledge;
        }
        if ((param == "y" || param == "rem") && idx != -1) {
            llOwnerSay(llList2String(_relayRestrictions, idx) + "=y");
            _relayRestrictions = llDeleteSubList(_relayRestrictions, idx, idx);
            jump acknowledge;
        }
        llOwnerSay(command);
@acknowledge;
        llShout(-1812221819, cmd_id + "," + (string)id + "," + command + "," + "ok");
@continue;
        commands = llDeleteSubList(commands, 0, 0);
    } while(llGetListLength(commands) != 0);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
LID(key id) {
    if(id != NULL_KEY && llGetAttached() != 0) {
        llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("login", (string)((integer)wasKeyValueGet("login", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))+1), llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
        llResetScript();
    }
    llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("login", "-1", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
}
 
// General variables
list _menuItems = [];
list _objectKeys = [];
list _mList = [];
key _aMaster = NULL_KEY;
integer _aMoveTarget = 0;
string _SCAN = "";
string _FFA = "";
string _LOCK = "";
string _RELAY = "";
key _leashedKey = NULL_KEY;
integer _comChannel = 0;
integer _comHandle = 0;
 
// RLV relay variables
integer _relayHandle = 0;
key _relayObject = NULL_KEY;
string _relayCommand = "";
integer _relayTmpHandle = 0;
integer _relayConfirmed = FALSE;
list _relayRestrictions = [];
 
default {
    state_entry() {
        // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
        // if LID™, then login = 1
        // else login = 0 - script running | parameter undefined
        if((integer)wasKeyValueGet("login", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0)) > 0) {
            llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("login", "0", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
            llSetTimerEvent(25);
            return;
        }
        llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("login", "0", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
        state check;
    }
 
    timer() { state check; }
 
    changed(integer change) {
        if(change & CHANGED_INVENTORY || change & CHANGED_OWNER) {
            llResetScript();
        }
    }
 
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
 
}
 
state check {
    state_entry() {
        llSetTimerEvent(5);
        _comChannel = 10+(integer)llFrand(10);
        llListen(_comChannel, "", llGetOwner(), "");
        llOwnerSay("@version=" + (string)_comChannel);
    }
 
    listen(integer channel, string name, key id, string message) {
        llSetTimerEvent(0);
@setup;
        if(wasKeyValueGet("lock", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0)) == "on") {
            llOwnerSay("@detach=n");
            _LOCK = "LOCK: ON";
            jump setffa;
        }
        llOwnerSay("@detach=y");
        _LOCK = "LOCK: OFF";
        llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("lock", "off", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
@setffa;
        if(wasKeyValueGet("ffa", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0)) == "on") {
            _FFA = "FFA: ON";
            jump setrelay;
        }
        _FFA = "FFA: OFF";
        llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("ffa", "OFF", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
@setrelay;
        if(wasKeyValueGet("relay", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0)) == "on") {
            _RELAY = "RELAY: ON";
            jump donesettings;
        }
        _RELAY = "RELAY: OFF";
        llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("relay", "off", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
@donesettings;
        _mList = [];
        integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
        do {
            if(llGetInventoryName(INVENTORY_NOTECARD, i) == "Masters") jump foundmasters;
        } while(--i>-1);
        state main;
@foundmasters;
        _comHandle = 0;
        _aMaster = llGetNotecardLine("Masters", _comHandle);
    }
 
    dataserver(key query_id, string data) {
        if(query_id != _aMaster) return;
        if(data == EOF) {
            _comHandle = 0;
            state main;
        }
        if(data == "") jump continue;
        _mList += [llStringTrim(data, STRING_TRIM)];
@continue;
        _aMaster = llGetNotecardLine("Masters", ++_comHandle);
    }
 
    timer() {
        llSetTimerEvent(0);
        llOwnerSay("[QuickCollar℠]: Your viewer is not RLV-enabled. This gizmo requires a RLV-enabled viewer. Cannot proceed.");
    }
 
    changed(integer change) {
        if(change & CHANGED_INVENTORY || change & CHANGED_OWNER) {
            llResetScript();
        }
    }
 
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
 
}
 
state main {
 
    state_entry() {
        llListen(1, "", "", llGetSubString(llToLower(llKey2Name(llGetOwner())), 0, 1));
        if(_SCAN == "wearables") {
            _comHandle = llListen(_comChannel+1, "", _aMaster, "");
            return;
        }
        if(_SCAN == "attachments") {
            _comHandle = llListen(_comChannel+2, "", _aMaster, "");
            return;
        }
        if(_RELAY == "RELAY: ON") {
            _relayHandle = llListen(-1812221819, "", "", "");
        }
    }
 
    sensor(integer num) {
        llSetTimerEvent(60);
        _menuItems = [];
        _objectKeys = [];
        --num;
        do {
            _menuItems += llGetSubString(llDetectedName(num), 0, 23);
            _objectKeys += llDetectedKey(num);
        } while(--num>-1);
        if(llGetListLength(_menuItems) == 0) {
            llInstantMessage(_aMaster, "[QuickCollar℠]: Sorry, no objects or avatars detected...");
            return;
        }
        if(_SCAN == "sit") {
            _comHandle = llListen(_comChannel+3, "", _aMaster, "");
            llDialog(_aMaster, "Please select an object to sit the wearer on:\n", wasEndlessMenu(_menuItems, ["⏏ Exit", "⟻ Back", "Next ⟼"], ""), _comChannel+3);
            return;
        }
 
        if(_SCAN == "go") {
            _comHandle = llListen(_comChannel+4, "", _aMaster, "");
            llDialog(_aMaster, "Please select the destination to send the wearer to:\n", wasEndlessMenu(_menuItems, ["⏏ Exit", "⟻ Back", "Next ⟼"], ""), _comChannel+4);
            return;
        }
 
        if(_SCAN == "leash") {
            _comHandle = llListen(_comChannel+5, "", _aMaster, "");
            llDialog(_aMaster, "Please select the object or avatar to leash the wearer to:\n", wasEndlessMenu(_menuItems, ["⏏ Exit", "⟻ Back", "Next ⟼"], ""), _comChannel+5);
            return;
        }
    }
 
    at_target( integer tnum, vector targetpos, vector ourpos ) {
        if(tnum != _aMoveTarget) return;
        llStopMoveToTarget();
        vector pointTo = targetpos - llGetPos();
        llOwnerSay("@setrot:" + (string)llAtan2(pointTo.x, pointTo.y) + "=force");
        llTargetRemove(_aMoveTarget);
    }
 
    run_time_permissions(integer perm) {
        if (perm & PERMISSION_TAKE_CONTROLS) {
            llTakeControls(0, FALSE, FALSE);
            llOwnerSay("[QuickCollar℠]: " + llKey2Name(_aMaster) + " has made you stay.");
        }
    }
 
    listen(integer channel, string name, key id, string message) {
        llSetTimerEvent(60);
        if(message == llGetSubString(llToLower(llKey2Name(llGetOwner())), 0, 1) && ((id != llGetOwner() && _FFA == "FFA: ON") || id == llGetOwner() || llListFindList(_mList, (list)((string)id)) != -1)) {
            _aMaster = id;
            jump remenu;
        }
 
        if(message == "⏏ Exit") jump remenu;
 
        if(message == "⟻ Back") {
            llDialog(_aMaster, "Please browse the available items:\n", wasEndlessMenu(_menuItems, ["⏏ Exit", "⟻ Back", "Next ⟼"], "<"), channel);
            return;
        }
 
        if(message == "Next ⟼") {
            llDialog(_aMaster, "Please browse the available items:\n", wasEndlessMenu(_menuItems, ["⏏ Exit", "⟻ Back", "Next ⟼"], ">"), channel);
            return;
        }
 
        if(message == "FFA: OFF" || message == "FFA: ON") {
            if(_FFA == "FFA: OFF") {
                _FFA = "FFA: ON";
                llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("ffa", "on", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
                jump remenu;
            }
            _FFA = "FFA: OFF";
            llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("ffa", "off", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
            jump remenu;
        }
 
        if(message == "LOCK: OFF" || message == "LOCK: ON") {
            if(_LOCK == "LOCK: OFF") {
                llOwnerSay("@detach=n");
                _LOCK = "LOCK: ON";
                llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("lock", "on", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
                jump remenu;
            }
            llOwnerSay("@detach=y");
            _LOCK = "LOCK: OFF";
            llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("lock", "off", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
            jump remenu;
        }
 
        if(message == "RELAY: OFF" || message == "RELAY: ON") {
            _relayConfirmed = FALSE;
            llListenRemove(_relayHandle);
            if(_RELAY == "RELAY: OFF") {
                _relayHandle = llListen(-1812221819, "", "", "");
                _RELAY = "RELAY: ON";
                llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("relay", "on", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
                jump remenu;
            }
            _RELAY = "RELAY: OFF";
            _relayObject = NULL_KEY;
            do {
                string restriction = llList2String(_relayRestrictions, 0);
                if(llGetSubString(restriction, 0, 0) != "@") jump next;
                llOwnerSay(restriction + "=y");
@next;
                _relayRestrictions = llDeleteSubList(_relayRestrictions, 0, 0);
            } while(llGetListLength(_relayRestrictions));    
            llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("relay", "off", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
            jump remenu;
        }
 
        if(message == "⚐ PANIC") {
            llOwnerSay("@clear");
            llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("ffa", "off", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
            llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("lock", "off", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
            llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("relay", "off", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
            llResetScript();
        }
 
        if(message == "[ ︻ Sits ]" ) {
            llDialog(_aMaster, "Please select whether to sit or unsit:\n", ["⏏ Exit", "︻ Sit", "┯ Stand"], _comChannel);
            return;
        }
 
        if(message == "┯ Stand") {
            llOwnerSay("@unsit=force");
            jump remenu;
        }
 
        if(message == "︻ Sit") {
            _SCAN = "sit";
            llInstantMessage(_aMaster, "[QuickCollar℠]: Scanning for nearby objects, please wait...");
            llSensor("", "", (PASSIVE | ACTIVE), 10, PI);
            return;        
        }
 
        if(message == "[ ✣ Moves ]") {
            llDialog(_aMaster, "Please select the desired movement:\n", ["⏏ Exit", "⌲ Go To", "⍊ Stay", "☼ Free"], _comChannel);
            return;
        }
 
        if(message == "☼ Free") {
            llReleaseControls();
            llOwnerSay("[QuickCollar℠]: " + llKey2Name(id) + " has allowed you to move freely...");
            jump remenu;
        }
 
        if(message == "⍊ Stay") {
            llRequestPermissions(llGetOwner(), PERMISSION_TAKE_CONTROLS);
            jump remenu;
        }
 
        if(message == "⌲ Go To") {
            _SCAN = "go";
            llInstantMessage(_aMaster, "[QuickCollar℠]: Scanning for nearby targets, please wait...");
            llSensor("", "", (AGENT | PASSIVE | ACTIVE), 10, PI);
            return;
        }
 
        if(message == "[ ❖ Clothes ]" ) {
            llDialog(_aMaster, "Please select the type of clothing to remove:\n", ["⏏ Exit", "▦ Worn", "▣ Attached"], _comChannel);
            return;
        }
 
        if(message == "▦ Worn") state show_wearables;
 
        if(message == "▣ Attached") state show_attachments;
 
        if(message == "[ ⚉ Hide ]" ) {
            llDialog(_aMaster, "Please select whether to hide or show the collar:\n", ["⏏ Exit", "⚉ Hide", "⚇ Show"], _comChannel);
            return;
        }
 
        if(message == "⚉ Hide") {
            llSetLinkAlpha(LINK_SET, .0, ALL_SIDES);
            jump remenu;
        }
 
        if(message == "⚇ Show") {
            llSetLinkAlpha(LINK_SET, 1, ALL_SIDES);
            jump remenu;
        }
 
        if(message == "[ 乀 Leash ]" ) {
            llDialog(_aMaster, "Please select whether to leash or unleash the wearer:\n", ["⏏ Exit", "乀 Leash", "✂ Release"], _comChannel);
            return;
        }
 
        if(message == "乀 Leash" ) {
            _SCAN = "leash";
            llInstantMessage(_aMaster, "[QuickCollar℠]: Scanning for nearby objects, please wait...");
            llSensor("", "", (AGENT | PASSIVE | ACTIVE), 10, PI);
            return;
        }
 
        if(message == "✂ Release" ) {
            llSensorRemove();
            llStopMoveToTarget();
            llTargetRemove(_aMoveTarget);
            _leashedKey = NULL_KEY;
            llParticleSystem([]);
            jump remenu;   
        }
 
        if(channel == -1812221819 && _RELAY == "RELAY: ON" && _relayConfirmed == TRUE && id == _relayObject) {
            list tokens = llParseString2List(message, [","], []);
            if (llList2String(tokens, 1) != llGetOwner()) return;
            if (llGetListLength(tokens) != 3) return;
            wasTrapCommand(id, message);
            if(llList2String(tokens, 2) == "!release") {
                _relayObject = NULL_KEY;
                _relayConfirmed = FALSE;
            }
            return;
        }
 
        if(channel == -1812221819 && _RELAY == "RELAY: ON" && _relayConfirmed == FALSE && _relayObject == NULL_KEY) {
            list tokens = llParseString2List(message, [","], []);
            if(llList2String(tokens, 1) != llGetOwner()) return;
            if(llGetListLength(tokens) != 3) return;
            string meta = llList2String(tokens, 2);
            if(meta == "!version" || meta == "!release") {
                wasTrapCommand(id, message);
                return;
            }
            _relayObject = id;
            _relayCommand = message;
            _relayTmpHandle = llListen(_comChannel+6, "", llGetOwner(), "");
            llDialog(llGetOwner(), name + " would like to control your viewer. Do you want to allow it?", ["Yes", "No"], _comChannel+6);
            return;    
        }
 
        if(channel == _comChannel + 6) {
            if(message == "Yes") {
                _relayConfirmed = TRUE;
                wasTrapCommand(_relayObject, _relayCommand);
                return;
            }
            _relayConfirmed = FALSE;
            _relayObject = NULL_KEY;
            llListenRemove(_relayTmpHandle);
            return;
        }
 
        if(channel == _comChannel + 1) {
            llOwnerSay("@remoutfit:" + message + "=force");
            state show_wearables;
        }
 
        if(channel == _comChannel + 2) {
            llOwnerSay("@detach:" + message + "=force");
            state show_attachments;
        }
 
        if(channel == _comChannel + 3) {
            do {
                if(llSubStringIndex(llList2String(_menuItems, 0), message) != -1) jump rlvsit;
                _menuItems = llDeleteSubList(_menuItems, 0, 0);
                _objectKeys = llDeleteSubList(_objectKeys, 0, 0);
            } while(llGetListLength(_menuItems));
            jump remenu;
@rlvsit;
            llOwnerSay("@sit:" + llList2String(_objectKeys, 0) + "=force");
            _menuItems = [];
            _objectKeys = [];
            jump remenu;
        }
 
        if(channel == _comChannel + 4) {
            do {
                if(llSubStringIndex(llList2String(_menuItems, 0), message) != -1) jump rlvmove;
                _menuItems = llDeleteSubList(_menuItems, 0, 0);
                _objectKeys = llDeleteSubList(_objectKeys, 0, 0);
            } while(llGetListLength(_menuItems));
            jump remenu;
@rlvmove;
            llParticleSystem([]);
            llSensorRemove();
            _leashedKey = NULL_KEY;
            moveTo(llList2Vector(llGetObjectDetails(llList2String(_objectKeys, 0), [OBJECT_POS]), 0), (integer)(llVecDist(llList2Vector(llGetBoundingBox(llList2String(_objectKeys, 0)), 1), llList2Vector(llGetBoundingBox(llList2String(_objectKeys, 0)), 0))/2.0));
            _menuItems = [];
            _objectKeys = [];
            jump remenu;
        }
 
        if(channel == _comChannel + 5) {
            do {
                if(llSubStringIndex(llList2String(_menuItems, 0), message) != -1) jump rlvleash;
                _menuItems = llDeleteSubList(_menuItems, 0, 0);
                _objectKeys = llDeleteSubList(_objectKeys, 0, 0);
            } while(llGetListLength(_menuItems));
            jump remenu;
@rlvleash;
            _leashedKey = llList2Key(_objectKeys, 0);
            llSensorRepeat("", NULL_KEY, AGENT, .1, 0, 1.2-llGetRegionTimeDilation());
            llParticleSystem([12,"0560a0ad-ee6e-9a87-6a27-9322bad689ce",5,<3.0e-2,3.0e-2,3.0e-2>,6,<5.0e-2,5.0e-2,5.0e-2>,1,<1,1,1>,3,<1,1,1>,2,1,4,1,15,10,13,1e-3,7,10,9,1,8,<0,0,-.1>,0,355,20,_leashedKey]);
            _menuItems = [];
            _objectKeys = [];
            jump remenu;
        }
        return;
@remenu;
        llListenRemove(_comHandle);
        _comChannel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        _comHandle = llListen(_comChannel, "", _aMaster, "");
        llDialog(_aMaster, "\n            Welcome to the Quick Collar℠.\nCreated in 2013 by Wizardry and Steamworks\n                 15 July 2013: Version: 1.1\n", [_FFA, _LOCK, _RELAY, "[ ❖ Clothes ]", "⚐ PANIC", "[ ︻ Sits ]", "[ ✣ Moves ]", "[ 乀 Leash ]", "[ ⚉ Hide ]"], _comChannel);
 
    }
 
    no_sensor() {
        if(llStringLength(llKey2Name(_leashedKey)) == 0) jump awol;
        vector target = llList2Vector(llGetObjectDetails(_leashedKey, [OBJECT_POS]), 0);
        integer distance = (integer)llVecDist(target, llGetPos());
        if(distance < 5) return;
        if(distance >= 5 && distance < 65) {
            moveTo(target, (integer)(llVecDist(llList2Vector(llGetBoundingBox(_leashedKey), 1), llList2Vector(llGetBoundingBox(_leashedKey), 0))/2.0));
            return;
        }
@awol;
        llOwnerSay("[QuickCollar℠]: Your master is AWOL. You are free to move.");
        stopMove();
    }
 
    timer() {
        llSetTimerEvent(0);
        llListenRemove(_comHandle);
    }
 
    touch_start(integer num) {
        key toucher = llDetectedKey(0);
        if(toucher != llGetOwner() && llListFindList(_mList, (list)((string)toucher)) == -1 && _FFA == "FFA: OFF") return;
        _aMaster = toucher;
        _comChannel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        _comHandle = llListen(_comChannel, "", _aMaster, "");
        llSetTimerEvent(60);
        llDialog(_aMaster, "\n            Welcome to the Quick Collar℠.\nCreated in 2013 by Wizardry and Steamworks\n                 15 July 2013: Version: 1.1\n", [_FFA, _LOCK, _RELAY, "[ ❖ Clothes ]", "⚐ PANIC", "[ ︻ Sits ]", "[ ✣ Moves ]", "[ 乀 Leash ]", "[ ⚉ Hide ]"], _comChannel);
    }
 
    changed(integer change) {
        if(change & CHANGED_INVENTORY || change & CHANGED_OWNER) {
            llResetScript();
        }
    }
 
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
 
}
 
state show_wearables {
    state_entry() {
        integer RLVchannel = 10+(integer)llFrand(10);
        llListen(RLVchannel, "", llGetOwner(), "");
        llOwnerSay("@getoutfit=" + (string)RLVchannel);
    }
 
    listen(integer channel,string name,key id,string message) {
        list CLOTHES = [ "gloves","jacket","pants","shirt","shoes","skirt","socks","underpants","undershirt","skin","eyes","hair","shape", "alpha", "tattoo", "physics" ];
        _menuItems = [];
        channel = 0;
        do {
            if(llGetSubString(message, channel, channel) != "1") jump skip;
            _menuItems += llList2String(CLOTHES, channel);
@skip;
        } while(++channel<41);
        CLOTHES = [];
        _SCAN = "wearables";
        llDialog(_aMaster, "Please select the wearables to take off:\n", wasEndlessMenu(_menuItems, ["⏏ Exit", "⟻ Back", "Next ⟼"], ""), _comChannel+1);
        _SCAN = "wearables";
        state main;
    }
 
    changed(integer change) {
        if(change & CHANGED_INVENTORY || change & CHANGED_OWNER) {
            llResetScript();
        }
    }
 
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
 
}
 
state show_attachments {
    state_entry() {
        integer RLVchannel = 10+(integer)llFrand(10);
        llListen(RLVchannel, "", llGetOwner(), "");
        llOwnerSay("@getattach=" + (string)RLVchannel);
    }
 
    listen(integer channel,string name,key id,string message) {
        list ATTACHMENTS = [ "none","chest","skull","left shoulder","right shoulder","left hand","right hand","left foot","right foot","spine","pelvis","mouth","chin","left ear","right ear","left eyeball","right eyeball","nose","r upper arm","r forearm","l upper arm","l forearm","right hip","r upper leg","r lower leg","left hip","l upper leg","l lower leg","stomach","left pec","right pec","center 2","top right","top","top left","center","bottom left","bottom","bottom right","neck","root"];
        _menuItems = [];
 
        channel = 0;
        do {
            if(llGetSubString(message, channel, channel) != "1") jump skip;
            _menuItems += llList2String(ATTACHMENTS, channel);
@skip;
        } while(++channel<41);
        ATTACHMENTS = [];
        llDialog(_aMaster, "Please select the attachments to detach:\n", wasEndlessMenu(_menuItems, ["⏏ Exit", "⟻ Back", "Next ⟼"], ""), _comChannel+2);
        _SCAN = "attachments";
        state main;
    }
 
    changed(integer change) {
        if(change & CHANGED_INVENTORY || change & CHANGED_OWNER) {
            llResetScript();
        }
    }
 
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
 
}