Developing custom AdminTool modules

With the release of SmartFoxServer 2X v2.16 and the final HTML5 version of its Administration Tool, we decided to introduce a long-awaited feature: support for custom administration modules.

In this article we will provide a brief introduction on the subject and a walkthrough of the development and integration workflow of the modules.

» Introduction

In multiplayer games and applications, a common requirement is to be able to monitor how users are behaving during their sessions. In general, this is something the default AdminTool’s modules (in particular Zone Monitor and Analytics) provide at a global level, based on the realtime server status or the inspection of the logs.
But oftentimes developers also need to manage their application “on-the-fly”, or monitor its status through specific parameters which are inner to the server side Extension and to which the default modules don’t have access (and wouldn’t know how to treat anyway).

A few use cases are:

  • show statistics on quests, items, weapons, spells, etc, to drive future developments and changes to the gameplay;
  • control the status of Non-Player Characters (NPC); add or remove them, or adjust the rules controlling their behavior;
  • trigger game-specific events for a selection of users, or all of them;
  • monitor the behavior of users to identify specific cheating attempts;
  • etc.

Custom AdminTool Modules have been introduced to help developers easily create their own monitoring and management tools, specific for their games and applications, and collect them in one convenient place.

An AdminTool module is made of two separate parts: a server side request handler and the client side user interface. In the following paragraphs we will briefly go through the development of a hypothetical Game Manager example module. As we will refer to some concepts borrowed from SFS2X Extensions development, some knowledge of the subject is recommended.

A detailed tutorial, including the source code and the description of the API, is available in the AdminTool documentation.

» Module definition

A custom module is defined alongside the standard modules, in the /config/admin/admintool.xml XML file, under the main SmartFoxServer folder. The following is an example entry for our Game Manager module:

<module id="GameManager" name="Game Manager" description="Management tool for our game" className="my.namespace.GameManagerReqHandler">

In particular, the className attribute indicates the full name of the Java class that implements the request handler that SFS2X must load and instantiate when started. In this example we used a fake namespace: my.namespace.GameManagerReqHandler.

The module also requires a vector icon in SVG format, saved under the /config/admin/icons folder. The following is an example using the same color scheme of the standard modules on a dark background.

» Server side request handler

The request handler is one of the core concepts of SmartFoxServer Extensions development: a request handler serves the purpose of receiving a specific set of requests from the client and sending back the related responses.

In Eclipse we create a new Java project, adding the sfs2x-admin.jar, sfs2x-core.jar and sfs2x.jar files from the SmartFoxServer’s /lib folder to the project’s libraries. We can then create a new Java class named GameManagerReqHandler (as declared in the module definition) inside the my.namespace package (again as declared before).

The following snippet shows the boilerplate code which turns the newly created class into the request handler that we need.

package my.namespace;
public class GameManagerReqHandler extends BaseAdminModuleReqHandler
    public static final String MODULE_ID = "GameManager";
    private static final String COMMANDS_PREFIX = "gameMan";
    public CustomToolReqHandler()
    protected void handleAdminRequest(User sender, ISFSObject params)
        String cmd = params.getUtfString(SFSExtension.MULTIHANDLER_REQUEST_ID);

The class must use the @MultiHandler and @Instantiation annotations and extend the BaseAdminModuleReqHandler class, which takes care of validating the requests and provides other useful methods as described in the documentation.
The MODULE_ID constant must match the ID set for the module in its definition, while the COMMANDS_PREFIX is the prefix string which identifies all the commands (requests) directed to our module. Both constants must be passed to the parent class constructor.

The handleAdminRequest method is the core of the module’s server side handler. This is where all commands coming from the client are processed. With the help of an if-else statement we can execute different actions based on the actual command sent by the client.

As mentioned in the introduction, when creating a custom module the actual benefit comes from being able to access our game Extension to extract informations on the game state, send commands and more.
Communicating with an Extension from the request handler can be easily achieved through the Extension’s handleInternalMessage method, described in the Java Extensions in-depth overview.

For example we want to display some overall stats about “spells” cast by players in the game, for example the total amount per spell type. Let’s assume this information is available in the Zone Extension attached to our MyGame Zone, and it is requested by the module’s client through a generic “stats” command (which may include other stats too).
This is what we should add to the handleAdminRequest method:

protected void handleAdminRequest(User sender, ISFSObject params)
    String cmd = params.getUtfString(SFSExtension.MULTIHANDLER_REQUEST_ID);
    if (cmd.equals("stats"))
        // Get a reference to the Zone Extension
        ISFSExtension ext = sfs.getZoneManager().getZoneByName("MyGame").getExtension();
        // Extract stats about "spells" from Extension
        ISFSObject spellsObj = (ISFSObject) ext.handleInternalMessage("spells", null);
        // Send response back to client
        ISFSObject outParams = new SFSObject();
        outParams.putSFSObject("spells", spellsObj);
        //outParams.putSFSObject("others", otherObj); // Other stats collected by the Extension
        sendResponse("stats", outParams, sender);

    // else if (...)

