16 November 2015
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.
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 takes care of rezzing the teleport dish as well as showing the menu.
/////////////////////////////////////////////////////////////////////////// // 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(); } }
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 telepoort dish contains a script that is responsible for transporting the avatar to the destination chosen by the base script.
/////////////////////////////////////////////////////////////////////////// // 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.
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 |