About

The script shows a simple example of how to leverage the tracker Corrade notification in order to track and display avatars on the grid map. Contrasted to other solutions using a beacon-like script that emits the avatar position to a central server, this template recycles the "Friend can locate you on map" SL feature and in doing so, minimizes communication overhead, does not require avatars to wear scripts and is perhaps a great tool for combat / RPG simulators - for instance, creating a "radar" typical to games where everyone is plotted on the map.

Marketplace Item

Design and Setup

To get this template to work, edit the configuration notecard and set group, password and corrade to the values corresponding to your Corrade setup.

############################# START CONFIGURATION ############################
 
# This is a configuration notecard based on the key-value data Wizardry and Steamworks reader. 
# Everything following the "#" character is ignored along with blank lines. Values can be set for various 
# parameters in a simple and understandable format with the following syntax: 
# KEY = "VALUE" 
# Every time you change this configuration notecard, the script will reset itself and the new configuration 
# will be read from this notecard.
 
# The "corrade", "group" and "password" settings must correspond to your Corrade settings.
 
# This is the UUID of the Corrade bot.
corrade = "fa6d6450-48f0-430f-868c-c6bbb7298983"
 
# The name of the group - it can also be the UUID of the group.
group = "My Group"
 
# The password for the group.
password = "mypassword"
 
############################# END CONFIGURATION #############################

Then, using the Corrade configuration tool Nucleus, enable the tracker notification for the configured group as well as the notifications permission in order to receive notifications.

Lastly, for any avatar that you wish to track, make Corrade send a friendship request to the avatars and tell the avatars to enable the Friend can locate you on map friendship permission. With Corrade as a friend and the map location friendship permission enabled, Corrade will periodically send updates on the avatar location to the template. By clicking the device, a menu will appear allowing the user to select which avatar to display - at which point, the map will be set to the region map of the tracked friend and the location dot will move on top of the map to their relative position. As a bonus, users can click the green dot and receive an SLURL link that they can click and teleport to the location of the tracked avatar.

Using Corrade and the built-in Friend can locate you on map feature, the entire process is opt-in: when avatars do not wish to be tracked, they simply use their viewer to disable the Friend can locate you on map permission and Corrade will not be able to send updates. Conversely, if avatars choose to be tracked, the Friend can locate you on map friendship permission can be enabled and Corrade will start sending updates to the template.

Limitations

The purpose of the template is to demonstrate the tracker Corrade notification and does not contain boilerplate LSL scripting paradigms such as access restrictions on who is able to use the template nor does the template allow tracking multiple avatars at the same time on the same map (ie: users will have to click and select an avatar to track). Nevertheless, due to Corrade, the avatars are tracked in real time such that after selecting an avatar, whenever the avatar moves, the template will update the map and move the dot locator to the new updated position. From Corrade's point of view, it is entirely possible to extend the template or redesign it such that it will be able to track multiple avatars on the same map but that falls outside the scope of the template just like access permissions.

Corrade updates the friend position every time an avatar (friend with Friend can locate you on map permission granted to Corrade) has moved more than $1m$ on either $x$ or $y$ axes and periodic scans are performed every $1$ minute - which may be an intolerable delay depending on the application to be designed.

Code

The template consists of two scripts:

  • the map script placed in the root prim,
  • the dot script placed in the prim that will indicate the position of the avatar

