Don't click here unless you want to be banned.

LSL Wiki : LibraryChatCodec

HomePage :: PageIndex :: RecentChanges :: RecentlyCommented :: UserSettings :: You are crawl814.us.archive.org

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();
            }
        }
    }
}
Comments [Hide comments/form]
w00t! Here's an example of one of the modules I used to develop the SLPL content browsing system. In order to do so, I decided to strive for modularity - scripts are seperated into self-contained "modules", each with its own particular task to perform. In order to communicate with other modules in the same primitive, each exposes an interface, declared in the link_message event. The ChatCodec script above is one of those modules.

I <3 it because its something that I can almost instantly reuse in other projects. What kinda irks me is how much code "scaffolding" I have to infect every script with in order for each to play nice with the rest. (Im referring to those "For method invocation" functions.)

I'm not 100% sure its the best approach. And NO, this has absolutely nothing to do with April Fools day :)
-- ChristopherOmega (2006-03-31 23:07:22)
Very nice. I've been toying with how to do this for a while, but now I have no need to.
-- DolusNaumova (2006-04-01 07:53:37)
Argh. Strife (BW) just notifyed me of an incredible problem with this. The protocol doesn't handle non-ASCII strings decently, since the chat functions (llSay/llWhisper/etc) have a 255 byte limit, not 255 character limit. When non-ASCII is used, it counts as two (I think) characters. A fix is on my todo list, but my free time just dried up :-(
-- ChristopherOmega (2006-04-02 21:16:02)
UTF-8 characters can range from 1 to 6 bytes per character depending where the character is in the Unicode range.
-- BlindWanderer (2006-04-06 17:10:46)
For now, you could strip out >1 byte characters.
-- DolusNaumova (2006-04-21 18:06:20)
Attach a comment to this page: