Philip P. Ide

Author, programmer, science enthusiast, half-wit.
Life is sweet. Have you tasted it lately?

User Tools

Site Tools


blog:opensim:ircbridge_network

IRCBridgeModule Networking

Example application of objects in different regions communicating

Opensimulator has a module called IRCBridgeModule. I won't go through how to configure this module, which comes by default with OpenSimulator, I'm just going to talk about networking using the module. To setup the module, just peruse the wiki page: Opensimulator IRCBridgeModule. See also the Gotchas below for a very specific configuration setting.

The first thing you have to know about the module, is that communications using this bridge are grid agnostic - that is, it doesn't know (or care) about the grid it is on. An object on one grid can talk to an object on another grid. You have to be able to control both regions in order to configure the simulators properly, but it's possible. I have regions on OSGrid, a private grid of my own with (currently) ten regions on it, and a standalone server supporting two regions: objects can (and do) happily chat to each other across all these configurations.

In the image at the top of this article, you can see an example of inter-region communications at work. The master - the object you see in the image - sends out a request for information from objects in other regions. Their response is the number of agents (account-based avatars) and NPCs (non-account-based avatars) in each region. It takes less than a second from sending out the request to receiving the last response. In fact, I have trouble releasing the mouse-button before all the information is returned (touching the board sends out the request). As you can see, the list includes most of my regions in mossgrid and two from OSGrid. The fact that some regions are in another grid has no relationship with how long a response takes to arrive back to the display board.

Other Options

Objects (prims) have other options for communicating with each other. If they're in the same region they can use llWhisper, llSay, or llShout depending on distance (10m, 20m and 100m respectively) or use llRegionSay or llRegionSayTo which disregard distance. llRegionSayTo requires the sending prim knows the receiving prim's UUID in advance. They can also use osMessageObject, but again, it requires knowledge of the receiving prim's UUID.

You can also communicate via http, where an object acquires a URL from the region server, and the other object sends messages to that URL. In effect, it becomes a web server. This requires the sending prim to know the receiving prim's URL in advance. URL's will change when the region server is restarted and when the prim's scripts are reset or when the prim is copied or re-rezzed. Therefore, an external database needs to store the current URL so the sending prim can fetch the address when it needs it.

There are other methods too (e.g. prim email), but only HTTP and IRC are grid agnostic. If you don't control the simulators for all your regions, HTTP may be your only option, otherwise IRC is an excellent choice. Note too the caveat about allowing regions to contact regions on the same LAN via http: OpenSimulator determined that this represented a serious security flaw, and it is disallowed by default. It can be enabled easily enough, but you should know what the risks are and how to mitigate them.

Peer-Peer

Let me clarify what I'm doing here: IRC is used as a transport mechanism. The script I have written will create a peer-to-peer network whose use is simplified by a node/sub-node system.

Any node can be a sub-node of another node (which in turn might also be a sub-node). To define nodes and groups of nodes in a sub-node hierarchy, I have implemented an inverse domain style system. Within their defined group (the list of parent groups), a node name must be unique.

    nodeNameA = "npc.market.gerry";
    nodeNameB = "npc.market.gerry"; // incorrect, name duplicated
    nodeNameC = "npc.market.alan";  // correct, new unique name
    nodeNameD = "npc.marina.gerry"; // correct, group name has changed
    nodeNameA = "fly.market.gerry"; // correct, group name has changed

This is referred to as the node address, and it is used to target messages at specific nodes or groups of nodes - e.g. using the above example:

    addressA = "npc.market.gerry"; // address of specific node
    addressB = "npc.market.*";     // all nodes in the 'market' group
    addressC = "npc.*";            // all nodes and all groups in the 'npc' group

The node address is placed in the object's description. The top-level group is defined on the left, and sub-groups are defined progressively towards the right with the final section being the name, e.g. “topGroup.subGroup1.subGroup2.subGroup3.ObjectName”. There is no requirement to make groups, so you could name your nodes fred, peter etc. instead of npc.fred, npc.peter. As long as they have unique addresses, the system will work. The idea of placing them in groups and subgroups is to enable you to send messages to entire groups.

