Touch-Link Transfer Protocol (TLTP for short)
Change Log:
Version 0.33e
Main changes since 0.32a
Support for TLML-L pages, re-added scanned in the background.
Main changes since 0.31
- support for TLML-Light pages
Main changes from 0.25:
- types of data transmitted are now described by the first character sent
- types of data: animation, sound, TLML, URL, XTMP, notecard key, chat request, RPC
- TLTP codes migrated to TLML
- TLTP-RPC
- URL format revised
Main changes from 0.2:
- codes for XTMP
- code for remote animation playing
- code for remote sound playing
- added stub for future TLTP-RPC
Main changes from 0.1:
- both modes of communication are now stateless and connectionless, although the TLTP-over-chat method is still filtered on the server->client way, by channel number
- chat-capable servers now advertise on -9, instead of clients advertising
- new -06 error code for server-initiated redirections
Return to protocol exchange
Description of TLML
Sample server and client code
What is this ?
TLTP is a protocol between a
HUD attachment or stand-alone object acting as a programmable interface and an object either local or remote for exchanging data.
The data sent can be
TLML-formatted prim-pages that can link to another
TLML page, effectively making this a kind of HTTP-for-SL. It can also be chat commands, link messages to send, animation or sounds to play, etc... enabling it to serve as transport layer for anything in SL.
TLTP
The protocol supports three modes for the transfer of the
TLML data:
chat functions or
llEmail functions for a direct transmission of the data, or the
dataserver event for reading the data from a notecard.
The chat way:
Service Discovery:
The server advertises itself on channel -9, saying its default URL (containing the server's channel number, see URL format below). The client listens for this and stores it temporarily.
Browsing
The client then just has to open a listener on a random channel to request a page, by saying an URL composed of the index of the page requested, the chat communication method identifier "0" and the number of the random channel on which to send the page, on the server's channel. This URL can also include the name of the client.
Example:
- Server advertises the URL "|0|-671|Bob's Server|homepage", which stands for "feel free to browse Bob's Server on private channel -671, starting at the prim-page of index 'homepage' ".
- Client says "U|homepage|0|76711|My browser" on channel -671 to request the default page.
- Server then sends the TLML page on channel 76711 so that only the client that requested this page will receive it.
To send the prim-page back, the server simply says the
TLML commands composing this page, on the same client-specified channel.
Example continued:
- Server says "T/0/U!nextpage!1!66864f3c-e095-d9c8-058d-d6575e6ed1b8/8201F/<1,1,0>/<0.25,0.25,0.01>/<0.75,0.75,0.01>" on channel 76711, which is TLML code for a centered opaque yellow rectangle covering half the screen, that links to the URL "U!nextpage!1!66864f3c-e095-d9c8-058d-d6575e6ed1b8" when clicked.
The client then usually extracts the string that trails the starting "T/0" to its first child prim, which interprets it and changes appearance according to the parameters it contains.
The email way:
Service Discovery:
There is no automatic discovery method. However browser scripts should support bookmarks, and chat servers can redirect to email servers.
This leaves some space for developping a DNS-like service.
Browsing:
The client simply sends an
email of subject "TLTP" to the server and message containing an URL composed of the index of the requested page, the communication method identifier "1" and its key (and optionnally its name), and the server replies with a stream of emails of subject "TLTP" and of message containing one or more
TLML command line(s) for this page. Because of the slow speed of this transfer method, it's best to limit it to simple pages of 2 or 3 prims.
URL Format:
All URLs should be strings starting with "U" when sent over TLTP, followed by a TightList composed of:
string | Index of the page |
integer cast as string | Optional. Communication method: 0 for chat, 1 for email This will support more methods in the future. |
key or integer (cast as string) | Optional, set only if previous field is set. Key of the server for email, or server's channel for chat |
string | Optional, set only if previous two fields are set. Name of the server |
Example URLs:
U!bestiary (when passed, the browser will request the page "bestiary" to the last called server)
U!Forward!0!128 (the browser will ask for the page "Forward" on channel 128, this can be used to make remote controllers easily)
U!Store!1!66864f3c-e095-d9c8-058d-d6575e6ed1b8!Jesrad Seraph.The Bestiary (Points at the page "Store" on server The Bestiary owned by Jesrad Seraph, of key 66864f3c-e095-d9c8-058d-d6575e6ed1b8, via email)
URL Type | Method | Format |
Chat | 0 | Index, 0, channel [, server_name]] |
Email | 1 | Index, 1, address [, server_name] |
Joint notecard | 2 | Index, 2 [, line, name] |
The name of the server that can be added to URLs is meant to be used in Bookmarks and for future systematic resolution of names ;)
Data types:
TLTP Servers and clients are not limited to exchanging URLs and TLML data, they can send any of these types of data:
First character | Data | Description |
U | TightList | URL see section above |
T | string | TLML command |
t | notecard_key | key of a notecard containing data This is just a page stored in a notecard, the entire card will be read |
x | string | XTM command |
X | string | XTMP command |
J | TightList([begin, end, offset, (visible-begin, (visible-end))]) | all integers, last 2 optional, can only be called from within a TLML-L page |
C | TightList([(channel,) text]) | channel is optional, if left out, llSay is used otherwise it uses llShout |
c | string | uses llOwnerSay to say string |
s | stops all sounds playing |
S | TightList([sound(, volume)]) | llTriggerSound(sound, volume) sound can be a name (but must be in browsers inventory) or key, volume is optional, defaults to 0.5 |
L | TightList([sound(, volume)]) | llPlaySound(sound, volume) sound can be a name (but must be in browsers inventory) or key, volume is optional, defaults to 0.5 |
l llToLower("L") | TightList([sound(, volume)]) | llLoopSound(sound, volume) sound can be a name (but must be in browsers inventory) or key, volume is optional, defaults to 0.5 |
D | TightList([start(, end)]) | both integers, end is optional, if left out only start is cleared. |
R | TightList([scriptname] + parameters ) | composed of the name of the RPC script, and the necessary parameters |
N | TightList([difference, notecard_texture_key, subgroup]) | difference is how far and direction to move the current position on the viewers that corespond to notecard_texture_key with regaurds to subgroup |
W | float | duration to wait before interpreting further commands Useful for mimicking Flash ;) |
K | no idea | Reserved for keys |
k | no idea | Reserved for keys |
p | integer | Request permissions, discard existing permissions |
P | integer | Request permissions, keep existing permissions |
A | string | name of an animation to play llStartAnimation Use the name for built-in animations |
a | string | name or key of animation to stop llStopAnimation |
M | TightList | TightList of multiple TLTP commands chained together |
Sample Code
TLTP Server:
Works
// TLTP Server
// version 0.33e
// Author: Jesrad Seraph
// Modify and redistribute freely as long as you allow free modification and redistribution
// This example server supports both chat and email transport methods
// The pages are sent as keys for server performance
// Currently, the default page is the first page cached.
// TODO: support crossing the transport methods (listen->email, email->shout)
integer cur_emailer = 0;
integer max_emailer;
string indexname; // contains the index page name
integer channel = 777; // private server channel for accepting requests
integer handle; // listener handle for getting requests
float advert_rate = 5.0; // timerate for announcing the server
string tltp_str = "TLTP"; // saving memory
//string eof = EOF;
key ind;
integer index;
list TLML_L;
// courtesy of Strife Onizuka
list TightListParse(string a)
{
string b = llGetSubString(a,0,0);//save memory
return llParseStringKeepNulls(llDeleteSubString(a,0,0), [a=b],[]);
}
string TightListDump(list a, string b)
{
string c = (string)a;
if(llStringLength(b)==1)
if(llSubStringIndex(c,b) == -1)
jump end;
integer d = -llStringLength(b += "|\\/?!@#$%^&*()_=:;~{}[],\n\" qQxXzZ");
while(1+llSubStringIndex(c,llGetSubString(b,d,d)) && d)
++d;
b = llGetSubString(b,d,d);
@end;
c = "";//save memory
return b + llDumpList2String(a, b);
}
sendEmail(string a, string s, string m)
{
if (max_emailer > 0)
{
llMessageLinked(LINK_THIS, cur_emailer, TightListDump([a + "@lsl.secondlife.com", s, m], "!"), "");
cur_emailer = (cur_emailer + 1) % max_emailer;
} else llEmail(a + "@lsl.secondlife.com", s, m);
}
default
{
state_entry()
{
if (llGetInventoryNumber(INVENTORY_NOTECARD) <= 0)
{
llOwnerSay("No pages found.");
} else state ready;
}
changed(integer c)
{
if (c & CHANGED_INVENTORY) { llSleep(1.0); llResetScript(); }
}
}
state ready
{
state_entry()
{
index = llGetListLength(TLML_L);
while(index)
if(llGetInventoryType(llList2String(TLML_L, --index)) == INVENTORY_NONE)
TLML_L = llDeleteSubList(TLML_L,index,index);
indexname = llGetInventoryName(INVENTORY_NOTECARD, index = 0);
integer a = llGetInventoryNumber(INVENTORY_NOTECARD);
while(index < a)
if(llListFindList(TLML_L, [llGetInventoryName(INVENTORY_NOTECARD, index)]) + 1)
++index;
else
{
ind = llGetNotecardLine(llGetInventoryName(INVENTORY_NOTECARD, index), 0);
a = index;
}
max_emailer = llGetInventoryNumber(INVENTORY_SCRIPT) - 1;
handle = llListen(channel, "", "", "");
llOwnerSay("Server ready on channel " + (string)channel);
llSetTimerEvent(advert_rate);
llOwnerSay((string)llGetKey() + "@lsl.secondlife.com");
if (llGetObjectDesc() == "") llSetObjectDesc("Server");
}
dataserver(key queryid, string data)
{
if(ind == queryid)
{
if(llList2String(TightListParse(data),-1) == "TLML-L")
if(llListFindList(TLML_L, [data = llGetInventoryName(INVENTORY_NOTECARD, index)]) == -1)
TLML_L += data;
if(++index < llGetInventoryNumber(INVENTORY_NOTECARD))
ind = llGetNotecardLine(llGetInventoryName(INVENTORY_NOTECARD, index), 0);
}
}
changed(integer c)
{
if (c & CHANGED_INVENTORY) { llSleep(1.0); state default; }
}
timer()
{
llShout(-9, "U" + TightListDump([indexname, 0, channel, llGetObjectDesc()], "#") );
llGetNextEmail("", "");
}
listen(integer ch, string n, key id, string msg)
{
string a = llGetSubString(msg, 0, 0);
if (a != "U") return;
list info = TightListParse(llDeleteSubString(msg, 0, 0));
if (llGetListLength(info) < 3) return; // simple sanity check
string req = llList2String(info, 0);
key thepage = llGetInventoryKey(req);
string m = llList2String(info, 1);
integer c = (integer)llList2String(info, 2);
if (m != "0") return; // not supporting cross transport methods right now
if (thepage == NULL_KEY)
{
llShout(c, "T" + TightListDump(["-4", indexname], "!"));
} else {
integer type = llGetInventoryType(req);
if (type + 1)
{
if (type == INVENTORY_NOTECARD) {
if(llListFindList(TLML_L, [req]) + 1)
req = "U" + TightListDump([(string)thepage, 2, 0, llGetKey()], "!");
else
req = "t" + (string)thepage;
llShout(c, req);
} else
if (type == INVENTORY_ANIMATION) { llShout(c, "A" + (string)thepage); } else
if (type == INVENTORY_SOUND) { llShout(c, "S" + TightListDump([(string)thepage, "1.0"], "!")); } else
if (type == INVENTORY_SCRIPT) { llShout(c, "T" + TightListDump(["-4", indexname], "!")); } else
{
llShout(c, "cDownloading " + req);
if((n = llGetOwnerKey(id)) != id)
llGiveInventory(n, req);
else
{
llShout(c, "cCannot resolve your key ! Please come to sim " + llGetRegionName());
}
}
}
else
{
llShout(c, "T" + TightListDump(["-4", indexname], "!"));
}
}
}
email(string time, string address, string subj, string message, integer left)
{
if (subj == tltp_str)
{
integer start = llSubStringIndex(message, "\n\n") + 2;
string a = llGetSubString(message, start, start);
if (a != "U") return;
list info = TightListParse(llDeleteSubString(message, 0, start));
string req = llList2String(info, 0);
key thepage = llGetInventoryKey(req);
string m = llList2String(info, 1);
if (m != "1") return; // not supporting cross transport methods right now
if (thepage == NULL_KEY)
{
} else {
integer type = llGetInventoryType(req);
if (type + 1)
{
// page is not a notecard
if (type == INVENTORY_NOTECARD) {
if(llListFindList(TLML_L, [req]) + 1)
req = "U" + TightListDump([(string)thepage, 2, 0, llGetKey()], "!");
else
req = "t" + (string)thepage;
sendEmail(llList2String(info, 2), tltp_str, req); } else
if (type == INVENTORY_ANIMATION) { sendEmail(llList2String(info, 2), tltp_str, "A" + indexname); } else
if (type == INVENTORY_SOUND) { sendEmail(llList2String(info, 2), tltp_str, "S" + TightListDump([thepage, "1.0"], "!")); } else
if (type == INVENTORY_SCRIPT) { sendEmail(llList2String(info, 2), tltp_str, "T" + TightListDump(["-4", indexname], "!")); } else
{
sendEmail(llList2String(info, 2), tltp_str, "cDownloading " + req);
string id = llGetOwnerKey(llGetSubString(message, 0, 35));
if (id != llGetSubString(message, 0, 35)) {
llGiveInventory(id, req);
} else sendEmail(llList2String(info, 2), tltp_str, "cCannot resolve your key ! Please come to sim " + llGetRegionName());
}
}
else
{
sendEmail(llList2String(info, 2), tltp_str, "T!-4");
}
}
} else llOwnerSay("Unknown email received: " + subj + ": " + message);
}
}
Sample RPC plugin (call it MoveTo, use by sending R!MoveTo!<vector>[!time] to a browser that contains this script):
// MoveTo TLTP Plugin
// Sample plugin for RPC demonstration
integer rpc_code = 4400;
list TightListParse(string a)
{
string b = llGetSubString(a,0,0);//save memory
return llParseStringKeepNulls(llDeleteSubString(a,0,0), [a=b],[]);
}
default
{
link_message(integer part, integer code, string msg, key id)
{
if (code != rpc_code) return;
if (msg != llGetScriptName()) return;
list l = TightListParse((string)id);
float delta;
if (llGetListLength(l) > 1) { delta = (float)llList2String(l, 1); } else
delta = 0.5;
vector target = (vector)llList2String(l, 0);
if (target == ZERO_VECTOR)
{
llStopMoveToTarget();
} else llMoveToTarget(target, delta);
}
}