Table of Contents

About

This script is a simple avatar follower that uses RLV and keeps the avatar that is wearing this script in-range of 5 meters from the chosen avatar. It uses the local chat such that the wearing avatar can request to follow an avatar. For example, issuing:

@follow Lance

will follow any avatar that contains the substring Lance in its name. In that sense, the script performs a search for all avatars and tries to match the criteria on local-chat to the found avatars.

Once the target has been found, the script periodically sweeps (in 1 second intervals) and locates the position of the avatar and then uses RLV to turn around to face the target avatar and then moves closer in case the distance exceeds 5 meters.

Setup and Usage

The script has two commands: @follow and @unfollow, the former being followed by some search criteria which can be part of the name of the avatar to follow (note that the script requires user-names to be passed, rather than display names).

Practical Applications

There are cases such as going shopping or mutually exploring a simulator when it is difficult to match each other's pace and this gadget can solve that, making sure that the avatars are well within 5 meters of each other.

Walk, Do Not Run

One of the shortcomings of RLV collars and other devices that are meant to move avatars around in a certain range is that the avatar to be restricted will eventually surpass the boundary, at which point the script will use llMoveToTarget to bring the avatar within range again. However, more than often, as it is the case with notable technologies such as OpenCollar, the avatar burst towards the target, jolting, running for a few seconds and then eventually slowing down due to the dampening of llMoveToTarget. This is an undesirable effect, sometimes, since the purpose may not be to make the avatar run back into range, but rather slowly drift towards the target.

The follower challenges the running issue by calculating the necessary dampening seconds that need to be passed to llMoveToTarget such that wherever the avatar may be located, the avatar will find itself walking gently to the target. This was accomplished by measuring the top-speed of an avatar while walking and then calculating the number of seconds needed to maintain a constant walking speed, using:

\begin{eqnarray*}
t &=& \frac{S}{v} 
\end{eqnarray*}

where $t$ is the time it would take if the avatar had to walk $S$ meters at a speed of $v$ in the direction of the target. Since we know the distance to the target, as well as the top-speed of an avatar while walking, we can compute $t$ and feed it to llMoveToTarget.

The function responsible for the movement is moveTo:

///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
moveTo(vector target, integer bb) {
    llStopMoveToTarget();
    llTargetRemove(moveTarget);
    vector mePos = llGetPos();
    vector pointTo = target - mePos;
    llOwnerSay("@setrot:" + (string)llAtan2(pointTo.x, pointTo.y) + "=force");
    moveTarget = llTarget(target, 2 + bb);
    llMoveToTarget(target, llVecDist(target, mePos)/3.20);
}

which takes as parameter the position target of an avatar (or object) and the distance of the bounding box enclosing the avatar (the latter avoids the follower from pushing into the avatar).

Code

follow.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2014 - 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    //
///////////////////////////////////////////////////////////////////////////
moveTo(vector target, integer bb, key agent) {
    llStopMoveToTarget();
    llTargetRemove(moveTarget);
    vector mePos = llGetPos();
    vector pointTo = target - mePos;
    llOwnerSay("@setrot:" + (string)llAtan2(pointTo.x, pointTo.y) + "=force");
    moveTarget = llTarget(target, 2 + bb);
    float dampening = llVecDist(target, mePos)/3.20;
    if(llGetAgentInfo(agent) & AGENT_ALWAYS_RUN) {
        dampening = llVecDist(target, mePos)/5.13;
        jump move;
    }
@move;
    if(dampening < 0.045) dampening = 0.045;
    llMoveToTarget(target, dampening);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
wasTellOwner(string type, string message) {
    llOwnerSay("⎣" + type + "⎤: " + message + ".");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueSet(string k, string v, string data) {
    if(llStringLength(k) == 0) return "";
    if(llStringLength(v) == 0) return "";
    if(llStringLength(data) == 0) return k + "=" + v;
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []), 
            0, -1, 2
        ), 
    [ k ]);
    if(i != -1) return llDumpList2String(
        llListReplaceList(
            llParseString2List(data, ["&"], []), 
            [ k + "=" + v ], 
        i, i), 
    "&");
    return data + "&" + k + "=" + v;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueDelete(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []), 
            0, -1, 2
        ), 
    [ k ]);
    if(i != -1) return llDumpList2String(
        llDeleteSubList(
            llParseString2List(data, ["&"], []),
        i, i), 
    "&");
    return data;
}
 