Map

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2013 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueEncode(list kvp) {
    if(llGetListLength(kvp) < 2) return "";
    string k = llList2String(kvp, 0);
    kvp = llDeleteSubList(kvp, 0, 0);
    string v = llList2String(kvp, 0);
    kvp = llDeleteSubList(kvp, 0, 0);
    if(llGetListLength(kvp) < 2) return k + "=" + v;
    return k + "=" + v + "&" + wasKeyValueEncode(kvp);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// escapes a string in conformance with RFC1738
string wasURLEscape(string i) {
    string o = "";
    do {
        string c = llGetSubString(i, 0, 0);
        i = llDeleteSubString(i, 0, 0);
        if(c == "") jump continue;
        if(c == " ") {
            o += "+";
            jump continue;
        }
        if(c == "\n") {
            o += "%0D" + llEscapeURL(c);
            jump continue;
        }
        o += llEscapeURL(c);
@continue;
    } while(i != "");
    return o;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// unescapes a string in conformance with RFC1738
string wasURLUnescape(string i) {
    return llUnescapeURL(
        llDumpList2String(
            llParseString2List(
                llDumpList2String(
                    llParseString2List(
                        i, 
                        ["+"], 
                        []
                    ), 
                    " "
                ), 
                ["%0D%0A"], 
                []
            ), 
            "\n"
        )
    );
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseString2List(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasListToCSV(list l) {
    list v = [];
    do {
        string a = llDumpList2String(
            llParseStringKeepNulls(
                llList2String(
                    l, 
                    0
                ), 
                ["\""], 
                []
            ),
            "\"\""
        );
        if(llParseStringKeepNulls(
            a, 
            [" ", ",", "\n", "\""], []
            ) != 
            (list) a
        ) a = "\"" + a + "\"";
        v += a;
        l = llDeleteSubList(l, 0, 0);
    } while(l != []);
    return llDumpList2String(v, ",");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
list wasCSVToList(string csv) {
    list l = [];
    list s = [];
    string m = "";
    do {
        string a = llGetSubString(csv, 0, 0);
        csv = llDeleteSubString(csv, 0, 0);
        if(a == ",") {
            if(llList2String(s, -1) != "\"") {
                l += m;
                m = "";
                jump continue;
            }
            m += a;
            jump continue;
        }
        if(a == "\"" && llGetSubString(csv, 0, 0) == a) {
            m += a;
            csv = llDeleteSubString(csv, 0, 0);
            jump continue;
        }
        if(a == "\"") {
            if(llList2String(s, -1) != a) {
                s += a;
                jump continue;
            }
            s = llDeleteSubList(s, -1, -1);
            jump continue;
        }
        m += a;
@continue;
    } while(csv != "");
    // postcondition: length(s) = 0
    return l + m;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasMenuIndex = 0;
list wasDialogMenu(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);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2019 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
float wasMapValueToRange(float value, float xMin, float xMax, float yMin, float yMax) {
    return yMin + (
        (
            yMax - yMin
        )
        *
        (
            value - xMin
        )
        /
        (
            xMax - xMin
        )
    );
}
 
// Move the dot over the local positon of the avatar on the map.
updateDot(string message) {
    if(active == "")
        return;
 
    integer idx = llListFindList(avatars, [ message ]);
    if(idx == -1) {
        // DEBUG
        llOwnerSay("Avatar not found in tracked list...");
        return;
    }
 
    // Stride 4: Avatar name x region name x local position x map UUID
    list update = llList2List(avatars, idx, idx + 3);
 
    string region = llList2String(update, 1);
    vector position = (vector)llList2String(update, 2);
    key mapUUID = llList2Key(update, 3);
 
    // DEBUG
    //llOwnerSay("Updating map display...");
 
    // Set the map textue.
    llSetTexture((string)mapUUID, 0);
 
    // Compute the offset to move the dot and send it to the link set.
    vector scale = llGetScale();
 
    // DEBUG
    //llOwnerSay("position:" + (string)position + " x: " + (string)x + " y:" + (string)y);
 
    llMessageLinked(
        LINK_SET,
        0,
        wasListToCSV(
            [
                "avatar",
                message,
                "region",
                region,
                "position",
                (string)position,
                "scale",
                (string)scale
            ]
        ),
        NULL_KEY
    );
}
 
// corrade data
string CORRADE = "";
string GROUP = "";
string PASSWORD = "";
 
 
// for holding the callback URL
string callback = "";
 
// menu for selecting avatars
list menu = [];
 
// Stride 4: Avatar name x region name x local position x map UUID
list avatars = [];
 
// the active avatar
string active = "";
 
// key-value data will be read into this list
list tuples = [];
 
// for notecard reading
integer line = 0;
 
default {
    state_entry() {
        if(llGetInventoryType("configuration") != INVENTORY_NOTECARD) {
            llOwnerSay("Sorry, could not find an inventory notecard.");
            return;
        }
        // DEBUG
        llOwnerSay("Reading configuration file...");
        llGetNotecardLine("configuration", line);
    }
    dataserver(key id, string data) {
        if(data == EOF) {
            // invariant, length(tuples) % 2 == 0
            if(llGetListLength(tuples) % 2 != 0) {
                llOwnerSay("Error in configuration notecard.");
                return;
            }
            CORRADE = llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "corrade"
                              ]
                          )
                      +1);
            if(CORRADE == "") {
                llOwnerSay("Error in configuration notecard: corrade");
                return;
            }
            GROUP = llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "group"
                              ]
                          )
                      +1);
            if(GROUP == "") {
                llOwnerSay("Error in configuration notecard: password");
                return;
            }
            PASSWORD = llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "password"
                              ]
                          )
                      +1);
            if(GROUP == "") {
                llOwnerSay("Error in configuration notecard: group");
                return;
            }
            // DEBUG
            llOwnerSay("Read configuration file...");
            state url;
        }
        if(data == "") jump continue;
        integer i = llSubStringIndex(data, "#");
        if(i != -1) data = llDeleteSubString(data, i, -1);
        list o = llParseString2List(data, ["="], []);
        // get rid of starting and ending quotes
        string k = llDumpList2String(
            llParseString2List(
                llStringTrim(
                    llList2String(
                        o, 
                        0
                    ), 
                STRING_TRIM), 
            ["\""], []
        ), "\"");
        string v = llDumpList2String(
            llParseString2List(
                llStringTrim(
                    llList2String(
                        o, 
                        1
                    ), 
                STRING_TRIM), 
            ["\""], []
        ), "\"");
        if(k == "" || v == "") jump continue;
        tuples += k;
        tuples += v;
