|
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 IntroductionThis 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. AuthThe 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:
MenuMenu OverviewOpenCollar'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:
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 MessagesA 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 MessagesIn 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 MessagesNow, 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 TriggerNext, 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 MenuFinally, 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); DialogAs 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:
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 dialogNow instead of calling llDialog(), you call the Dialog() function above. It takes 5 arguments:
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 dialogThen 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 TimeoutIt'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. HttpdbOne 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 settingTo 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 settingWhen 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 settingPlugins 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 setThe 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 settingIf 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); AnimationIn 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 AnimationTo play an animation, do this: llMessageLinked(LINK_SET, ANIM_START, "nadu", NULL_KEY); Stopping an AnimationThe 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. RLVSetting a restrictionOpenCollar 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 restrictionWhen 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 restrictionSometimes 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. | |