Chat Extension Module
De DigiWiki.
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(); } } } }


