My favorites | Sign in
Project Home Wiki Issues
Search
for
CollarSubsystems  
Explanation of OpenCollar's core systems and how to use them. Oriented towards those who want to create their own OpenCollar plugins.
Type-Dev
Updated Jan 13, 2011 by masteroutside@gmail.com

Introduction

This page describes the major subsystems used in OpenCollar. This is not a list of plugins, but rather an explanation of the conventions used by several plugins together when communicating via link messages. Individual, self-contained plugins may be described on their own pages. All of the link message functions and events described below depend on the scripts having a set of common constants used to differentiate types of messages. You can copy this "Message Map" from any other OpenCollar script and paste it into the top of your own.

Auth

The auth system is responsible for determining whether a command is coming from an owner, secowner, group member, the collar wearer, or someone else.

When the listen script hears a command that starts with the wearer's specific prefix, it sends a link message that essentially means "I heard someone give a command, but I don't know if they're authorized." If someone near Nandana Singh said "nsmenu", for example, the listen script would respond by sending this link message:

llMessageLinked(LINK_SET, COMMAND_NOAUTH, "menu", avkey);

Where "avkey" is the key of the avatar that said "nsmenu". The auth script looks for these COMMAND_NOAUTH messages, and when it gets one, it compares the avkey with the keys it has saved for owner, secowners, and the wearer. It then re-sends the command, but this time it includes information about that person's level of authority. If the person who said "nsmenu" were Nandana's owner, then auth would send the following link message:

llMessageLinked(LINK_SET, COMMAND_OWNER, "menu", avkey);

Any other plugin may now react to this message, knowing that the person who gave the command is the collar wearer's owner. Other possibilities for authority level are COMMAND_SECOWNER, COMMAND_WEARER, and COMMAND_GROUP.

Group level auth is tricky, because of limitations on the ability to detect active groups in LSL scripts. Also, group level authentication is used when the collar's "open access" feature is enabled. COMMAND_GROUP is the auth level when:

  • the collar's "open access" feature is enabled, and the person giving the command is not owner, secowner, or wearer.
  • OR
    • there is a group recorded in the wearer's settings,
    • AND the wearer had that group active when attaching the collar,
    • AND the person giving the command has that group active
  • OR
    • the command came from an object which is set to the same group recorded in the wearer's settings. (The collar wearer need not have the group active, or even be a member of that group.)

Menu

Menu Overview

OpenCollar's plugin-based design requires that each plugin generally provide its own menus. However, there also has to be a way for a plugin to put a single button in a parent menu, in order to trigger the plugin's menu. OpenCollar uses a system of link messages to accomplish this task:

  • requests ("please tell me if you have any buttons to put in the Appearance menu")
  • responses ("I have a button named 'Colors' that should go in the Appearance menu")
  • removal ("please remove my button named 'Colors' from the Appearance menu")
  • triggers ("someone just clicked 'Colors').

Each plugin that provides a menu should define two strings: the parent menu name and its own menu name. For example, the colors plugin could do this:

string parentmenu = "Appearance";
string mymenu = "Colors";

Not every menu is set up to register buttons from other plugins in this way. As of collar version 3.101, valid values for 'parentmenu' are: "Main", "Appearance", "Help/Debug", "RLV", and "Un/Dress". Please do NOT use "Main" as 'parentmenu' in your new plugins. As of 3.300 there is an "AddOns" menu where AddOns may be added unless they fit into another submenu better.

Request Messages

A MENUNAME_REQUEST message can be sent by the script that controls your parent menu. The parent menu name would be in the string field, like this:

llMessageLinked(LINK_SET, MENUNAME_REQUEST, "Appearance", NULL_KEY);

Remember that message is being sent by the parent menu, not your plugin. The plugin just needs to respond to it with a MENUNAME_RESPONSE message, like this:

    link_message(integer sender, integer num, string str, key id)
    {
        if (num == MENUNAME_REQUEST)
        {
            if (str == parentmenu)
            {
                llMessageLinked(LINK_SET, MENUNAME_RESPONSE, parentmenu + "|" + mymenu, NULL_KEY);
            }
        }
    }

Response Messages

In order to make the Colors button show in the Appearance menu, the colors plugin should send a MENUNAME_RESPONSE link message with both the parent and child menu names in the string field, joined with a pipe ("|") character:

llMessageLinked(LINK_SET, MENUNAME_RESPONSE, parentmenu + "|" + mymenu, NULL_KEY);

If creating a plugin with a menu, you should send a MENUNAME_RESPONSE message like this every time the collar is rezzed. It's a good idea to wait one second first, to make sure that the parent menu script is ready to receive your message. Example:

default
{
   on_rez(integer param)
   {
      llResetScript();
   }

   state_entry()
   {
      llSleep(1.0);
      llMessageLinked(LINK_SET, MENUNAME_RESPONSE, parentmenu + "|" + mymenu, NULL_KEY);      
   }
}

Remove Messages

Now, perhaps your plugin needs to remove its button, because its state has changed for some reason. You can send a message like this:

llMessageLinked(LINK_SET, MENUNAME_REMOVE, parentmenu + "|" mymenu, NULL_KEY);

The parent menu will respond by removing the specified button from its menu.

Menu Trigger

Next, how do you know when someone has clicked your button in the parent menu? The parent menu will send a message like this:

llMessageLinked(LINK_SET, SUBMENU, "Colors", id);

Where "id" is the key of the person who clicked the button. You can respond to that in your plugin script like this:

    link_message(integer sender, integer num, string str, key id)
    {
        if (num == SUBMENU)
        {
            if (str == mymenu)
            {
                //someone clicked for my menu!
                DoMenu(id);
            }
        }
    }

Returning to Parent Menu

Finally, what happens when someone wants to leave your menu and go back to the parent? You send this link message:

llMessageLinked(LINK_SET, SUBMENU, parentmenu, id);

Dialog

As of version 3.400, OpenCollar contains a dialog helper script for plugins to use instead of calling llDialog themselves. The helper handles all of the details about timeouts, cleaning up listeners, and doing multi page menus, so plugin writers do not need to worry about any of that. You may be thinking "Wait, you just talked about the menu system, isn't a dialog the same thing?" Not quite. The menu system described above is a way to define menu structure, but it doesn't say anything about how you call llDialog(). That's where the dialog helper comes in. It provides some very important advantages over doing llDialog() yourself:

  1. You don't need to worry about randomizing channels to avoid crosstalk between dialogs.
  2. You don't need to worry about calling llListenRemove() in your script. (Most dialog scripts have to call this in three places: a timer event, the listen event, and in the menu function itself in case someone clicks for two menus right in a row.)
  3. You don't need to have a listen event in your script.
  4. You don't need a timer event in your script.
  5. You don't need to write your own code for handling multi page menus (like for all the anims in an object).
  6. If we ever need to make a change to how dialogs are handled, we need to do it only one place instead of 20+.

To use the dialog helper, your script needs these three message map lines:

integer DIALOG = -9000;
integer DIALOG_RESPONSE = -9001;
integer DIALOG_TIMEOUT = -9002;

...and also this function:

key ShortKey()
{//just pick 8 random hex digits and pad the rest with 0.  Good enough for dialog uniqueness.
    string chars = "0123456789abcdef";
    integer length = 16;
    string out;
    integer n;
    for (n = 0; n < 8; n++)
    {
        integer index = (integer)llFrand(16);//yes this is correct; an integer cast rounds towards 0.  See the llFrand wiki entry.
        out += llGetSubString(chars, index, index);
    }
     
    return (key)(out + "-0000-0000-0000-000000000000");
}

key Dialog(key rcpt, string prompt, list choices, list utilitybuttons, integer page)
{
    key id = ShortKey();
    llMessageLinked(LINK_SET, DIALOG, (string)rcpt + "|" + prompt + "|" + (string)page +
 "|" + llDumpList2String(choices, "`") + "|" + llDumpList2String(utilitybuttons, "`"), id);
    return id;
}

Sending a dialog

Now instead of calling llDialog(), you call the Dialog() function above. It takes 5 arguments:

  1. rcpt: The key of the avatar to whom you would like to give the dialog
  2. prompt: The menu prompt that will be shown to the user. (A warning about the dialog timeout will be automatically added, so don't worry about including that here.)
  3. choices: the list of options from which the user can choose
  4. utilitybuttons: a list buttons that you would like to have displayed on every page of the menu. This usually includes UPMENU ("^"). (But you don't need to add a MORE (">") button. The dialog helper will take care of that if necessary.)
  5. page: the page number of the dialog to show, if there are enough buttons to span more than one page. This is useful if you want to process a response and then give back the same page of the menu that the user was just looking at. If you have only one page's worth of buttons, just put 0 here.

The Dialog() function will return a key when you call it. You should store this key in a global variable so that you can filter out other dialog responses and just respond to yours when it shows up in the link_message event. Here's an example:

key menuid;
string UPMENU = "^";
//... then a bunch of other lines
menuid = Dialog(avkey, "What is your favorite color?", ["Red", "Yellow", "Blue"], [UPMENU], 0);

Receiving the response from the dialog

Then in the link_message event you would handle the response like this:

link_message(integer sender, integer num, string str, key id)
{
    if (num == DIALOG_RESPONSE)
    {
        if (id == menuid)
        {
            //str will be a 3-element, pipe-delimited list in form avkey|selection|pagenum
            list menuparams = llParseString2List(str, ["|"], []);
            key av = (key)llList2String(menuparams, 0);
            string response = llList2String(menuparams, 1);
            integer page = (integer)llList2String(menuparams, 2);
            llInstantMessage(av, "Your favorite color is " + response);
        }
    }
}

Dialog Timeout

It's possible that the user will click "Ignore" on the menu, or not answer within the timeout. In this case, the dialog helper will send a DIALOG_TIMEOUT message, as shown here:

link_message(integer sender, integer num, string str, key id)
{
    //...some stuff
    else if (num == DIALOG_TIMEOUT)
    {
        if (id == menuid)
        {
            llInstantMessage(avkey, "Your menu timed out!");
        }
    }
}

If you would like to provide multiple menus to different users simultaneously, the dialog system allows you to do so. You just need to store their av keys and dialog ids in a global list instead of using a single "menuid" global variable. (And clean that list up by removing the appropriate strides from your list when you get DIALOG_RESPONSE and DIALOG_TIMEOUT messages.)

Note: Because of the way that the dialog system packs lists into a string for sending in a link message, you should avoid having the pipe ("|") or backtick ("`") characters in your prompt or buttons. These are used internally as delimiters, and your menu will come out weird if you use them.

Httpdb

One of OpenCollar's great strengths is its ability to store settings in an external database. This means that when a user updates her collar scripts or changes to a different collar design, her settings are remembered. Collar plugins can take advantage of this by sending link messages to be processed by the httpdb script, and by watching for messages that the httpdb script returns.

Saving a setting

To save a value to the database, send a HTTPDB_SAVE message, like this:

llMessageLinked(LINK_SET, HTTPDB_SAVE, "favoritecolor=blue", NULL_KEY);

As in the example above, the contents of the link message's string field must be in the format "token=value". If the user has no "favoritecolor" setting, one will be created and set to "blue". If one already exists, its value will be set to "blue".

Receiving a setting

When the collar starts up, the httpdb script requests all of the collar wearer's settings from the database, and sends each out as a HTTPDB_RESPONSE message. You should watch for such messages in the link message event in your plugin:

    link_message(integer sender, integer num, string str, key id)
    {
        if (num == HTTPDB_RESPONSE)
        {//parse the string on the "=" character
            list tokenvalue = llParseString2List(str, ["="], []);
            if (llList2String(tokenvalue, 0) == "favoritecolor")
            {
                favcolor = llList2String(tokenvalue, 1);
            }
        }
    }

Requesting a setting

Plugins may also request that the httpdb script send a particular value. This is done with a HTTPDB_REQUEST message:

llMessageLinked(LINK_SET, HTTPDB_REQUEST, "favoritecolor", NULL_KEY);

The httpdb script will receive this message and check for a "favoritecolor" token in its list. If it finds one, it will send it in a HTTPDB_RESPONSE message. If it can't find one, it will send a HTTPDB_EMPTY message. DO NOT send a HTTPDB_REQUEST message when the script first starts up. These unnecessary messages create a flood that sometimes results in other messages being dropped. The httpdb script will send all settings on rez, so just let your plugin wait patiently.

Knowing when a setting is not set

The script will send settings=sent as an HTTPDB_RESPONSE message when it has sent everything so if your script needs more than one token an they are not always set use this to know when they all have been sent.

Deleting a setting

If you no longer want to store a token/value pair in the database, you can send a HTTPDB_DELETE message:

llMessageLinked(LINK_SET, HTTPDB_DELETE, "favoritecolor", NULL_KEY);

Animation

In order to prevent multiple plugins from needing to request animation permissions, and also to prevent conflicting anims from playing simultaneously (nadu while hugging, for example), OpenCollar uses the anim/pose plugin to handle all animations. Other plugins may start and stop animations with ANIM_START and ANIM_STOP link messages.

Starting an Animation

To play an animation, do this:

llMessageLinked(LINK_SET, ANIM_START, "nadu", NULL_KEY);

Stopping an Animation

The anim/pose plugin will process your link message and play the animation. To stop the animation, do this:

llMessageLinked(LINK_SET, ANIM_STOP, "nadu", NULL_KEY);

The anim/pose script keeps a list of all animations that it has been asked to play. It treats this list as a stack, with the last animation on top, and only that animation playing. So if another plugin has already started the "tower" animation when you request that anim/pose start "nadu", then "tower" will be stopped, "nadu" will be started", and "nadu" will be put on top of the stack, with "tower" just underneath. When your plugin sends a message to stop the "nadu" anim, then it will be pulled off the stack, "tower" will be on top again, and "tower" will be played.

RLV

Setting a restriction

OpenCollar may be used both by people running the regular Second Life viewer and those using the Restrained Life viewer (though many features are only available to Retrained Life users). Because Restrained Life viewer (RLV) commands are sent to the viewer through LSL's llOwnerSay function, they would cause annoying spam if sent to people not running RLV. Therefore, OpenCollar's rlvmain script uses a series of steps to determine whether the user is running RLV or not (this is a lot trickier than it seems at first). All other plugins that need to send RLV commands can just send them to rlvmain, which will only send them on if RLV is enabled. To send an rlvcommand from your plugin, use a RLV_CMD link message, like this:

llMessageLinked(LINK_SET, RLV_CMD, "detach=n", NULL_KEY);

Supported commands are the same as those described in the Restrained Life API, except that the "@" character should not be used to prefix RLV_CMD messages as it is in a plain llOwnerSay. rlvmain will add the "@".

When the collar is rezzed, rlvmain will do its RLV version detection, and then send a RLV_VERSION message if RLV is detected. Your plugin can watch for an RLV_VERSION message and react appropriately to if your plugin needs a certain minimum RLV version to function.

Clearing restriction

When an authorized user clicks "Clear All" in the rlvmain menu, rlvmain will send the "@clear" message that removes all RLV restrictions imposed by the collar. rlvmain will also send a RLV_CLEAR message to let plugins know that all restrictions have been cleared, so that the plugins can reset their settings.

Having more than one script setting restriction

Sometimes one plugin may change an RLV setting set by another (suppose the TP plugin restricts teleporting, but then the relay re-enables it). In order to correct for this, an RLV_REFRESH message may be sent by rlvmain. Your plugin should keep a list of all the rlv settings it currently has active, and respond to RLV_REFRESH messages by re-sending all restrictions in this list through one or more RLV_CMD messages.


Sign in to add a comment
Powered by Google Project Hosting