///////////////////////////////////////////////////////////////////////////
//    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))]);
}
 
string criteria = "";
key uuid = NULL_KEY;
integer moveTarget = 0;
 
default {
    state_entry() {
        // LID™ reasoning - 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", llGetObjectDesc()) > 0) {
            llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("login", "0", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
            state check;
            return;
        }
        llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("login", "0", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
        state check;
    }
    // LID™ reasoning - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    // order: on_rez -> attach
    //
    // if object attached, then login := login+1
    // else login := -1
    attach(key id) {
        // attached
        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))]);
            // Reset script for llGetCreator() bugture.
            llResetScript();
        }
        // detached
        llSetLinkPrimitiveParamsFast(2, [PRIM_DESC, wasKeyValueSet("login", "-1", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0))]);
    }
    // login = 1 after object attached
    //       = -1 after object detached
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
}
 
state check {
    state_entry() {
        // alarm for seekng RLV
        llSetTimerEvent(25);
        integer channel = 10+(integer)llFrand(10);
        llListen(channel, "", llGetOwner(), "");
        llOwnerSay("@version=" + (string)channel);
    }
    timer() {
        llSetTimerEvent(0);
        // alarm triggered, we don't have RLV
        wasTellOwner("follow", "This gizmo requires a RLV-enabled viewer.");
        return;
    }
    listen(integer channel, string name, key id, string message) {
        llSetTimerEvent(0);
        // we have RLV
        state seek;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
}
 
state seek {
    state_entry() {
        uuid = (key) wasKeyValueGet("follow", llList2String(llGetLinkPrimitiveParams(2, [PRIM_DESC]), 0));
        if(uuid != "") state follow;
        if(criteria != "") {
            llSensorRepeat("", "", AGENT, 65, TWO_PI, 1);
            // alarm for searching agent matching criteria
            llSetTimerEvent(5);
        }
        llListen(0, "", llGetOwner(), "");
    }
    listen(integer channel, string name, key id, string message) {
        list cmd = llParseString2List(message, [" "], []);
        if(llList2String(cmd, 0) != "@follow") {
            cmd = [];
            return;
        }
        criteria = llList2String(cmd, 1);
        llSensorRepeat("", "", AGENT, 65, TWO_PI, 1);
        // alarm for searching agent matching criteria
        llSetTimerEvent(5);
    }
    timer() {
        wasTellOwner("follow", "Could not find avatar to follow...");
        llResetScript();
    }
    sensor(integer num) {
        --num;
        do {
            list name = llParseString2List(llDetectedName(num), ["."], []);
            do {
                string nib = llList2String(name, 0);
                if(llSubStringIndex(llToLower(nib), llToLower(criteria)) != -1) {
                    wasTellOwner("follow", "Target agent found...");
                    uuid = llDetectedKey(num);
                    llSetLinkPrimitiveParamsFast(
                        2, 
                        [PRIM_DESC, 
                            wasKeyValueSet(
                                "follow", 
                                (string)uuid, 
                                llList2String(
                                    llGetLinkPrimitiveParams(
                                        2, 
                                        [PRIM_DESC]
                                    ), 
                                    0
                                )
                            )
                        ]
                    );
                    criteria = "";
                    wasTellOwner("follow", "Starting follow...");
                    state follow;
                }
                name = llDeleteSubList(name, 0, 0);
            } while(llGetListLength(name) != 0);
        } while(--num>-1);
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
}
 
state follow {
    state_entry() {
        llRequestPermissions(llGetOwner(), PERMISSION_TAKE_CONTROLS);
        llSensorRepeat("", uuid, AGENT, 65, TWO_PI, 1);
        llListen(0, "", llGetOwner(), "");
    }
    listen(integer channel, string name, key id, string message) {
        list cmd = llParseString2List(message, [" "], []);
        if(llList2String(cmd, 0) != "@follow" && llList2String(cmd, 0) != "@unfollow") {
            cmd = [];
            return;
        }
        if(llList2String(cmd, 0) == "@unfollow") {
            wasTellOwner("follow", "Stopping follow...");
        }
        llSetLinkPrimitiveParamsFast(
            2, 
            [PRIM_DESC, 
                wasKeyValueDelete(
                    "follow", 
                    llList2String(
                        llGetLinkPrimitiveParams(
                            2, 
                            [PRIM_DESC]
                        ), 
                    0
                    )
                )
            ]
        );
        if(llList2String(cmd, 0) == "@follow") {
            criteria = llList2String(cmd, 1);
            llSetTimerEvent(0);
            state seek;
        }
        llResetScript();
    }
    run_time_permissions(integer perm) {
        if(perm & PERMISSION_TAKE_CONTROLS) {
            llTakeControls(
                CONTROL_FWD |
                CONTROL_BACK |
                CONTROL_LEFT |
                CONTROL_RIGHT |
                CONTROL_ROT_LEFT |
                CONTROL_ROT_RIGHT |
                CONTROL_UP |
                CONTROL_DOWN |
                CONTROL_LBUTTON |
                CONTROL_ML_LBUTTON, TRUE, TRUE);
            return;
        }
        wasTellOwner("follow", "Could not get control permissions. Restarting script.");
        llResetScript();
    }
    sensor(integer num) {
        --num;
        do {
            vector pos = llDetectedPos(num);
            if(llVecDist(pos, llGetPos()) < 5) {
                llStopMoveToTarget();
                llTargetRemove(moveTarget);
                return;
            }
            if(llGetAgentInfo(uuid) & AGENT_IN_AIR) return;
            moveTo(
                pos,
                (integer)(llVecDist(llList2Vector(llGetBoundingBox(uuid), 1), 
                llList2Vector(llGetBoundingBox(uuid), 0))/2.0),
                uuid
            );
            return;
        } while(--num>-1);
    }
    at_target(integer tnum, vector targetpos, vector ourpos) {
        llStopMoveToTarget();
        llTargetRemove(moveTarget);
    }
    control(key id, integer level, integer edge) {
        // while key is held down, don't move the avatar
        if(level & ~edge == 0) return;
        llStopMoveToTarget();
        llTargetRemove(moveTarget);
        llReleaseControls();
        llSetTimerEvent(0);
        state move;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
}
 
state move {
    state_entry() {
        llRequestPermissions(llGetOwner(), PERMISSION_TAKE_CONTROLS);
    }
    run_time_permissions(integer perm) {
        if(perm & PERMISSION_TAKE_CONTROLS == 0) {
            wasTellOwner("follow", "Could not get control permissions. Restarting script.");
            llResetScript();
        }
        llTakeControls(
            CONTROL_FWD |
            CONTROL_BACK |
            CONTROL_LEFT |
            CONTROL_RIGHT |
            CONTROL_ROT_LEFT |
            CONTROL_ROT_RIGHT |
            CONTROL_UP |
            CONTROL_DOWN |
            CONTROL_LBUTTON |
            CONTROL_ML_LBUTTON, TRUE, TRUE);
    }
    control(key id, integer level, integer edge) {
        // alarm 
        llSetTimerEvent(1);
    }
    timer() {
        llSetTimerEvent(0);
        llReleaseControls();
        state follow;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
    // LID™ - http://grimore.org/fuss:lsl#log-in_detection_with_attachments
    attach(key id) {
        LID(id);
    }
}