Since this is a peer-to-peer network, there is no central server to ensure a node (object) has a unique address, so the nodes determine this themselves. When a node is rezzed, it first sends out a PING message. The message carries with it the full address of the node sending the message, the region it lives in, and the object's UUID.

If another node already exists with the same address, it sends back a PONG. The PONG is targeted at the initiating object using its name (the address), the region it lives in and its UUID. If an object receives a PONG matching all three criteria, it disables itself and let's the owner know (it also sets hover-text to say the object has been disabled on the IRC network). Otherwise, the object is good to go.

All this happens magically without any action required by the owner.

First-Class Messages

Each message sent is a first-class message. This means there is no response required - if it was then a response would require a response and quickly flood the system. Therefore, sending a response to a message is entirely optional, and any response sent is also a first-class message.

First-class messaging also means that the sender doesn't wait for a response and can carry on doing whatever it was doing.

Subdomains

The idea of having subdomains is to group things together. As an example, let's take a vending system. You have vendors in all your regions, and for some strange reason known only to yourself, they are replicated several times. This is after all, an example, and although it may not match a real-world scenario, it is easy to understand.

You have several table vendors, several chair vendors etc. Let's say you have a new table to add, or a script is updated in a chair and want to replace the existing one in the vendor. If the objects you are 'selling' are in vendor inventory, you need to run around all your regions, find the vendors and update their inventories. Yuk.

A centralised vending system is simpler. A central vend-server has all the items in its inventory, and the vendors themselves get images of the items for sale (the texture UUIDs) from the vend-server. The vendor needs only display the images to avatars and set the price, and a good vending system is capable of displaying a user-controlled slideshow of the images, effectively selling multiple items.

When someone buys something, the vendor collect the money (if any) and passes the item and avatar information to the vend-server, which then delivers the item to the avatar. This system means you only have to update the vend-server inventory with new/replaced items. Much easier.

Note: in a system where in-world currency is involved, the vendors will perform the transaction and only after confirming the avatar has paid in full will it contact the vend-server. Further, for security reasons, the vendor should contact the vend-server via https rather than IRCBridge. IRCBridge is useful for communications from the vend-server to the vendors though, especially when updating inventory. However, using IRCBridge (and this domain-based system) will allow you to fetch the URL of the vend-server without having to register the URL on an external database.

Your vending system might have these subdomains:

  1. vend.furniture.tables
  2. vend.furniture.chairs
  3. vend.furniture.sofas
  4. vend.furniture.beds
  5. vend.houses.mansions
  6. vend.houses.castles

With the above system you can add a new table to the server, then send a message to the vendors to update their list of textures (images of the items to display). If you only had one table vendor it would probably be called vend.furniture.tables, however, if you had multiple vendors selling tables, then they might be called vend.furniture.tables.01, vend.furniture.tables.02 and so on.

Using standard object-object communications, you'd have to send each one of them the same message. Using IRCBridge and the domain system, your code would look as simple as this:

// options are [ to_region, to_address ]
list defaultOptions = [ "*", "*" ];
 
// send a single message to all the table vendors
string item_price = "0"; // this is opensim, we don't charge.
string msg_to_send = "MSG::ADDTEXTURE::"+texture_uuid+";;"+item_price;
 
relay( msg_to_send, ["vend.furniture.tables.*"] );

This sends a single message to the IRCBridge, which then echoes it to each connected region. Each node receives the message and determines whether it is meant for them. If it is, it passes the message to a handler script via a link_message event to process it; if the message wasn't intended for a node it simply ignores the message.

// defined at the top of your code
integer LNK_MSG_RECV = -4247894;
integer LNK_MSG_SEND = -4247895;
 
// options are [ to_region, to_address ]
list defaultOptions = [ "*", "*" ];
 
// This is what relay looks like in your own code
relay( string message, list who ){
    if( who == [] ){
        who = defaultOptions;
    }
    if( llGetListLength( who ) == 1 ){
        who = ["*"]+who;
    }
    llMessageLinked( LINK_THIS, LNK_MSG_SEND, message, llList2CSV(who) );
}

