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. The channel number must be positive, since the viewer can't talk on negative channel numbers. It's recommended to exclude channels < 1000 or so, to avoid highly used channel numbers.
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(2147482646.0) + 1000.0); // 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); } } }
General Functionality
This is the proposed functionality that would be implemented by the main patch itself, and supported by all viewers that speak this protocol. The idea is to provide some simple and generally useful functionality. All of them would use COMPONENT "DaleGlass.Viewer".
GetViewerVersion
This command would return the viewer's version as a string, with the following format:
<major>$<minor>$<revision>$<build>
For example, viewer version 1.17.0 (12) would be sent as:
1$17$0$12
GetViewerBuilder
Returns the name of the person who built the viewer. "Linden Lab" would be reserved for LL. My viewer would say "Dale Glass" here for instance.
GetViewerRevision
Returns the viewer's revision number. This would apply only to custom built viewers. The recommendation is that the reply to this message be the revision number from the builder's source repository.
GetExtensionList
Returns the list of supported viewer extensions, as a comma separated string. MUST NOT contain anything that'd conflict with llCSV2List.
Example reply:
DaleGlass.Viewer, DaleGlass.AvatarScanner, ExampleAvatar.CoolScript
The intention behind this command is allowing scripts that are capable of using multiple extensions determine which are present.
GetOperatingSystem
Returns a string describing the OS being used:
- Linux
- Windows
- OS X
- Solaris
- (etc)
The intention is to allow scripts to determine that some functionality isn't available because the implementation is OS-specific. This MUST NOT be used as a primary method of detecting whether some functionality is supported (as this could change). Instead, this should only be used after GetExtensionList fails to return the desired functionality, and used to determine whether the lack of it is because it's not available on that OS at all.
See Also
See also: Q re client app current/ obsolete/ Mac/ Linux/ Windows/ LSL-Editor/ what at LlRequestClientData