Handling Events in Modules

Module code is typically triggered by events dispatched by Union Server. For example, a module might take action based on an event dispatched when a client joins a room or a room is created. The following objects dispatch events in Union Server: Server (dispatches ServerEvent events), Room (dispatches RoomEvent events), and Client (dispatches ClientEvent events).

Listening to Events

To listen to an event invoke the addEventListener() method on the Object that dispatches the event. The method takes three arguments:

  1. the event to listen to
  2. the Object to callback
  3. the method to callback


A Simple Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package net.user1.union.example.roommodule;

import net.user1.union.api.Module;
import net.user1.union.core.context.ModuleContext;
import net.user1.union.core.event.RoomEvent;

/**
 * A simple example of a RoomModule listening to a RoomEvent.
 */

public class SimpleEventListener implements Module {
    private ModuleContext m_ctx;
   
    public boolean init(ModuleContext ctx) {
        m_ctx = ctx;
       
        // Listen for clients joining the room to which this module is attached.
        // When the event happens call the method onAddClient below.
        m_ctx.getRoom().addEventListener(RoomEvent.ADD_CLIENT, this, "onAddClient");
        return true;
    }

    public void onAddClient(RoomEvent evt) {
        // this method is called when a client joins the room
    }
   
    public void shutdown() {
        // stop listening for the event
        m_ctx.getRoom().removeEventListener(RoomEvent.ADD_CLIENT, this, "onAddClient");
    }
}

Preventing Default Behavior

Some events include a default behaviour. For example, the event RoomEvent.JOIN_ROOM_REQUESTED is dispatched when a client makes a request to join a room. It has a default behaviour after being dispatched of allowing the request to continue. Preventing the default behaviour of this event will cause the join room request to be canceled. Note the difference between this event and the RoomEvent.ADD_CLIENT which has no default behaviour and is dispatched after the client has joined the room.


Preventing Default Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package net.user1.union.example.roommodule;

import net.user1.union.api.Module;
import net.user1.union.core.context.ModuleContext;
import net.user1.union.core.event.RoomEvent;

/**
 * An example of preventing the behaviour of an event.
 */

public class PreventDefaultEventListener implements Module {
    private ModuleContext m_ctx;
   
    public boolean init(ModuleContext ctx) {
        m_ctx = ctx;
       
        // Listen for clients requesting to join a room.
        m_ctx.getRoom().addEventListener(RoomEvent.JOIN_ROOM_REQUESTED, this, "onJoinRoomRequest");
        return true;
    }

    public void onJoinRoomRequest(RoomEvent evt) {
        // randomly don't allow clients to join
        if (Math.random() < .2) {
            evt.preventDefault();
        }
    }
   
    public void shutdown() {
        // stop listening for the event
        m_ctx.getRoom().removeEventListener(RoomEvent.JOIN_ROOM_REQUESTED, this, "onJoinRoomRequest");
    }
}

Removing Listeners

Ensure that you remove event listeners when they are no longer needed. Improper cleanup of listeners is usually not a problem for shorter lived Objects such as typical clients and rooms since the listeners are cleaned up by Union when these Objects are shutdown. However, take care in removing listeners if you have a long running room or are listening for events dispatched by the server.

Thread Safety

Handlers for events are not thread-safe. When modifying shared resources the event handler should enforce some form of mutual exclusion.

Example:

We have a room module that is listening for the room module message event (RoomEvent.MODULE_MESSAGE) so that it can handle the room module message "SET_HIGH_SCORE". It will first check if the given high score is higher than the current high score which is stored in a room attribute. If it is higher it will set the given high score as the new high score. However, it is possible that two different clients could send the "SET_HIGH_SCORE" room module message concurrently. We therefore need to synchronize the check and the set to ensure that only one message is being handled at a time.

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void onRoomModuleMessage(RoomEvent evt) {
    Message msg = evt.getMessage();
    if ("SET_HIGH_SCORE".equals(msg.getMessageName())) {
        synchronized (this) {
            int givenScore = Integer.parseInt(msg.getArg("score"));
            int currentHighScore = (Integer)evt.getRoom().getAttributeValue("highScore");
            if (givenScore > currentHighScore) {
            try {
                    evt.getRoom().setAttribute("highScore", givenScore, Attribute.SCOPE_GLOBAL, Attribute.FLAG_SERVER_ONLY);
                } catch (AttributeException e) {
                    // handle
                }
            }
        }
    }
}

JavaScript

The Mozilla Rhino engine for the JavaScript programming language which is included in the Oracle JDK and JRE libraries has support for synchronization with the "sync" function demonstrated in the example below. Alternatively, this could be done by accessing the java.util.concurrent.locks Java classes which support locking with Objects.

function onRoomModuleMessage(evt) {
    var msg = evt.getMessage();
    if ("SET_HIGH_SCORE" == msg.getMessageName()) {
        sync (handleHighScore(evt));
    }
}

function handleHighScore(evt) {
    var givenScore = parseInt(msg.getArg("score"));
    var currentHighScore = evt.getRoom().getAttributeValue("highScore");
    if (givenScore > currentHighScore) {
        evt.getRoom().setAttribute("highScore", givenScore,
        		net.user1.union.core.attribute.Attribute.SCOPE_GLOBAL,
        		net.user1.union.core.attribute.Attribute.FLAG_SERVER_ONLY);
    }
}