Obviously, if you target a specific object by its unique address, only that object will respond:

// send a single message to a specific prim
string msg_to_send = "blah blah blah";
 
relay( msg_to_send, ["vend.furniture.tables.02"] );

We saw earlier that vend.furniture.tables.* specifies a group of objects. Suppose we had table vendors in multiple regions, but we only wanted to send a message to those that are in a specific region? We can do that too:

// send a simple message to all table vendors in the region "Hogwarts"
string msg_to_send = "blah blah blah";
 
relay( msg_to_send, ["Hogwarts", "vend.furniture.tables.*"] );

Going back to selecting groups of objects, we can also select a higher level group. Sending to vend.furniture.* will send to all the vendors in the subdomains table, chairs, sofas and beds. However, if we omit the dot+asterisk at the end to produce vend.furniture, then only a node named vend.furniture will react to the message.

It's worth noting here that if all the groups mentioned above existed, and there was a node named vend.furniture, then messages sent to vend.furniture.* would be ignored by the node vend.furniture.

Black Box

There are two scripts that come with this system. There is a core script which handles all the communications and filtering. This works like a black box - you don't need to edit it! All you need to know is how to send a message to it so that it will transmit that message for you, and how to receive any inbound messages intended for your object.

The other script is an example of how to send and receive messages from the IRC network - the handler script.

Message Types

Note that the messages we've sent in the above examples has the format MSG::ADDTEXTURE. The prototype for messages allows a great deal of flexibility:

string message_type = "MSG";
string message_name = "anything you want";
string data_payload = "parameter1;;parameter2;;parameter3";
 
string message = message_type + "::" + message_name + ";;" + data_payload;

This allows you to define message groups:

string m1 = "SAYTO::IM;;avatarUUID;;text to say";      // send an IM to the avatar
string m2 = "SAYTO::REGION;;avatarUUID;;text to say";  // chat to the avatar on chan 0
string m3 = "SAYTO::REGION;;objectUUID;;text to say;;-21";  // chat to the object on chan -21
string m4 = "MOVE::GOTO;;<an actual vector>";          // tell object to change location
string m5 = "MOVE:FLY;;<an actual vector>";           // tell object to fly to a new location

The purpose of grouping messages into types is to simplify the code you write to handle such messages. If your needs are pretty simple anyway, then you can just use MSG as the message type and in your handling code just ignore the message type.

In my experience, it hasn't been necessary to define a message type, as the message itself and the addresses it is being sent to are sufficient to ensure there is no ambiguity. However, the message type option is available to you and should future-proof the code.

Code

In addition to the core script, a user needs a script to receive inbound messages, handle them and provide responses if required. It receives these messages via link_message events, and sends both messages and responses the same way. Any inbound messages not intended for this node are automatically filtered out and won't trigger a link_message event.

Responses to an inbound message should normally be targeted at the sender.

Messages to send are formatted like this:

integer AGENT_AGENT = 0;
integer AGENT_NPC   = 1;
integer AGENT_ALL   = 2;
 
// format a message as MSG_TYPE::MESSAGE;;data
// this message requests a count of agents
myMessage = "MSG::COUNT_AGENTS;;"+(string)AGENT_AGENT;

The COUNT_AGENTS string tells the handling function what it is supposed to do (allowing it to handle multiple functions), and anything after the double-semicolon is treated as extra data. If you wish to break the data up (to create a list), use a different separator. The default is a double-semi-colon: ;;

You don't have to send the data yourself, just pass it to the core script via a link message:

// options are [ to_region, to_address ]
list defaultOptions = [ "*", "*" ]; // default is to send to everybody in all regions
 
// message is in the format described above. 
relay( string message, list who ){
    if( who == [] ){
        who = defaultOptions;
    }
    if( llGetListLength( who ) == 1 ){
        who = ["*"]+who;
    }
    llMessageLinked( LINK_THIS, LNK_MSG_SEND, message, llList2CSV(who) );
}

The scripts are configured via items in the object's data store. These are set using the script IRC-DataStore. The data is in a strided list of key/value pairs, and needs to be edited while still in your inventory, and needs to match the values set in the [IRC] section of opensim.ini. Note the irc-bridge-password is the access_password in OpenSim.ini.