We identify the new request coming from the client, then we get a reference to the Extension attached to our MyGame Zone and get the data we need using the handleInternalMessage method, passing an identifier of the data we need to extract (“spells”). For convenience, the call to the Extension returns an SFSObject, which we can immediately send back to the sender of the request (aka the client part of our module) using the sendResponse service method made available by the BaseAdminModuleReqHandler parent class. The first parameter passed to the sendResponse method is the string identifier of the response, which the client will use to know what to do with the returned data. For convenience we use the same identifier of the request (“stats”), but this is not mandatory.

For the sake of completeness, the following code shows how the handleInternalMessage method could look like inside the Extension code:

public Object handleInternalMessage(String cmdName, Object params)
    if (cmdName.equals("stats"))
        ISFSObject spellsObj = new SFSObject();
        spellsObj.putInt("teleport", this.getTeleportSpellCount());
        spellsObj.putInt("fireball", this.getFireballSpellCount());
        spellsObj.putInt("protection", this.getProtectionSpellCount());
        spellsObj.putInt("heal", this.getHealSpellCount());
        return spellsObj;
    return null;

Assuming our handler is now complete, we can deploy it in SmartFoxServer exporting it as a JAR file in the /extensions/__lib__ folder. We can then start SmartFoxServer.

» Client side view and controller

On the client side, the AdminTool application is based on the Custom Elements web standard. This means that our custom module must follow the same approach: in a nutshell, we have to declare a custom html tag and the JavaScript class defining it — in other words, the view and its controller.

The module’s html is made of a single custom tag at the root level, which encapsulates all the other html elements which constitute our interface. The name of the custom tag must be the module ID in kebab-case, followed by -module. The tag must also have the CSS class module applied to it.
We can then add more elements to the view: a button to request the stats to the Extension, an output area and some styling.

    game-manager-module {
        padding: 1rem;
    .gm-output {
        padding: 1rem;
        margin-top: 1rem;
        background-color: #ddd;
        border-radius: .5rem;
<game-manager-module class="module">
        <button id="gm-spellsBt" type="button" class="gm-button">Get spells stats</button>
    <div id="gm-outputArea" class="gm-output">Click on the button.</div>

After saving the html file with the name game-manager.html (module ID in lowercase and kebab-case), we can move on to the actual JavaScript logic of our module.

export default class GameManager extends BaseModule
	initialize(idData, shellController)
		// Call super method
		super.initialize(idData, shellController);
		// Add button click listeners
		document.getElementById('gm-statsBt').addEventListener('click', () => this.sendExtensionRequest('stats'));
		// Call super method
	onExtensionCommand(cmd, data)
		// Clear output area
		document.getElementById('gm-outputArea').innerHTML = '';
		// Handle response to "stats" request
		if (cmd == 'stats')
			const spellStats = data.getSFSObject('spells');
			const spells = spellStats.getKeysArray();
			for (let spell of spells)
				document.getElementById('gm-outputArea').innerHTML += `${spell}: ${spellStats.getInt(spell)}<br>`;
		// Handle other responses
		// else if (cmd == '....')

This is a JavaScript class as introduced by ECMAScript 2015 standard. Its name must be equal to the module ID as per its definition. The export and default keywords make it possibile to load this class dynamically when needed. The class must extend a class provided by the AdminTool itself, called BaseModule. Similarly to what happens on the server side, this class provides some useful methods to override or just call. Note the “gameMan” string passed to the parent constructor, which must be equal to the COMMANDS_PREFIX constant on the server side.

The initialize and destroy methods are called by the AdminTool when the module is loaded or unloaded respectively, while the onExtensionCommand method is the one that gets called when a response (or more generically a “command”) is sent by our server side request handler. In this simple example, we handle the response to our “stats” request by displaying the returned data in the UI.

We can now save the JS file with the name game-manager.js (module ID in lowercase and kebab-case) and deploy the client.
The html file (game-manager.html) goes to the /www/ROOT/admin/modules folder, and the JavaScript file we just saved to the /www/ROOT/admin/assets/js/custom-modules folder.

We can now finally launch the AdminTool (local url: http://localhost:8080/admin), click on the newly added module to open it and click on the button in the UI to test the data retrieval:

For a detailed description and the source code of this example, please also check the full tutorial available in the AdminTool documentation.