ChatCodec
This script module was created to extend LSL's
chat functionality - in particular, to allow for the efficiant communication of text
strings over 255 characters long. It does this by implementing the
ExchangePacketChat protocol.
// Copyright (c) 2006 Francisco V. Saldana (Christopher Omega)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Handles object-object chat.
// Modules register a listen using addChatHandle(integer channel)
// which returns a boolean indicating if the handle was successfully registered.
// Modules unregister a listen using removeChatHandle(integer channel),
// channels are automatically unregistered when a module calls moduleReset.
// The receivedChatData(integer channel, string name, key id, string message)
// method is invoked when chat is heard on a registered channel.
// This module's chat method encodes arbitrary-length chat data in such a way
// that it can be fully reconstructed by objects using the same protocol
// on the receiving end.
// When the module receives the first packet of a message, time to wait (in seconds)
// for the next packet before it clears all in-transit message data.
integer MAX_SEND_WINDOW = 10;
// ========== For method invocation ==========
string randomSeperator(integer len) {
integer firstChar = (integer)llFrand(60) + 20; // Range of printable chars = 0x20 to 0x7E
if (len <= 1)
return llUnescapeURL("%"+(string)firstChar);
integer lastChar;
do { // Last char must not equal first char.
lastChar = (integer)llFrand(60) + 20;
} while (lastChar == firstChar);
string ret = llUnescapeURL("%"+(string)firstChar);
for (len -= 2; len > 0; --len)
ret += llUnescapeURL("%" + (string)((integer)llFrand(60) + 20));
return ret + llUnescapeURL("%"+(string)lastChar);
}
string listToString(list src) {
string chars = (string) src; // Squashes all elements together.
string seperator;
do { // Find a seperator that's not in the list's string form
seperator = randomSeperator(3); // so we dont kill data.
} while (llSubStringIndex(chars, seperator) != -1);
return seperator + llDumpList2String(src, seperator);
}
list stringToList(string src) { // First 3 chars is seperator.
return llParseStringKeepNulls(llDeleteSubString(src, 0, 2),
[llGetSubString(src, 0, 2)], []);
}
callMethod(integer identifyer, string methodName, list parameters) {
llMessageLinked(LINK_THIS, identifyer, // ID only necessary for return value.
listToString(parameters), methodName);
}
returnValue(integer identifyer, string methodName, list value) {
callMethod(identifyer, methodName + "_ret", value);
}
// =============================================
m_receivedChatData(integer channel, string name, key id, string message) {
callMethod(0, "receivedChatData", [channel, name, id, message]);
}
m_moduleReset(string moduleName) {
callMethod(0, "moduleReset", [moduleName]);
}
list listReplaceList(list dest, list src, integer start) {
string NULL = "";
integer len = llGetListLength(dest);
for (len = llGetListLength(dest); len < start; ++len) {
dest += NULL;
}
return llListReplaceList(dest, src, start, start + llGetListLength(src) - 1);
}
list getModulesChannel(integer channel) {
integer chanIndex = llListFindList(listenChannels, [channel]);
if (chanIndex != LIST_INDEX_NOT_FOUND) {
string modulesRegistered = llList2String(channelUsers, chanIndex);
list moduleList = stringToList(modulesRegistered);
return moduleList;
} else {
return [];
}
}
integer addModuleChannel(string moduleName, integer channel) {
integer chanIndex = llListFindList(listenChannels, [channel]);
if (chanIndex != LIST_INDEX_NOT_FOUND) {
list moduleList = getModulesChannel(channel);
integer moduleIndex = llListFindList(moduleList, [moduleName]);
if (moduleIndex == LIST_INDEX_NOT_FOUND) {
moduleList += moduleName;
channelUsers = listReplaceList(channelUsers,
[listToString(moduleList)], chanIndex);
return TRUE;
}
}
return FALSE;
}
integer removeModuleChannel(string moduleName, integer channel) {
integer chanIndex = llListFindList(listenChannels, [channel]);
if (chanIndex != LIST_INDEX_NOT_FOUND) {
list moduleList = getModulesChannel(channel);
integer moduleIndex = llListFindList(moduleList, [moduleName]);
if (moduleIndex != LIST_INDEX_NOT_FOUND) {
moduleList = llDeleteSubList(moduleList, moduleIndex, moduleIndex);
// This avoids problems with storing an empty string.
if (moduleList != []) {
channelUsers = listReplaceList(channelUsers,
[listToString(moduleList)], chanIndex);
} else {
// The module list is empty, so just delete the data at the index.
channelUsers = llDeleteSubList(channelUsers, chanIndex, chanIndex);
}
return TRUE;
}
}
return FALSE;
}
integer addChannelUser(string moduleName, integer channel) {
integer channelIndex = llListFindList(listenChannels, [channel]);
// If no one us already listening on the channel:
if (channelIndex == LIST_INDEX_NOT_FOUND) {
if (llGetListLength(listenChannels) < 64) {
listenHandleIDs += llListen(channel, "", NULL_KEY, "");
listenChannels += channel;
channelUsers += moduleName;
return TRUE;
} else {
return FALSE;
}
} else {
// Add a channel user.
addModuleChannel(moduleName, channel);
return TRUE;
}
}
removeChannelUser(string moduleName, integer channel) {
integer channelIndex = llListFindList(listenChannels, [channel]);
// If someone is using the channel
if (channelIndex != LIST_INDEX_NOT_FOUND) {
// Remove one channel user.
integer curChannelUsers = llGetListLength(getModulesChannel(channel));
integer isModuleRemoved = removeModuleChannel(moduleName, channel);
if (isModuleRemoved) {
curChannelUsers--;
// If, now, no one is using the channel:
if (curChannelUsers <= 0) {
// Unregister it.
integer listenHandleID = (integer) llList2String(listenHandleIDs, channelIndex);
llListenRemove(listenHandleID);
// Remove all list data concerning the channel.
listenHandleIDs = llDeleteSubList(listenHandleIDs, channelIndex, channelIndex);
listenChannels = llDeleteSubList(listenChannels, channelIndex, channelIndex);
}
}
} // Else channel never was registered.
}
fireReceivedChatDataEvent(integer channel, string name, key id, string message) {
m_receivedChatData(channel, name, id, message);
}
string intToHex(integer x, integer len) {
string hexc="0123456789ABCDEF";
integer x0 = x & 0xF;
string res = llGetSubString(hexc,x0,x0);
x = (x >> 4) & 0x0FFFFFFF; //otherwise we get infinite loop on negatives.
while (x != 0) {
x0 = x & 0xF;
res = llGetSubString(hexc,x0,x0) + res;
x = x >> 4;
}
for (len -= llStringLength(res); len > 0; --len)
res = "0" + res;
return res;
}
// List of identifyers and the multipart messages that are currently being recieved.
list messagesInTransit;
string this;
list listenHandleIDs = [];
list listenChannels = [];
list channelUsers = [];
integer LIST_INDEX_NOT_FOUND = -1;
default {
state_entry() {
this = llGetScriptName();
m_moduleReset(this);
}
link_message(integer sender, integer id, string parameters, key methodName) {
if (methodName == "addChatHandle") {
list paramList = stringToList(parameters);
// Method signature:
// addChatHandle(string moduleName, integer channel)
string moduleName = llList2String(paramList, 0);
integer channel = (integer) llList2String(paramList, 1);
returnValue(id, methodName, [addChannelUser(moduleName, channel)]);
} else if (methodName == "removeChatHandle") {
list paramList = stringToList(parameters);
// Method signature:
// removeChatHandle(string moduleName, integer channel)
string moduleName = llList2String(paramList, 0);
integer channel = (integer) llList2String(paramList, 1);
removeChannelUser(moduleName, channel);
} else if (methodName == "moduleReset") {
list paramList = stringToList(parameters);
// Method signature:
// moduleReset(string moduleName)
string moduleName = llList2String(paramList, 0);
integer numChannels = llGetListLength(listenChannels);
integer i;
for (i = 0; i < numChannels; ++i) {
removeChannelUser(moduleName, llList2Integer(listenChannels, i));
}
} else if (methodName == "chat") {
// Method signature:
// chat(integer channel, string message)
list paramList = stringToList(parameters);
integer channel = (integer) llList2String(paramList, 0);
string message = llList2String(paramList, 1);
if (channel != 0) {
integer MAX_CHAT_LEN = 255;
integer dataLen = MAX_CHAT_LEN - 8; // Three ints: 2byte ID, 1byte index and 1byte count.
// That's four chars for the ID and 2 each for index and count.
integer packetCount = llCeil(llStringLength(message) / (float)dataLen);
string hexCount = intToHex(packetCount, 2);
string hexID = intToHex((integer)llFrand(0xFFFE) + 1, 4);
integer i;
integer char;
string packetData;
for(i = 0, char = 0; i < packetCount; ++i, char += dataLen) {
// Send the message in 255 character "packets"
// Each packet is in the form:
// <message ID><packet index><packet count><packet data>
packetData = llGetSubString(message, char, char + dataLen - 1);
llShout(channel, hexID + intToHex(i, 2) + hexCount + packetData);
}
}
} else if (methodName == "moduleReady") {
list paramList = stringToList(parameters);
string module = llList2String(paramList, 0);
if (module == this)
returnValue(id, methodName, [TRUE]);
}
}
listen(integer channel, string name, key id, string packet) {
integer messageId = (integer) ("0x" + llGetSubString(packet, 0, 3)); // First 4 chars.
if (messageId != 0) {
integer packetIndex = (integer) ("0x" + llGetSubString(packet, 4, 5)); // Next 2 chars.
integer packetCount = (integer) ("0x" + llGetSubString(packet, 6, 7)); // next 2 chars.
string packetData = llDeleteSubString(packet, 0, 7); // Everything else.
integer identifyerIndex = llListFindList(messagesInTransit, [messageId]);
if (identifyerIndex == -1) {
// New message
messagesInTransit = [messageId, packetData] + messagesInTransit;
identifyerIndex = 0;
} else {
// Message has more parts
string message = llList2String(messagesInTransit, identifyerIndex + 1);
message += packetData;
messagesInTransit = listReplaceList(messagesInTransit, [message], identifyerIndex + 1);
}
if (packetIndex + 1 == packetCount) {
// Message has been fully reconstructed.
string message = llList2String(messagesInTransit, identifyerIndex + 1);
fireReceivedChatDataEvent(channel, name, id, message);
messagesInTransit = llDeleteSubList(messagesInTransit, identifyerIndex, identifyerIndex + 1);
}
} else {
// Not a valid multipart message
fireReceivedChatDataEvent(channel, name, id, packet);
}
llSetTimerEvent(MAX_SEND_WINDOW);
}
timer() {
// A safety measure to account for objects that moved out of chat range
// before sending the last packet of their message.
messagesInTransit = [];
llSetTimerEvent(0);
}
}
To utilize the above script, paste it into a new script, name it "ChatCodec" and drop it into the inventory of an object. Other scripts in the object must declare these functions:
// ========== For method invocation ==========
string randomSeperator(integer len) {
integer firstChar = (integer)llFrand(60) + 20; // Range of printable chars = 0x20 to 0x7E
if (len <= 1)
return llUnescapeURL("%"+(string)firstChar);
integer lastChar;
do { // Last char must not equal first char.
lastChar = (integer)llFrand(60) + 20;
} while (lastChar == firstChar);
string ret = llUnescapeURL("%"+(string)firstChar);
for (len -= 2; len > 0; --len)
ret += llUnescapeURL("%" + (string)((integer)llFrand(60) + 20));
return ret + llUnescapeURL("%"+(string)lastChar);
}
string listToString(list src) {
string chars = (string) src; // Squashes all elements together.
string seperator;
do { // Find a seperator that's not in the list's string form
seperator = randomSeperator(3); // so we dont kill data.
} while (llSubStringIndex(chars, seperator) != -1);
return seperator + llDumpList2String(src, seperator);
}
list stringToList(string src) { // First 3 chars is seperator.
return llParseStringKeepNulls(llDeleteSubString(src, 0, 2),
[llGetSubString(src, 0, 2)], []);
}
callMethod(integer identifyer, string methodName, list parameters) {
llMessageLinked(LINK_THIS, identifyer, // ID only necessary for return value.
listToString(parameters), methodName);
}
returnValue(integer identifyer, string methodName, list value) {
callMethod(identifyer, methodName + "_ret", value);
}
// =============================================
This would be the equivelant for
llListen:
m_addChatHandle(string scriptName, integer channel) {
callMethod(0, "addChatHandle", [scriptName, channel]);
}
This would be the equivelant for
llListenRemove:
m_removeChatHandle(string scriptName, integer channel) {
callMethod(0, "removeChatHandle", [scriptName, channel]);
}
This would be the equivelant for
llWhisper,
llSay, and
llShout:
m_chat(integer channel, string message) {
callMethod(0, "chat", [channel, message]);
}
And the equivelant for the
listen event:
default {
link_message(integer sender, integer callIdent, string params, key methodName) {
if (methodName == "receivedChatData") {
list paramList = stringToList(params);
integer channel = (integer)llList2String(paramList, 0);
string name = llList2String(paramList, 1);
key id = (key)llList2String(paramList, 2);
string message = llList2String(paramList, 3);
// Code that was in listen() goes here.
// ...
}
}
}
By using this module as a wrapper for the LL-provided chat functions, it automatically handles what's needed for the transfer of a 65025-character text string (maximum in theory). The receiving party must also be using this script or a script capable of decoding the message protocol.
A well-formed user of this script module will look similar to this:
// ========== For method invocation ==========
string randomSeperator(integer len) {
integer firstChar = (integer)llFrand(60) + 20; // Range of printable chars = 0x20 to 0x7E
if (len <= 1)
return llUnescapeURL("%"+(string)firstChar);
integer lastChar;
do { // Last char must not equal first char.
lastChar = (integer)llFrand(60) + 20;
} while (lastChar == firstChar);
string ret = llUnescapeURL("%"+(string)firstChar);
for (len -= 2; len > 0; --len)
ret += llUnescapeURL("%" + (string)((integer)llFrand(60) + 20));
return ret + llUnescapeURL("%"+(string)lastChar);
}
string listToString(list src) {
string chars = (string) src; // Squashes all elements together.
string seperator;
do { // Find a seperator that's not in the list's string form
seperator = randomSeperator(3); // so we dont kill data.
} while (llSubStringIndex(chars, seperator) != -1);
return seperator + llDumpList2String(src, seperator);
}
list stringToList(string src) { // First 3 chars is seperator.
return llParseStringKeepNulls(llDeleteSubString(src, 0, 2),
[llGetSubString(src, 0, 2)], []);
}
callMethod(integer identifyer, string methodName, list parameters) {
llMessageLinked(LINK_THIS, identifyer, // ID only necessary for return value.
listToString(parameters), methodName);
}
returnValue(integer identifyer, string methodName, list value) {
callMethod(identifyer, methodName + "_ret", value);
}
// =============================================
m_addChatHandle(string scriptName, integer channel) {
callMethod(0, "addChatHandle", [scriptName, channel]);
}
m_removeChatHandle(string scriptName, integer channel) {
callMethod(0, "removeChatHandle", [scriptName, channel]);
}
m_chat(integer channel, string message) {
callMethod(0, "chat", [channel, message]);
}
m_moduleReset(string scriptName) {
callMethod(0, "moduleReset", [scriptName]);
}
chatInit() {
// Example chat handle.
// llListen(10283, "", NULL_KEY, "");
m_addChatHandle(llGetScriptName(), 10283);
}
default {
state_entry() {
m_moduleReset(llGetScriptName());
chatInit();
}
link_message(integer sender, integer callIdent, string params, key methodName) {
if (methodName == "receivedChatData") {
list paramList = stringToList(params);
integer channel = (integer)llList2String(paramList, 0);
string name = llList2String(paramList, 1);
key id = (key)llList2String(paramList, 2);
string message = llList2String(paramList, 3);
// Code that was in listen() goes here.
// ...
} else if (methodName == "moduleReset") {
list paramList = stringToList(params);
string scriptName = llList2String(paramList, 0);
if (scriptName == "ChatCodec") {
chatInit();
}
}
}
}