@continue;
        llGetNotecardLine("configuration", ++line);
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
            llResetScript();
        }
    }
}
 
state url {
    state_entry() {
        // DEBUG
        llOwnerSay("Requesting URL...");
        llRequestURL();
    }
    http_request(key id, string method, string body) {
        if(method != URL_REQUEST_GRANTED) return;
        callback = body;
        // DEBUG
        llOwnerSay("Got URL...");
        state bind_tracker;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) {
            llResetScript();
        }
    }
}
 
state bind_tracker {
    state_entry() {
        // DEBUG
        llOwnerSay("Binding to tracker notification...");
 
        llInstantMessage(CORRADE, 
            wasKeyValueEncode(
                [
                    "command", "notify",
                    "group", wasURLEscape(GROUP),
                    "password", wasURLEscape(PASSWORD),
                    "action", "set",
                    "type", "tracker",
                    "URL", wasURLEscape(callback),
                    "callback", wasURLEscape(callback)
                ]
            )
        );
 
        llSetTimerEvent(60);
    }
    http_request(key id, string method, string body) {
        llHTTPResponse(id, 200, "OK");
        if(wasKeyValueGet("command", body) != "notify" ||
            wasKeyValueGet("success", body) != "True") {
            // DEBUG
            llOwnerSay("Unable to bind to tracker notification: " + 
                wasURLUnescape(
                    wasKeyValueGet("error", body)
                )
            );
 
            llResetScript();
        }
 
        // DEBUG
        llOwnerSay("Tracking notification bound...");
 
        state tracker;
    }
    timer() {
        // DEBUG
        llOwnerSay("Timeout binding to tracker notification...");
 
        llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if((change & CHANGED_INVENTORY) || 
            (change & CHANGED_REGION_START) || 
            (change & CHANGED_OWNER)) {
            llResetScript();
        }
    }
    state_exit() {
        llSetTimerEvent(0);
    }
}
 