// Settings list from the IRC-DataStore script
list secrets = [
                "relay_private_channel_out", -21873412, 
                "relay_private_channel_in" , -21873413,
                "irc-bridge-password"      , "kangaroo"
                ];

Once edited, drop the script into the object. It will set the values in the object, report the number of entries it has created and delete itself. There is another script that you can drop into your object and when the owner touches the object it will list all the keys and values in the data store (except those stored with protection).

Data store key/value pairs persist after a region or simulator restart, and if you take the object into inventory and then rez it again, its data store is intact. However, if you copy an object (in-world or in your inventory), the copy doesn't inherit the data store.

You'll notice when you look at the scripts, they don't store these values in protected mode. If you want them stored protected then you have the scripts and it's just a simple edit - remember to edit the core script too, it'll need to know it has to fetch protected data store items, and how.

All your other code to generate outgoing messages and to handle incoming messages (arriving via link_message events) are done in a separate script which you will have to create. A basic script template exists to get you started:

integer LNK_MSG_RECV  = -4247894;
integer LNK_MSG_SEND  = -4247895;
integer LNK_MSG_RESET = -4247896; // reset core script
 
// msg sections
integer MSG_TYPE = 0;
integer MSG_DATA = 1;
 
list defaultOptions = ["*", "*"];
 
// if this is a client prim that only responds to requests and doesn't issue any
// then we can keep this function simple
//
relay( string message, list who ){
    if( who == [] ){
        who = defaultOptions;
    }
    if( llGetListLength( who ) == 1 ){
        who = ["*"]+who;
    }
    llMessageLinked( LINK_THIS, LNK_MSG_SEND, message, llList2CSV(who) );
}
 
// senderDetails is a CSV string "from_region, from_node" (node = prim unique address)
//
handleMessage( string msg, list senderDetails ){
    // msg = "MSG_TYPE::MSG_MSG;;data"
    // senderDetails = [from-region, from-address]
    list details = llParseStringKeepNulls( msg, ["::"], [] );
    string msgType = llList2String( details, MSG_TYPE );
    string msg = llList2String( details, MSG_MSG );
    list data = llParseStringKeepNulls( msg, ";;", "" );
    msg = llList2String( data, 0 );
 
    // REQ_REGION_INFO is something you have defined
    if( msg == "REQ_REGION_INFO" ){
        // do something interesting
        // blah-di-blah
 
        // requires response so reply to sender
        // using the sender details targets the specific object that sent the message
        relay( msgType+"::ANS_REGION_INFO;;some_data", senderDetails );
    }
}
 
default {
    state_entry() {
        // reset irc-handler as last part of startup
        llMessageLinked( LINK_THIS, LNK_MSG_RESET, "", "" );
    }
    link_message(integer sender_num, integer num, string msg, key id){
        if( num == LNK_MSG_RECV ){
            // incoming message intended for us, so send to handler
            handleMessage( msg, (string)id );
        }
    }
}

Gotchas

There are a few gotchas with this.

  • The [IRC] block in OpenSim.ini must correctly configure the format of messages as:
msgformat = "PRIVMSG {0} : {1}::{2}::{3}"

or the scripts won't work.

  • If you have duplicate region names, scripts may respond to messages aimed at 'the other' region with the same name. The solution here would be to ensure node names are actually unique and target nodes.
  • All messages go to all scripts in all regions connected via the IRCBridgeModule and listening on the inbound channel, so a constant stream of messages is going to have an impact on simulator performance (and therefore affecting all regions supported by that simulator). For this reason, you should limit the number of messages sent. Scripts that filter out messages still consume server resources, but thankfully they are efficient at filtering.
  • If you only have one region, you can send messages to groups of objects in exactly the same way using an llRegionSay call, and the only benefit to using IRCBridgeModule is it allows for seamless expansion should you later add more regions.
  • In IRC, you don't hear yourself. That means a region that sends a message doesn't receive it. Therefore the core script also echoes any message it sends to the region via an llRegionSay message, in case there are any nodes that need to receive the message in the same region as the sender. The message is formatted in exactly the same way as if it had arrived via IRC and sent on the same channel as inbound IRC messages so it is transparent.

