LSL To Client Communication
Communication
A mechanism is needed to allow LSL scripts to talk to the viewer in order to allow implementing some interesting functionality, such as viewer UI extensions that communicate with in-world objects, for instance. Viewer to LSL communication is easily possible, by making the viewer talk on a specific channel, or use XML RPC. This may not be ideal for all cases, but it's already doable without any modifications.
What needs work is LSL to Client communication, as there currently isn't any good method in the official client for that.
Requirements
A robust mechanism should satisfy these requirements:
- Scripts must be able to receive messages from the viewer (can be done already with chat and XML RPC)
- Scripts must be able to reply to the viewer
- It must be possible to communicate with multiple viewers at once without confusion
- Order of delivery must be guaranteed (chat messages currently can be reordered)
- Messages must be reliable
The hack below can satisfy requirements #1 to #3. Requirement #4 would require extra work (although it's doable) and is possibly best ignored, as LL mentioned the possibility of moving chat to TCP, which would automatically solve the issue.
Requirement #5 is mostly satisfied as data isn't lost, and the only problem may be with the viewer or the script disappearing, which can be dealt with by designing the proper protocol, if needed.
Hack
While there's no official solution, here's what I came up with for now:
- Client to LSL: Client speaks on a channel directly. Nothing special here.
- LSL to Client: Script uses llOwnerSay, with this format:
$VwrComm$VERSION$COMPONENT$DATA
Fields:
- VwrComm: Header, must be present
- VERSION: Protocol version
- COMPONENT: Component inside the viewer the message is for
- DATA: Data being sent. The format of the data isn't specified.
Behavior
This is how the current implementation handles the protocol:
- Message must come from an object. Normal chat doesn't trigger it
- String gets split by "$" and must have at least 4 tokens
- First token must be VwrComm
- Second token must be "0" (version number, this will change)
- Third token matches the name of a known component.
If requirements #1 to #3 aren't satisfied, the string is passed as-is, and the code doesn't suppress its display.
If requirement #4 isn't satisfied, the text won't appear on screen, but the code won't continue further either. A message will be logged in the viewer's log, saying that a message with the wrong version was received.
If requirement #5 isn't satisfied, display is also suppressed, and a message is also logged.
If all those requirements are satisfied, the data in the message is sent to the specified part of the viewer for futher processing.
Example
$VwrComm$0$DaleGlass.AvatarScanner$LoginComplete
Implementation
Code for this can be found at [1], line 2047
Note that at the time of writing, the client isn't capable of telling the difference between object speech sent to all users (llSay/etc) and object speech sent only to the owner (llOwnerSay). It also doesn't check the speaking object's key or its owner's. This code must be fixed to obtain that information, so that the viewer can ignore messages from objects not owned by the user, when such a thing is necessary.
Protocol
The above describes the system and format for exchanging data. However, there are still a few problems to solve:
- Multiple extensions to the viewer must be able to coexist, without conflicting with others
- Viewer may not implement this at all, in which case the messages get displayed on screen
- Viewer replies to the script have potential security issues
Proposed solutions:
- See next section
- The LSL script initiates the conversation. It sends a single request and waits for a reply. This ensures that non-supporting clients don't get flooded with protocol data.
- During the establishment of the connection, the script chooses a random channel to listen on, and tells it to the viewer. This makes it very improbable that somebody will be able to intercept the data, as the channel will be different every time. An additional advantage is that there's no need to keep track of who uses what channel: the chance of a collision should be extremely low.
Component Naming
The COMPONENT field of the protocol specifies the name of the component inside the viewer the script wants to talk to. The data is an arbitrary ASCII string, which may not contain newlines or the $ character. The proposed format is "AvatarName.FunctionName", where AvatarName is the first and last names of the avatar who created the functionality concatenated without a space, a period, and the name of the avatar's creation. This system should ensure that should two people come up with different ways to do the same thing, the COMPONENT names won't clash.
Example:
$VwrComm$0$DaleGlass.AvatarScanner$LoginComplete
Connection Establishment
As described above, the LSL script initiates the communication by telling the viewer what channel it wants to talk on.
Connection Request
- The COMPONENT field's value is "DaleGlass.Viewer"
- The payload is "Connect$<channel>"
The fields are as follows:
- <channel> is the channel the script wants to talk on. For security, this SHOULD be randomly generated.
Example:
$VwrComm$0$DaleGlass.Viewer$Connect$12421
Viewer Reply
Viewer replies to the request by sending the message "OK" to the specified channel. Any other reply MUST be considered an error message.
After this, any further requests to the viewer from the object are replied to in on the specified channel.
Example
integer g_channel = 0; // Channel to talk on -- randomly generated integer g_listen_handle = 0; // Listen handle integer g_timeout = 30; // How long to wait for the viewer to reply ViewerComm(string component, string data) { llOwnerSay("$VwrComm$0$" + component + "$" + data); } default { state_entry() { // Generate a random channel number g_channel = (integer)(llFRand(4294967295) - 2147483648); // Set a timer in case the viewer doesn't reply llSetTimerEvent(g_timeout); // Listen for replies g_listen_handle = llListen(g_channel, "", llGetOwner(), ""); // Send connection request ViewerComm("DaleGlass.Viewer", "Connect$" + (string)channel); } timer() { llListenRemove(g_listen_handle); llOwnerSay("Client/Viewer connection timed out -- probably not supported"); } listen( integer channel, string name, key id, string message ) { if ( message == "OK" ) { // Connection successful llOwnerSay("Client/Viewer connection successful"); } else { // Something went wrong llOwnerSay("Client/Viewer connection error: " + message); } } }