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.

Exports

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 2021 - 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) {
    integer i = llGetListLength(data);
    if (i % 2 != 0 || i == 0) return "";
    --i;
    do {
        data = llListInsertList(
            llDeleteSubList(
                data, 
                i-1, 
                i
            ),
            [ llList2String(data, i-1) + "=" + llList2String(data, i) ], 
            i-1
        );
        i -= 2;
    } while(i > 0);
    return llDumpList2String(data, "&");
}
 
///////////////////////////////////////////////////////////////////////////
//    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 = 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: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
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) 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 destinationNames = [];
list safe_destinationNames = [];
list coordinates = [];
 
key sitting = NULL_KEY;
string firstname = "";
integer selected = FALSE;
integer line = 0;
string destinationName = "";
vector destination = ZERO_VECTOR;
integer timeout = 30;
integer dish_channel = 0;
 
default {
    state_entry() {
        // set the overhead text mode
        if(wasKeyValueGet("text", llGetObjectDesc()) == "false") {
            llSetText("", <0, 0, 0>, 0.0);
        }
        // read the destination notecard
        if(llGetInventoryType(llGetInventoryName(INVENTORY_NOTECARD, 0)) != INVENTORY_NOTECARD) {
            llOwnerSay("Failed to find a \"Destinations\" notecard in the teleporter inventory.\nPlease add a notecard containing destinations.");
            return;
        }
        if(wasKeyValueGet("text", llGetObjectDesc()) == "true") {
            llSetText("Reading destinations.", <1, 1, 0>, 1.0);
        }
        llGetNotecardLine(
            llGetInventoryName(INVENTORY_NOTECARD, 0), 
            line
        );
    }
    dataserver(key id, string data) {
        if(data == EOF) {
            if(wasKeyValueGet("text", llGetObjectDesc()) == "true") {
                llSetText("Read destinations.", <1, 0, 0>, 1.0);
            }
            // check for consistency
            if(llGetListLength(destinationNames) != llGetListLength(coordinates)) {
                if(wasKeyValueGet("text", llGetObjectDesc()) == "true") {
                    llSetText("Error reading destinations. Restarting...", <1, 0, 0>, 1.0);
                }
                llSetTimerEvent(1);
                return;
            }
            state select;
            return;
        }
        if(data == "") {
            llGetNotecardLine(
                llGetInventoryName(INVENTORY_NOTECARD, 0), 
                ++line
            );
            return;
        }
        list teleport = llParseString2List(data, ["#"], []);
        destinationNames += llStringTrim(
            llList2String(teleport, 0), 
            STRING_TRIM
        );
        coordinates += (vector)llStringTrim(
            llList2String(teleport, 1),
            STRING_TRIM
        );
        llGetNotecardLine(
            llGetInventoryName(INVENTORY_NOTECARD, 0), 
            ++line
        );
    }
    timer() {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state select {
    state_entry() {
        if(wasKeyValueGet("text", llGetObjectDesc()) == "true") {
            llSetText("Touch me to select a destination.", <0, 1, 0>, 1.0);
        }
 
        dish_channel = (integer)("0x8" + llGetSubString(llGetOwner(), 0, 6));
        llListen(dish_channel, "", "", "");
    }
    touch_start(integer total_numer) {
        key click = llDetectedKey(0);
        // check if the device is locked
        if((wasKeyValueGet("lock", llGetObjectDesc()) == "true" && click != llGetOwner()) ||
            (wasKeyValueGet("group", llGetObjectDesc()) == "true" && llDetectedGroup(0) == FALSE)) {
                return;
        }
        // block if teleporter is in use
        if(sitting != NULL_KEY && click != sitting) {
            return;
        }
        // grab user key and name
        sitting = click;
        firstname = llList2String(
            llParseString2List(
                llDetectedName(0),
                [
                    " "
                ], 
                []
            ),
            0
        );
        // display overhead text and set the countdown
        if(wasKeyValueGet("text", llGetObjectDesc()) == "true") {
            llSetText(firstname + " is using the teleporter [" + (string)timeout + "s].", <1, 1, 0>, 1.0);
        }
        llSetTimerEvent(1);
        // generate the list of destinations and send the dialog
        integer s = llGetListLength(destinationNames);
        integer i = 0;
        safe_destinationNames = [];
        do {
            safe_destinationNames += llGetSubString(
                `llList2String(
                    destinationNames,
                    i
                 ),
                 0,
                 8
            );
        } while(++i < s);
        integer channel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        llListen(channel, "", sitting, "");
        llDialog(
            sitting, 
            "Choose a destination to teleport to from the provided list of destinations or select \"✖︎ Abort\" to cancel the selection.",
            wasDialogMenu(
                safe_destinationNames,
                [
                    "⟵ Back",
                    "◉ Options",
                    "Next ⟶",
                    "",
                    "",
                    "✖︎ Abort"
                ], 
                ""
            ),
            channel
        );
    }
    listen(integer channel, string name, key id, string message) {
        // dish message
        if(message == "OK") {
            llResetScript();
        }
        if(message == "⏏ Exit") {
            llDialog(
                sitting, 
                "Choose a destination to teleport to from the provided list of destinations or select \"✖︎ Abort\" to cancel the selection.", 
                wasDialogMenu(
                    safe_destinationNames, 
                    [
                        "⟵ Back", 
                        "◉ Options", 
                        "Next ⟶", 
                        "", 
                        "", 
                        "✖︎ Abort"
                    ],
                    ""
                ),
                channel
            );
            return;
        }
 
        if(message == "✖︎ Abort") {
            llWhisper(dish_channel, "DIE");
            llResetScript();
        }
        if(message == "⟵ Back") {
            llDialog(
                sitting, 
                "Choose a destination to teleport to from the provided list of destinations or select \"✖︎ Abort\" to cancel the selection.", 
                wasDialogMenu(
                    safe_destinationNames, 
                    [
                        "⟵ Back", 
                        "◉ Options", 
                        "Next ⟶", 
                        "", 
                        "", 
                        "✖︎ Abort"
                    ],
                    "<"
                ),
                channel
            );
            return;
        }
        if(message == "Next ⟶") {
            llDialog(
                sitting, 
                "Choose a destination to teleport to from the provided list of destinations or select \"✖︎ Abort\" to cancel the selection.", 
                wasDialogMenu(
                    safe_destinationNames,
                    [
                        "⟵ Back", 
                        "◉ Options", 
                        "Next ⟶", 
                        "", 
                        "", 
                        "✖︎ Abort"
                    ], 
                    ">"
                ),
                channel
            );
            return;
        }
 
        // restrict the options menu to the owner of the dish
        list options = ["◉ Options", "◯ Owner", "◉ Owner", "◯ Group", "◉ Group", "◯ Text", "◉ Text" ];
        if(id == llGetOwner() && llListFindList(options, [ message ]) != -1) {
 
            if(message == "◯ Owner") {
                llSetObjectDesc(wasKeyValueSet("lock", "true", llGetObjectDesc()));
            }
            if(message == "◉ Owner") {
                llSetObjectDesc(wasKeyValueSet("lock", "false", llGetObjectDesc()));
            }
            if(message == "◯ Group") {
                llSetObjectDesc(wasKeyValueSet("group", "true", llGetObjectDesc()));
            }
            if(message == "◉ Group") {
                llSetObjectDesc(wasKeyValueSet("group", "false", llGetObjectDesc()));
            }
            if(message == "◯ Text") {
                llSetObjectDesc(wasKeyValueSet("text", "true", llGetObjectDesc()));
            }
            if(message == "◉ Text") {
                llSetObjectDesc(wasKeyValueSet("text", "false", llGetObjectDesc()));
            }
 
            options = [ "⏏ Exit" ];
 
            message = "Here you can toggle the teleporter so it is restricted to the owner, group or free for all.\n\n";
 
            if(wasKeyValueGet("group", llGetObjectDesc()) != "false") {
                message += "The teleporter is currently locked to the group.\n";
                options += "◉ Group";
            }
            if(wasKeyValueGet("group", llGetObjectDesc()) != "true") {
                message += "The teleporter is currently not locked to the group.\n";
                options += "◯ Group";
            }
            if(wasKeyValueGet("lock", llGetObjectDesc()) != "false") {
                message += "The teleporter is currently locked to the owner.\n";
                options += "◉ Owner";
            }
            if(wasKeyValueGet("lock", llGetObjectDesc()) != "true") {
                message += "The teleporter is currently not locked to the owner.\n";
                options += "◯ Owner";
            }
            if(wasKeyValueGet("text", llGetObjectDesc()) != "false") {
                message += "The teleporter will display overhead text.\n";
                options += "◉ Text";
            }
            if(wasKeyValueGet("text", llGetObjectDesc()) != "true") {
                message += "The teleporter will not display overhead text.\n";
                options += "◯ Text";
            }
            llDialog(sitting, message, options, channel);
            return;
        }
 
        // remenu on dud messages
        if(llListFindList(safe_destinationNames, (list)message) == -1) {
            llDialog(
                sitting, 
                "Choose a destination to teleport to from the provided list of destinations or select \"✖︎ Abort\" to cancel the selection.", 
                wasDialogMenu(
                    safe_destinationNames, 
                    [
                        "⟵ Back", 
                        "◉ Options", 
                        "Next ⟶", 
                        "", 
                        "", 
                        "✖︎ Abort"
                    ], 
                    ""
                ),
                channel
            );
            return;
        }
 
        integer i = llGetListLength(destinationNames)-1;
        do {
            destinationName = llList2String(destinationNames, i);
            if(llSubStringIndex(destinationName, message) != -1) {
                destination = llList2Vector(coordinates, i);
                selected = TRUE;
                timeout = 10;
                vector position = llGetPos();
                llWhisper(dish_channel, "DIE");
                llRezObject(
                    llGetInventoryName(INVENTORY_OBJECT, 0),
                    <position.x, position.y, position.z + 0.0064>, 
                    ZERO_VECTOR, 
                    ZERO_ROTATION, 
                    0
                );
                llWhisper(
                    dish_channel, 
                    wasKeyValueEncode(
                        [
                            "destination", destination,
                            "avatar", sitting
                        ]
                    )
                );
                return;
            }
        } while(--i>-1);
 
        if(wasKeyValueGet("text", llGetObjectDesc()) == "true") {
            llSetText("Could not find destination.", <1, 0, 0>, 1.0);
        }
 
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
    timer() {
        if(selected == TRUE) {
            if(wasKeyValueGet("text", llGetObjectDesc()) == "true") {
                llSetText(firstname + ", touch to teleport to \"" + destinationName + "\" [" + (string)timeout + "s].", <1, 1, 0>, 1.0);
            }
            llWhisper(
                dish_channel, 
                wasKeyValueEncode(
                    [
                        "destination", destination,
                        "avatar", sitting
                    ]
                )
            );
            if(--timeout == 0) {
                llResetScript();
            }
            return;
        }
        if(wasKeyValueGet("text", llGetObjectDesc()) == "true") {
            llSetText(firstname + " is using the teleporter [" + (string)timeout + "s].", <1, 1, 0>, 1.0);
        }
        if(--timeout == 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 2021 - 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) 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 = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
key sitting = NULL_KEY;
vector destination = ZERO_VECTOR;
integer channel = 0;
 
default {
    state_entry() {
        channel = (integer)("0x8" + llGetSubString(llGetOwner(), 0, 6));
        llListen(channel, "", "", "");
        llSetTimerEvent(10);
    }
    listen(integer num, string name, key id, string message) {
        if(message == "DIE") {
            llDie();
        }
        destination = (vector)wasKeyValueGet("destination", message);
        sitting = (key)wasKeyValueGet("avatar", message);
        if(destination == ZERO_VECTOR || sitting == NULL_KEY)  {
            return;
        }
        state configured;
    }
    timer() {
        llDie();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state configured {
    state_entry() {
        llSitTarget(<0,0,1>, ZERO_ROTATION);
        llSetClickAction(CLICK_ACTION_SIT);
        llSetTimerEvent(10);
    }
    changed(integer change) {
        if(change & CHANGED_LINK) {
            key agent = llAvatarOnSitTarget();
            if(agent == NULL_KEY) {
                llUnSit(agent);
                return;
            }
            if(agent == sitting) {
                llWhisper(channel, "OK");
                state teleport;
            }
            llInstantMessage(agent, 
                "Sorry, the teleporter is currently in use by: " + 
                    llList2String(
                        llParseString2List(
                            llKey2Name(sitting), 
                            [" "], 
                            []
                        ),
                        0
                    )
            );
            llUnSit(agent);
            return;
        }
    }
    timer() {
        llDie();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state teleport {
    state_entry() {
        // set timeout
        llSetTimerEvent(10);
        // clear sit position and action
        llSetRegionPos(destination);
        // request to set camera
        llRequestPermissions(sitting, 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>
            ]);
            llClearCameraParams();
        }
        llSetTimerEvent(0.1);
    }
    timer() {
        key agent = llAvatarOnSitTarget();
        if(agent != NULL_KEY) {
            llUnSit(agent);
        }
        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: 2022/11/24 07:46 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.