Examples

It's probably a good idea at this point to show some basic examples of usage. These aren't code examples, rather descriptions of scenarios.

Vending System

I've already mentioned a vending system, but I've added it here in case someone decided TL;DR and skipped to the example uses.

Head Counter

If you spend a lot of time scripting or building in one region (as I do in my sandbox), you might want to know how many avatars are in your other regions.

One of the examples in the repository is a headcount system. There is a 'master' prim in one region, and clients in all the other regions you are interested in. Once per hour, or whenever the owner touches the master prim, the master requests a head count, and the clients get a list of agents and checks which of those are NPCs, then returns a count of agents (excluding NPCs) and NPCs. The master then displays in hover-text, one line per region in the format:

    hText = "RegionName: Agents: n, NPCs: n\n";

To run this example, you must own the land/estate, so if any of your regions are on a different grid, you may have to pass your client prim to the avatar you login with on that grid. You need to have these privileges because it uses osGetAgents() rather than llGetAgentList(), but you can easily change that.

This is a fairly useless example, in that it doesn't give a real-time update, and nor would you want it to (see the gotcha above about constant streams of messages), but it shows how fast the response is. You could inject the code into a visitor board though, so a board shows recent visitors to all your regions, but you wouldn't want to do that if any of the regions are busy, or if the boards update even when no new visitors are present (bad, bad code!).

NPC Controller

It is possible to control NPCs with llRegionSay commands, and when sent to a group of NPC's it can allow you to synchronise them. If you or some of the NPCs are in a different region, you need something like the IRCBridge.

Even more fun, is to turn an NPC into a chat-bot, but take the 'artificial' out of the AI. Have your bot wear a client script to receive instructions, and another prim to control it (which you can place anywhere). When someone chats to your bot, it sends the chat to the controller which chats it to you, so you can respond on a channel which the control prim is listening to. It then sends back what you say so the bot can repeat it. You can also make the bot wave, dance, follow the avatar who spoke to it etc., all from another region (possibly even another grid). You can really have fun with this one.

If you have multiple regions that are adjacent to each other, running on different simulators or simulator instances (for performance reasons), you might want to rez all the NPCs on the region and all adjacent regions at the same time when someone enters the 'welcome' region. This way, even if they have draw distance set at a large value, the adjacent regions look 'alive'. Similarly, you can de-spawn all the NPCs when there is nobody in any of the regions so your simulators can relax.

Messaging

By messaging, I mean sending postcards - not notecards, these are different. With a postcard, an avatar touches a postbox which then displays to the avatar a text box for them to fill out their message. The message is sent to the post office, which creates a notecard using the avatar name and adds the message itself to the notecard contents, along with some header information about when it was sent, which region and grid it was sent from. Each of the postboxes are then updated to display the total number of unread postcards.

Since it is only data that is transmitted, not a notecard, it can be sent from one grid to another, and the text box ensures the amount of data is limited.

There is a working example of this in the github repository (see links below)

Conclusion

This set of scripts is a tidy solution to using the IRCBridgeModule in a highly effective and dynamic way. It provides extremely fast communications between many objects irrespective of the grid they are on, and the objects themselves are not responsible for ensuring their communication channel is maintained and available, or registered on external websites.

It doesn't work on Second Life, which is understandable, but building a bridge between an OpenSimulator region and a Second Life region has always been possible via http-in - it's just a little more involved.

Getting the Source

The source is available on GitHub at https://github.com/stroggprog/os-irc-domains where future updates will be made available.

The code in this wiki should be taken as a guide only because this page may not be updated alongside the actual code. See the github repository for updated examples and code.

This website uses cookies. By using the website, you agree with storing cookies on your computer. Also you acknowledge that you have read and understand our Privacy Policy. If you do not agree leave the website.More information about cookies
blog/opensim/ircbridge_network.txt · Last modified: 2025/05/08 13:40 by Phil Ide

Except where otherwise noted, content on this wiki is licensed under the following license: Copyright © Phil Ide
Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki