ChangeLog

16 November 2015

  • Updated the teleport script to display the buttons in-order as they are entered in the notecard.

Shortnote

This script was tested and works on OpenSim version 0.7.4!

Similar to teleport this variation on the same theme will allow you to specify multiple destinations to teleport to by reading coordinates from a notecard. This version does not implement cross-sim teleports such as the jumpdrive/crossdrive teleport system. Instead it just uses the teleport design to teleport avatars within the same region.

Marketplace

The Build

The build consists in two scripts: one script for the base, and one script for the dish. If you open up the teleporter you will find inside:

  • the base script
  • the destinations notecard
  • the teleport dish

Base Script

The base script takes care of rezzing the teleport dish as well as showing the menu.

baseScript.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    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueEncode(list data) {
    list k = llList2ListStrided(data, 0, -1, 2);
    list v = llList2ListStrided(llDeleteSubList(data, 0, 0), 0, -1, 2);
    data = [];
    do {
        data += llList2String(k, 0) + "=" + llList2String(v, 0);
        k = llDeleteSubList(k, 0, 0);
        v = llDeleteSubList(v, 0, 0);
    } while(llGetListLength(k) != 0);
    return llDumpList2String(data, "&");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseString2List(data, ["&", "="], []);
    integer i = llListFindList(a, [ k ]);
    if(i != -1) return llList2String(a, i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueSet(string k, string v, string data) {
    if(llStringLength(data) == 0) return k + "=" + v;
    if(llStringLength(k) == 0) return "";
    if(llStringLength(v) == 0) return "";
    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) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasMenuIndex = 0;
list wasDialogMenu(list input, list actions, string direction) {
    integer x = 11-wasListCountExclude(actions, [""]);
    if(direction == ">" &&  (wasMenuIndex+1)*x+wasMenuIndex+1 < llGetListLength(input)) {
        ++wasMenuIndex;
        jump slice;
    }
    if(direction == "<" && wasMenuIndex-1 >= 0) {
        --wasMenuIndex;
        jump slice;
    }
@slice;
    integer m = wasMenuIndex*x;
    return wasListMerge(
        wasDialogSort(
            llList2List(
                input, 
                m+wasMenuIndex, 
                m+x+wasMenuIndex
            ), 
            ""
        ), 
        actions, 
        "", 
        " "
    );
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasDialogSort(list input, string pad) {
    input = llList2List(
        input + 
            [ pad, pad, pad, pad, pad, pad, pad, pad, pad, pad, pad, pad ], 
        0, 
        12
    );
    return llList2List(input, 9, 11) + 
        llList2List(input, 6, 8) + 
        llList2List(input, 3, 5) + 
        llList2List(input, 0, 2);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasListMerge(list a, list b, string merge, string pad) {
    if(llGetListLength(a) == 0 && llGetListLength(b) == 0) return [];
    string af = llList2String(a, 0);
    a = llDeleteSubList(a, 0, 0);
    string bf = llList2String(b, 0);
    b = llDeleteSubList(b, 0, 0);
    if(af != merge && bf == merge)
        return [ af ] +  wasListMerge(a, b, merge, pad);
    if(af == merge && bf != merge)
        return [ bf ] + wasListMerge(a, b, merge, pad);
    if(af != merge && bf != merge)
        return [ af, bf ] + wasListMerge(a, b, merge, pad);
    // assert: af == merge && bf == merge 
    return [ pad ] + wasListMerge(a, b, merge, pad);
}
 
///////////////////////////////////////////////////////////////////////////
//    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);
}
 
list names = [];
list safe_names = [];
list coord = [];
 
key sit = NULL_KEY;
string firstname = "";
integer selected = FALSE;
integer line = 0;
string destName = "";
vector dest = ZERO_VECTOR;
integer seconds = 30;
integer dish_channel = 0;
string pad = "\n \n \n \n \n";
 
default {
    state_entry() {
        // read the destination notecard
        if(llGetInventoryType("destinations") != INVENTORY_NOTECARD) {
            llOwnerSay("Failed to find destinations notecard in inventory.\nPlease add a notecard containing the destinations");
            return;
        }
        llSetText("Reading destinations." + pad, <1, 1, 0>, 1.0);
        llGetNotecardLine("destinations", line);
    }
    dataserver(key id, string data) {
        if(data == EOF) {
            llSetText("Read destinations." + pad, <1, 0, 0>, 1.0);
            // check for consistency
            if(llGetListLength(names) != llGetListLength(coord)) {
                llSetText("Error reading destinations. Restarting..." + pad, <1, 0, 0>, 1.0);
                llSetTimerEvent(1);
                return;
            }
            state select;
            return;
        }
        if(data == "") jump next_line;
        list dc = llParseString2List(data, ["#"], []);
        names += llList2String(dc, 0);
        coord += (vector)llList2String(dc, 1);
@next_line;
        llGetNotecardLine("destinations", ++line);
    }
    timer() {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state select {
    state_entry() {
        llSetText("Touch me to select a destination." + pad, <0, 1, 0>, 1.0);
        dish_channel = (integer)("0x8" + llGetSubString(llGetOwner(), 0, 6));
        llListen(dish_channel, "teleport dish", "", "");
    }
    touch_start(integer total_numer) {
        key click = llDetectedKey(0);
        // check if the teleport is locked to owner or unlocked
        if(wasKeyValueGet("lock", llGetObjectDesc()) == "true" && click != llGetOwner()) return;
        // block if teleporter is in use
        if(sit != NULL_KEY && click != sit) return;
        // grab user key and name
        sit = click;
        firstname = llList2String(llParseString2List(llDetectedName(0), [" "], []), 0);
        // display overhead text and set the countdown
        llSetText(firstname + " is using the teleporter [" + (string)seconds + "]." + pad, <1, 1, 0>, 1.0);
        llSetTimerEvent(1);
        // generate the list of destinations and send the dialog
        integer s = llGetListLength(names);
        integer i = 0;
        safe_names = [];
        do {
            safe_names += llGetSubString(llList2String(names, i), 0, 8);
        } while(++i < s);
        integer channel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        llListen(channel, "", sit, "");
        llDialog(sit, "Choose a destination to teleport to from the provided list of destinations.", wasDialogMenu(safe_names, ["⟵ Back", "◉ Options", "Next ⟶", "", "", "✖︎ Abort"], ""), channel);
    }
    listen(integer channel, string name, key id, string message) {
        if(message == "⟵ Back") {
            llDialog(sit, "Choose a destination to teleport to from the provided list of destinations.", wasDialogMenu(safe_names, ["⟵ Back", "◉ Options", "Next ⟶", "", "", "✖︎ Abort"], "<"), channel);
            return;
        }
        if(message == "Next ⟶") {
            llDialog(sit, "Choose a destination to teleport to from the provided list of destinations.", wasDialogMenu(safe_names, ["⟵ Back", "◉ Options", "Next ⟶", "", "", "✖︎ Abort"], ">"), channel);
            return;
        }
        if(message == "◉ Options") {
            message = "Here you can toggle the teleporter so it is restricted to the owner or not.";
            if(wasKeyValueGet("lock", llGetObjectDesc()) != "false") {
                message += "\n\nThe teleporter is currently locked to the owner.";
                jump set_message;
            }
            message += "\n\nThe teleporter is currently unlocked.";
@set_message;
            llDialog(sit, message, ["✗ Lock", "✔ Unlock", "⏏ Exit"], channel);
            return;
        }
        if(message == "✗ Lock") {
            llSetObjectDesc(wasKeyValueSet("lock", "true", llGetObjectDesc()));
            llDialog(sit, "Here you can toggle the teleporter so it is restricted to the owner or not.\n\nThe teleporter is currently locked to the owner.", ["✗ Lock", "✔ Unlock", "⏏ Exit"], channel);
            return;
        }
        if(message == "✔ Unlock") {
            llSetObjectDesc(wasKeyValueSet("lock", "false", llGetObjectDesc()));
            llDialog(sit, "Here you can toggle the teleporter so it is restricted to the owner or not.\n\nThe teleporter is currently unlocked.", ["✗ Lock", "✔ Unlock", "⏏ Exit"], channel);
            return;
        }
        if(message == "⏏ Exit") {
            llDialog(sit, "Choose a destination to teleport to from the provided list of destinations.", wasDialogMenu(safe_names, ["⟵ Back", "◉ Options", "Next ⟶", "", "", "✖︎ Abort"], ""), channel);
            return;
        }
        if(message == "✖︎ Abort") {
            llWhisper(dish_channel, "DIE");
            llSleep(1);
            llResetScript();
        }
        // dish message
        if(message == "OK") {
            llResetScript();
        }
        integer i = llGetListLength(names)-1;
        do {
            destName = llList2String(names, i);
            if(llSubStringIndex(destName, message) != -1) {
                dest = llList2Vector(coord, i);
                jump ready;
            }
        } while(--i>-1);
        llSetText("Could not find destination." + pad, <1, 0, 0>, 1.0);
        seconds = 1;
        return;
@ready;
        selected = TRUE;
        seconds = 10;
        vector rez_pos = llGetPos();
        rez_pos.z += .030;
        llWhisper(dish_channel, "DIE");
        llRezObject("teleport dish", rez_pos, ZERO_VECTOR, ZERO_ROTATION, 0);
        llSleep(1);
        llWhisper(
            dish_channel, 
            wasKeyValueEncode(
                [
                    "destination", dest,
                    "avatar", id
                ]
            )
        );
    }
    changed(integer change) {
        llResetScript();
    }
    timer() {
        if(selected == TRUE) {
            llSetText(firstname + ", touch to teleport to \"" + destName + "\" [" + (string)seconds + "]." + pad, <1, 1, 0>, 1.0);
            jump continue;
        }
        if(selected == FALSE) {
            llSetText(firstname + " is using the teleporter [" + (string)seconds + "]." + pad, <1, 1, 0>, 1.0);
            jump continue;
        }
@continue;
        if(--seconds == 0) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}

Setting-up Destinations

To set-up the multiple destination teleport, you need to create a notecard called Destinations which will have the following format:

<Destination Name 1>#<Vector 1>
<Destination Name 2>#<Vector 2>

As an example, the following notecard was used during tests:

PGS#<51.528809, 26.227453, 1034.044189>
Laz#<18.070259, 77.660828, 1005.178101>

where PGS represents the name of the destination which can be anything less than 12 characters and the two vectors represent region coordinates.

Please remember to have a blank newline in the notecard at the end, otherwise the script will not be able to read the notecard.

The Teleport Dish

The telepoort dish contains a script that is responsible for transporting the avatar to the destination chosen by the base script.

dishScript.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) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseString2List(data, ["&", "="], []);
    integer i = llListFindList(a, [ k ]);
    if(i != -1) return llList2String(a, i+1);
    return "";
}
 
key sit = NULL_KEY;
vector dest = ZERO_VECTOR;
integer channel = 0;
 
default {
    state_entry() {
        channel = (integer)("0x8" + llGetSubString(llGetOwner(), 0, 6));
        llListen(channel, "[WaS-K] Multi-Destination Teleport", "", "");
        llSetTimerEvent(10);
    }
    listen(integer num, string name, key id, string message) {
        if(message == "DIE") llDie();
        dest = (vector)wasKeyValueGet("destination", message);
        sit = (key)wasKeyValueGet("avatar", message);
        if(dest == ZERO_VECTOR || sit == NULL_KEY) return;
        llSitTarget(<0,0,1>, ZERO_ROTATION);
        llSetClickAction(CLICK_ACTION_SIT);
    }
    changed(integer change) {
        if(change & CHANGED_LINK) {
            key a = llAvatarOnSitTarget();
            if(a) if(a == sit) {
                llWhisper(channel, "OK");
                llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_TEMP_ON_REZ, TRUE]);
                state teleport;
            }
            llInstantMessage(a, "Sorry, the teleporter is currently in use by: " + llList2String(llParseString2List(llKey2Name(sit), [" "], []), 0));
            llUnSit(a);
            return;
        }
    }
    timer() {
        llDie();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state teleport {
    state_entry() {
        // set timeout
        llSetTimerEvent(10);
        // clear sit position and action
        llSetRegionPos(dest);
        // request to set camera
        llRequestPermissions(sit, PERMISSION_CONTROL_CAMERA);
    }
    run_time_permissions(integer perm) {
        // set the camera so the viewer follows the agent
        // this works if the viewer is not zoomed-in on
        // the teleport pad
        if(perm & PERMISSION_CONTROL_CAMERA) {
            llSetCameraParams([
                CAMERA_ACTIVE, 1,
                CAMERA_BEHINDNESS_ANGLE, 45.0,
                CAMERA_BEHINDNESS_LAG, 0.5,
                CAMERA_DISTANCE, 8.0,
                CAMERA_FOCUS_LAG, 0.05 ,
                CAMERA_FOCUS_LOCKED, TRUE,
                CAMERA_FOCUS_THRESHOLD, 0.0,
                CAMERA_PITCH, 20.0,
                CAMERA_POSITION_LAG, 0.1,
                CAMERA_POSITION_LOCKED, TRUE,
                CAMERA_POSITION_THRESHOLD, 0.0,
                CAMERA_FOCUS_OFFSET, <3,0,2>
            ]);
            llReleaseCamera(sit);
        }
        llSetTimerEvent(0.05);
    }
    timer() {
        key a = llAvatarOnSitTarget();
        if(a) llUnSit(a);
        llDie();
    }
    on_rez(integer num) {
        llResetScript();
    }
}

It also deletes itself after an elapsed period of time in order to prevent littering the simulator. In case you rez the dish, you have to stop the scripts inside before they delete the dish.

Other Variants

One problem with precomputing jump gates is that the script may run out of memory. One could implement a check to test the available memory and then recompute the jump gates. However, that is ugly because the memory threshold may vary between implementations. The safe way is to not use a list at all and for that you can use the Safe Teleport variant.

Name Description
region teleport teleporter that uses llSetRegionPos
safe teleport this is a pre-llSetRegionPos script, and should not be used
unsafe teleport this is a pre-llSetRegionPos script, and should not be used

secondlife/multiple_destination_teleport.txt · Last modified: 2017/02/22 18:30 (external edit)

Access website using Tor


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