state tracker {
    state_entry() {
        // DEBUG
        llOwnerSay("Tracking...");
    }
    touch_start(integer num_detected) {
        if(llGetListLength(avatars) == 0) {
            llSay(0, "No avatars have been registered yet.");
            return;
        }
 
        menu = llList2ListStrided(avatars, 0, -1, 4);
 
        // DEBUG
        llOwnerSay("Sending menu with tracked avatars: " + llDumpList2String(menu, ","));
 
        integer comChannel = ((integer)("0x"+llGetSubString((string)llDetectedKey(0),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        llListen(comChannel, "", llDetectedKey(0), "");
        llDialog(llDetectedKey(0), "Please select an avatar from the list of avatars to display on the map.", wasDialogMenu(menu, ["โŸต Back", "", "Next โŸถ"], ""), comChannel);
    }
    listen(integer channel, string name, key id, string message) {
        if(message == "โŸต Back") {
            llDialog(id, "Please select an avatar from the list of avatars to display on the map.", wasDialogMenu(menu, ["โŸต Back", "", "Next โŸถ"], "<"), channel);
            return;
        }
        if(message == "Next โŸถ") {
            llDialog(id, "Please select an avatar from the list of avatars to display on the map.", wasDialogMenu(menu, ["โŸต Back", "", "Next โŸถ"], ">"), channel);
            return;
        }
 
        // DEBUG
        llOwnerSay("Chosen avatar: " + message);
 
        // Set the active avatar.
        active = message;
 
        llSetTimerEvent(1);
    }
    timer() {
        // Update the avatar on the map.
        updateDot(active);
    }
    http_request(key id, string method, string body) {
        llHTTPResponse(id, 200, "OK");
 
        // Process tracker notification by retrieving the map for the region.
        if(wasKeyValueGet("type", body) == "tracker") {
            // DEBUG
            llOwnerSay("Tracker: " + wasURLUnescape(body));
 
            string firstname = wasKeyValueGet("firstname", body);
            string lastname = wasKeyValueGet("lastname", body);
            string region = wasKeyValueGet("region", body);
 
            // DEBUG
            llOwnerSay("Requesting region map UUID...");
 
            // Send the request to retrieve the map UUID.
            llInstantMessage(CORRADE,
                wasKeyValueEncode(
                    [
                        "command", "getgridregiondata",
                        "group", wasURLEscape(GROUP),
                        "password", wasURLEscape(PASSWORD),
                        "region", region,
                        "data", wasListToCSV(
                            [
                                "MapImageID"
                            ]
                        ),
                        // pass the region name and avatar 
                        // name through afterburn
                        "_avatar", (firstname + " " + lastname),
                        "_region", region,
                        "_position", wasKeyValueGet("position", body),
                        // sent to URL
                        "callback", wasURLEscape(callback)
                    ]
                )
            );
 
            return;
        }
 
        // Store the map
        if(wasKeyValueGet("command", body) == "getgridregiondata" &&
            wasKeyValueGet("success", body) == "True") {
 
            // Retrive returned data and extract the map UUID.
            list data = wasCSVToList(
                wasURLUnescape(
                    wasKeyValueGet("data", body)
                )
            );
 
            key mapUUID = llList2Key(
                data,
                llListFindList(
                    data, 
                    [ 
                        "MapImageID" 
                    ]
                ) + 1
            );
 
            if(mapUUID == NULL_KEY) {
                // DEBUG
                llOwnerSay("Failed to retrive remote region map UUID...");
                return;
            }
 
            // Stride 4: Avatar name x region name x local position x map UUID
            string avatar = wasURLUnescape(
                wasKeyValueGet("_avatar", body)
            );
            string region = wasURLUnescape(
                wasKeyValueGet("_region", body)
            );
            string position = wasURLUnescape(
                wasKeyValueGet("_position", body)
            );
 
            // DEBUG
            llOwnerSay("Remote region map ID for region: " + region + " is: " + (string)mapUUID);
 
            integer idx = llListFindList(avatars, [ avatar ]);
 
            // If the avatar is not registered then add the avatar to the list.
            if(idx == -1) {
                // DEBUG
                llOwnerSay("Adding new avatar to tracking list: " + avatar);
 
                llSetTimerEvent(0);
 
                avatars += avatar;
                avatars += region;
                avatars += position;
                avatars += (string)mapUUID;
 
                llSetTimerEvent(1);
 
                return;
            }
 
            // Extract the avatar from the list and update the details.
            llSetTimerEvent(0);
 
            list update = llList2List(avatars, idx, idx + 3);
            avatars = llDeleteSubList(avatars, idx, idx + 3);
 
            update = llListReplaceList(update, [region], 1, 1);
            update = llListReplaceList(update, [position], 2, 2);
            update = llListReplaceList(update, [mapUUID], 3, 3);
 
            avatars += update;
 
            llSetTimerEvent(1);
 
            // DEBUG
            llOwnerSay("Update list is: " + llDumpList2String(update, ","));
            llOwnerSay("Full list is: " + llDumpList2String(avatars, ","));
 
            return;
        }
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if((change & CHANGED_INVENTORY) || 
            (change & CHANGED_REGION_START) || 
            (change & CHANGED_OWNER)) {
            llResetScript();
        }
    }
    state_exit() {
        llSetTimerEvent(0);
    }
}
 

Dot

///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasListToCSV(list l) {
    list v = [];
    do {
        string a = llDumpList2String(
            llParseStringKeepNulls(
                llList2String(
                    l, 
                    0
                ), 
                ["\""], 
                []
            ),
            "\"\""
        );
        if(llParseStringKeepNulls(
            a, 
            [" ", ",", "\n", "\""], []
            ) != 
            (list) a
        ) a = "\"" + a + "\"";
        v += a;
        l = llDeleteSubList(l, 0, 0);
    } while(l != []);
    return llDumpList2String(v, ",");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
list wasCSVToList(string csv) {
    list l = [];
    list s = [];
    string m = "";
    do {
        string a = llGetSubString(csv, 0, 0);
        csv = llDeleteSubString(csv, 0, 0);
        if(a == ",") {
            if(llList2String(s, -1) != "\"") {
                l += m;
                m = "";
                jump continue;
            }
            m += a;
            jump continue;
        }
        if(a == "\"" && llGetSubString(csv, 0, 0) == a) {
            m += a;
            csv = llDeleteSubString(csv, 0, 0);
            jump continue;
        }
        if(a == "\"") {
            if(llList2String(s, -1) != a) {
                s += a;
                jump continue;
            }
            s = llDeleteSubList(s, -1, -1);
            jump continue;
        }
        m += a;
@continue;
    } while(csv != "");
    // postcondition: length(s) = 0
    return l + m;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2019 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
float wasMapValueToRange(float value, float xMin, float xMax, float yMin, float yMax) {
    return yMin + (
        (
            yMax - yMin
        )
        *
        (
            value - xMin
        )
        /
        (
            xMax - xMin
        )
    );
}
 
vector position = ZERO_VECTOR;
string region = "";
string avatar = "";
 
default
{
    touch_start(integer num) {
        // Do not send the SLURL if no avatar has been reported.
        if(position == ZERO_VECTOR ||
            region == "" ||
            avatar == "")
            return;
 
        llInstantMessage(
            llDetectedKey(0),
            "Avatar: " +
            avatar +
            " is currently at: " +
            "http://maps.secondlife.com/secondlife/" + 
            llEscapeURL(region )+ 
            "/" + 
            (string)llFloor(position.x) + 
            "/" + 
            (string)llFloor(position.y) + 
            "/0"
        );
    }
    link_message(integer link, integer value, string message, key id) {
        // DEBUG
        //llOwnerSay("Data received: " + message);
 
        list data = wasCSVToList(message);
 
        avatar = llList2String(
            data,
            llListFindList(
                data,
                [ 
                    "avatar"
                ]
            ) + 1
        );
 
        region = llList2String(
            data,
            llListFindList(
                data,
                [ 
                    "region"
                ]
            ) + 1
        );
 
        position = (vector)llList2String(
            data,
            llListFindList(
                data,
                [ 
                    "position"
                ]
            ) + 1
        );
 
        vector scale = (vector)llList2String(
            data,
            llListFindList(
                data,
                [ 
                    "scale"
                ]
            ) + 1
        );
 
        float x = wasMapValueToRange(position.x, 0, 256, 0, scale.x) - scale.x / 2;
        float y = wasMapValueToRange(position.y, 0, 256, 0, scale.y) - scale.y / 2;
 
        // Set the dot position.
        llSetPos(<x, y, 0>);
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if((change & CHANGED_INVENTORY) || 
            (change & CHANGED_REGION_START) || 
            (change & CHANGED_OWNER)) {
            llResetScript();
        }
    }
    state_exit() {
        llSetTimerEvent(0);
    }
}
 

Index


secondlife/scripted_agents/corrade/projects/in_world/grid_avatar_tracker.txt ยท Last modified: 2022/11/24 07:45 by 127.0.0.1

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.