Custom Modules
The Administration Tool supports the creation of custom modules to implement integrated configuration, monitoring and management capabilities for your game or application.
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 server status being monitored in realtime.
But oftentimes developers also need to manage their application "on-the-fly", or monitor its status through specific parameters internal to the server side Extension and to which the default modules don't have access (and wouldn't know how to treat anyway).
Some 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
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. The AdminTool provides the connection to the server and a secure admin login system, it offers an existing interface with useful basic features (for example notifications and alerts) and it integrates well known UI frameworks (like Bootstrap), so developers can concentrate on the core logic of their tools and speed up the development process.
An AdminTool module is made of two separate parts: a server-side request handler and the client side user interface. In the following sections we will describe how to create a sort of template module, showing how to develop both parts and handle the network communication between the two. Files can be downloaded at the bottom of this tutorial. Let's call our custom module Game Manager.
For the purpose of this tutorial, we will assume that the Playground Zone exists in SmartFoxServer, with a Java Extension attached to it. The Extension handles the server side logic of our hypothetical game.
Requirements
We need a Java IDE to assist us in writing the server-side code, compiling it and creating a binary package (jar file) that will be deployed in SmartFoxServer. In this tutorial we will be using Eclipse.
A text editor and an image editor capable of exporting in SVG format are also needed for the module definition. The text editor is good for the client side coding (both HTML5 and JavaScript) too, but if you want to use an IDE, we suggest Visual Studio Code.
Finally, some experience in SmartFoxServer Extensions development is required, to better understand most of the concepts discussed here.
Module definition
Defining the custom module is the first step we need to take, so that SmartFoxServer can load it when launched. In order to do it, let's browse the SmartFoxServer folder looking for this file: /config/admin/admintool.xml. Open it with a text editor.
This file contains the definition of all the default AdminTool modules, and this is where we will add our custom one too. A custom module definition needs the following attributes.
- id — The main identifier of the module, which we will use throughout this tutorial. Following the default modules' convention, we will set the id after the module's name, so
GameManager. - name — The actual module's name, displayed in the AdminTool interface. In our example
Game Manager. - description — A short description of the module's purpose. It will be displayed in a tooltip when the mouse pointer hovers the module's name in the AdminTool interface. For example, we can set it to
Custom management tool for my game. - className — This attribute is reserved specifically to custom modules. It indicates the full name of the Java class that implements the request handler that SmartFoxServer must load and instantiate when started. In this example we will use a fake namespace:
my.namespace.GameManagerReqHandler. - separator — This is an optional attribute, allowing to add a separating line before or after the module menu item in the AdminTool interface.
Let's add the following line to the modules' definition file. The position in the list will determine where the module menu item will appear in the AdminTool interface. In our example it will be the last one, so we also set the separator attribute to before.
Module icon
The module also requires a vector icon in SVG format. For coherence, we suggest to use a white + orange (#ff9900) + light/dark color scheme. For elements that should change color based on the light/dark mode of the AdminTool UI, set the fill property of their style to var(--highlight,#434343). The icon must be saved in the /config/admin/icons folder.
In our example we will use this icon:
<svg width="100%" height="100%" viewBox="0 0 576 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M112,320L80,320C71.223,320 64,327.223 64,336L64,352L128,352L128,336C128,327.223 120.777,320 112,320ZM224,224C212.8,224 202.2,221.86 192,218.8L192,352L256,352L256,218.8C245.8,221.86 235.2,224 224,224Z" style="fill:var(--highlight,#434343);fill-rule:nonzero;"/>
<path d="M416,352L32,352C14.445,352 0,366.445 0,384L0,480C0,497.555 14.445,512 32,512L416,512C433.555,512 448,497.555 448,480L448,384C448,366.445 433.555,352 416,352ZM224,224C285.441,224 336,173.441 336,112C336,50.559 285.441,0 224,0C162.559,0 112,50.559 112,112C112,173.441 162.559,224 224,224ZM200,48C213.166,48 224,58.834 224,72C224,85.166 213.166,96 200,96C186.834,96 176,85.166 176,72C176,58.834 186.834,48 200,48Z" style="fill:#ff9900;fill-rule:nonzero;"/>
</svg>
Server-side handler
In this section we will show how to create a basic request handler listening to commands coming from the module's client, and how to deploy it.
Project setup
A request handler serves the purpose of receiving a specific set of requests from the client and sending back the related responses. The request handler is one of the core concepts of SmartFoxServer Extensions development. Specifically, the request handler of a custom AdminTool module receives commands from the module's client.
Let's launch Eclipse and from the File menu choose New > Java Project. In the window that pops up, we give the project a name, for example GameManagerCustomModule, and proceed to the next screen. Now it is time to add the libraries that are needed to compile our handler. Click on Libraries tab, then on the Add External JARs... button and browse the file system to the lib folder of SmartFoxServer. Select three files: sfs-admin.jar, sfs-core.jar and sfs.jar, then click the Finish button in the window.

The new project will appear in the Package Explorer panel which by default is located at the top left column of the Eclipse interface. Now we can create a new Java class by right-clicking on the src folder and selecting New > Class from the menu.
Here we can enter the name of the class, GameManagerReqHandler (as declared in the module definition), and its package, my.namespace (again as declared before). After clicking on the Finish button, the newly created class will look like this:
Base handler code
We can now add the boilerplate code to turn the newly created class into the request handler that we need.
package my.namespace;
@MultiHandler
@Instantiation(InstantiationMode.SINGLE_INSTANCE)
public class GameManagerReqHandler extends BaseAdminModuleReqHandler
{
public static final String MODULE_ID = "GameManager";
private static final String COMMANDS_PREFIX = "gameMan";
public GameManagerReqHandler()
{
super(COMMANDS_PREFIX, MODULE_ID);
}
@Override
protected void handleAdminRequest(User sender, ISFSObject params)
{
String cmd = params.getString(SFSExtension.MULTIHANDLER_REQUEST_ID);
}
}
We added the following elements:
- The class must be annotated:
- @MultiHandler tells SmartFoxServer that multiple request types will be managed by this request handler (more information here).
- @Instantiation tells SmartFoxServer if a new instance of the handler should be created or not when a request is delivered; setting it to
SINGLE_INSTANCEmakes it possible not to lose the handler state between requests (more information here).
- The class must extend
BaseAdminModuleReqHandler, which takes care of validating the request (is the user allowed to access this module? — check the Administrators table under the Remote Admin tab of the Server Configurator module) and provides other useful methods as described in the appendix below. - A couple of constants:
- MODULE_ID must match the id set for the module in its definition above, so
GameManagerin our example; this is used internally by the parent class (BaseAdminModuleReqHandler) for the mentioned validation and other tasks. - COMMANDS_PREFIX is the prefix string which identifies all the commands (requests) directed to this module. In fact the same string (in our example
gameMan) will be used later during the development of the module's client part. This prefix must be unique among all the AdminTool modules, so you can't use the following strings:admin,analytics,banManager,bBoxMonitor,console,dashboard,licenseMan,logViewer,serverConfig,uploader,zoneConfig,zoneMonitor.
- MODULE_ID must match the id set for the module in its definition above, so
- A constructor, which must call the parent constructor passing the two constants defined before.
- The implementation of the parent class abstract method
handleAdminRequest, in which we extract the actual request identifier (cmd) sent by the client, as we will see later.
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 an example, let's suppose we want to know the total number of users connected to our Playground Zone (remember we assumed in the beginning that this Zone exists on the server and it has an Extension attached to it). This is requested by the command userCount; an additional parameter is also sent by the client to indicate if we want to retrieve the total number of users or just the actual players (assuming that our game allows users to join as spectators too). Let's update the handleAdminRequest method:
@Override
protected void handleAdminRequest(User sender, ISFSObject params)
{
String cmd = params.getString(SFSExtension.MULTIHANDLER_REQUEST_ID);
if (cmd.equals("userCount"))
{
// Retrieve flag sent by the client
boolean playersOnly = params.getBool("playersOnly");
// Retrieve Zone
Zone myZone = sfs.getZoneManager().getZoneByName("Playground");
// Count users
int count = 0;
if (!playersOnly)
count = myZone.getUserCount();
else
{
for (Room room : myZone.getRoomList()) {
count += room.getPlayersList().size();
}
}
// Send response back to client
ISFSObject outParams = new SFSObject();
outParams.putInt("value", count);
outParams.putBool("playersOnly", playersOnly);
sendResponse("userCount", outParams, sender);
}
}
In this code, the main if checks what command (request) was sent by the client, which is then processed.
First of all, we know that this request comes with an additional boolean parameter, which we retrive; then the appropriate count is made. Finally the response is sent back to the sender of the request (aka the client part of our module) using a 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 (userCount), but this is not mandatory. Among the returned data we also send back the flag indicating if the user count includes all users or players only.
Communicating with an Extension
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 Extension interoperability section of the Java Extensions in-depth overview document.
For example, we want to display some overall stats about "spells" cast by players in the game, like the total amount per spell type. Let's assume this information is requested by the module's client through a generic stats command (which may include other stats too).
@Override
protected void handleAdminRequest(User sender, ISFSObject params)
{
String cmd = params.getString(SFSExtension.MULTIHANDLER_REQUEST_ID);
if (cmd.equals("userCount"))
{
// ...
}
else if (cmd.equals("stats"))
{
// Get a reference to the Zone Extension
ISFSExtension ext = sfs.getZoneManager().getZoneByName("Playground").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);
}
}
Adding an else to the main if, we identify the new request coming from the client. There we get a reference to the Extension attached to our Playground Zone and get the data we need using the handleInternalMessage method, passing an identifier of the data we need to extract (spells). The second parameter of the method is set to null, because in this example we don't need to pass additional parameters (which could be useful in a real case scenario, for example to provide a filter). For convenience, the call to the Extension returns an SFSObject, which we can immediately send back to the client. Again, the response uses the same identifier of the request (stats).
For the sake of completeness, the following code shows how the handleInternalMessage method could look like inside the Extension code:
@Override
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;
}
Deployment
Assuming our handler is now complete, we can deploy it in SmartFoxServer. In the Package Explorer right-click the project folder and choose Export.... In the next dialogue box open the Java folder and choose JAR file, then click the Next button. In the JAR Export window click the Browse... button and navigate to the SmartFoxServer's /lib/extra folder, specifying then a name for the jar file to be created (for example GameManagerAdminModule).
We can now start (or restart) SmartFoxServer. Just have a look at the startup log, to make sure the request handler is loaded without errors.
Client
In this section we will show how to develop and deploy the client part of a custom AdminTool module.
The AdminTool application is based on the Custom Elements web standard. This means that our custom AdminTool 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 purpose of the following paragraphs is to show how to create a very simple module displaying the data returned in response to the userCount and stats requests handled by the server-side handler described before.
HTML5 view
The module's html is made of a single custom tag at the root level, which then encapsulates all the other html elements which make up our interface. The name of the custom tag must follow a strict naming convention: its the module identifier in lowercase characters, with all the words separated by a dash (kebab-case) and followed by -module. The tag must also have the CSS class module applied to it.
We can now open our editor of choice and type:
We can now add more elements to our view, including some styling. In order to prevent possible conflicts in the DOM, we strongly recommend to use a prefix for all ids and class names. Alternatively it is possible to make use of the Custom Element's Shadow DOM, although this is not mandatory because we are not creating an actually reusable web component (for which the Shadow DOM comes in handy).
In our example we go with a plain DOM and use the gm- prefix where needed:
<style>
game-manager-module {
padding: 1rem;
}
.gm-output {
padding: 1rem;
margin-top: 1rem;
background-color: #ddd;
border-radius: .5rem;
}
</style>
<game-manager-module class="module">
<div>
<button id="gm-usersBt" type="button" class="gm-button">Get users count</button>
<button id="gm-playersBt" type="button" class="gm-button">Get players count</button>
<button id="gm-statsBt" type="button" class="gm-button">Get spells stats</button>
</div>
<div id="gm-outputArea" class="gm-output">Click on a button.</div>
</game-manager-module>
Other than the <style> tag, our view contains 3 buttons and a <div> in which we will display the data returned by our server side request handler. You will find out that the styling of the html elements, like buttons, is different from the one found in default modules. To match the styling, you should use the css classes of the frameworks embedded in the AdminTool. More on this subject in the note on frameworks below.
Save the HTML file with the name game-manager.html (module identifier in lowercase and kebab-case).
JavaScript controller
It is now time to write the actual logic of our module's client. Let's start with the basic scaffolding:
Please note the following:
- This is a JavaScript class as introduced by ECMAScript 2015 standard. Its name must be equal to the module identifier as per its definition. The
exportanddefaultkeywords make it possibile to load this class dynamically when needed. - Usually a class which defines a Custom Element should extend
HTMLElement. In our case instead, we have to extend a class provided by the AdminTool itself,BaseModule, which in turn extendsHTMLElement. Similarly to what happens on the server side, this class provides some useful methods that we can call or we must override. More information in the appendix below. - The class constructor must call the constructor of the parent class, passing the same command prefix defined on the server side, in this example
gameMan. - The
initializeanddestroymethods are called by the AdminTool when the module is loaded and unloaded respectively. We can override them to initialize and destroy inner components of our module for example. If we do, we must always call thesupermethods too, passing the provided parameters (which will be described in the appendix). - The
onExtensionCommandmethod is the one that gets called when a response (or more generically a "command") is sent by the server side request handler.
Note
The AdminTool will take care of loading this class dynamically and use it to "upgrade" the custom html element we defined in our module's view. We don't have to explicitly call the window.customElements.define method as we would normally do in Custom Elements development.
We can now write the code to send requests to and receive responses from the server-side request handler.
export default class GameManager extends BaseModule
{
constructor()
{
super('gameMan');
}
initialize(idData, shellController)
{
// Call super method
super.initialize(idData, shellController);
// Add buttons click listeners
document.getElementById('gm-usersBt').addEventListener('click', () => this._onUserCountReqClick(false));
document.getElementById('gm-playersBt').addEventListener('click', () => this._onUserCountReqClick(true));
document.getElementById('gm-statsBt').addEventListener('click', () => this._onStatsReqClick());
}
destroy()
{
// Call super method
super.destroy();
}
onExtensionCommand(cmd, data)
{
// Clear output area
document.getElementById('gm-outputArea').innerHTML = '';
// Handle response to "userCount" request
if (cmd == 'userCount')
{
const playersOnly = data.getBool('playersOnly');
const count = data.getNumber('value');
document.getElementById('gm-outputArea').innerHTML = `Total ${playersOnly ? 'players' : 'users'}: ${count}`;
}
// Handle response to "stats" request
else if (cmd == 'stats')
{
const spellStats = data.getSFSObject('spells');
const spells = spellStats.getKeysArray();
for (let spell of spells)
document.getElementById('gm-outputArea').innerHTML += `${spell}: ${spellStats.getNumber(spell)}<br>`;
}
//this.shellCtrl.showConfirmWarning("test message", this)
}
_onUserCountReqClick(playersOnly)
{
const params = new Sfs3.SFSObject();
params.putBool('playersOnly', playersOnly);
this.sendExtensionRequest('userCount', params);
}
_onStatsReqClick()
{
this.sendExtensionRequest('stats');
}
}
This is what we did:
- In the
initializemethod we added the listeners to the button click events (actually we should also remove them in the destroy method). The listeners call thesendExtensionRequestmethod made available by the parent class, which sends the required command and additional parameters where needed. - The
onExtensionCommandmethod, as already mentioned, receives the responses sent by our server-side request handler. In this simple example, both responses are handled similarly, by displaying the returned data in the UI. The difference is in the way such data is returned by the server, which requires a different approach to print it.
Save the JavaScript file with the name game-manager.js (again module identifier in lowercase and kebab-case).
Deployment
We are now ready to deploy the client part of our custom module and run it. First of all we need to browse the folder containing the AdminTool in our SmartFoxServer instance: go to /www/ROOT/admin and:
- Copy the HTML file (
game-manager.html) under the/modulessubfolder. - Copy the JavaScript file (
game-manager.js) under the/assets/js/custom-modulessubfolder.
We can now finally launch the AdminTool (local url: http://localhost:8088/admin), click on the newly added module to open it and click on one of the buttons in the UI to test the data retrieval:

Note on frameworks
The AdminTool makes use of the following frameworks and libraries, which are then available to you in the development of your custom modules:
- jQuery v3.3.1
The popular JavaScript library providing simple and powerful methods for HTML document traversal and manipulation, event handling, etc. - Bootstrap v5.3.5 (customized)
The most popular front-end toolkit to quickly design mobile-ready interfaces. Using the CSS classes provided by the embedded Bootstrap, you can give your module the same look&feel of the default modules. - FontAwesome v6.7.2
The free version of the popular set of vector icons. - Chart.js v4.4.9
The simple and flexible JavaScript charting library. - Luxon v3.6.1
The library for dealing with dates and times in JavaScript.
The following library is also included in the AdminTool, but you are not allowed to use it unless you own or acquire a license.
- Kendo UI for jQuery v2025.2.520 (with Bootstrap theme + customization)
A complete jQuery UI component library to quickly build responsive web applications.
Download code
The module definition and icon, the Java project of the server-side request handler and the client side HTML and JavaScript files can be downloaded as a zip file through this link:
Appendix
Server side (Java)
BaseAdminModuleReqHandler class
| Type | Method |
|---|---|
| void |
handleAdminRequest(User sender, ISFSObject params) Abstract method to be implemented by the custom request handler. This method receives the requests from the client side of the custom AdminTool module. Parameters sender (User): the User who sent the request. params (SFSObject): an object containing the parameters sent by the client. |
| void |
sendResponse(String cmd, User recipient) Sends a simple command to a specific user, without additional parameters. Parameters cmd (String): the identifier of the response. recipient (User): the User to send the response to. |
| void |
sendResponse(String cmd, List<User> recipients) Sends a simple command to multiple users, without additional parameters. Parameters cmd (String): the identifier of the response. recipients (List<User>): the list of Users to send the response to. |
| void |
sendResponse(String cmd, ISFSObject params, User recipient) Sends a response to a specific user, including a number of additional parameters. Parameters cmd (String): the identifier of the response. params (SFSObject): an object containing the parameters to be sent to the recipient. recipient (User): the User to send the response to. |
| void |
sendResponse(String cmd, ISFSObject params, List<User> recipients) Sends a response to multiple users, including a number of additional parameters. Parameters cmd (String): the identifier of the response. params (SFSObject): an object containing the parameters to be sent to the recipient. recipients (List<User>): the list of Users to send the response to. |
| void |
trace(ExtensionLogLevel level, Object ... args) Prints a message to the console and log files using the specified logging level. Parameters level (ExtensionLogLevel): the level of the message to be logged. args (Object...): any number of strings/objects to log. |
Client side (JavaScript)
BaseModule class
| Type | Constructor |
|---|---|
| none |
constructor(commandsPrefix) The BaseModule class constructor. Must be invoked with super in the costructor of the client side custom module controller. Parameters commandsPrefix (String): the prefix to be used to deliver the requests to the appropriate server-side request handler. |
| Type | Property |
|---|---|
| Object |
idData An object containing the module configuration parameters:
|
| ShellController |
shellCtrl A reference to the controller class of the AdminTool shell, providing useful general methods to display notification, alerts, etc. Check the class ShellController class API below. |
| SmartFox |
smartFox A reference to the SmartFoxServer HTML5/JavaScript API instance managed by the AdminTool controller class. Provides access to all client API methods. |
| Type | Method |
|---|---|
| none |
initialize(idData, shellController) Called when the module is loaded. This method can be overridden to perform specific initialization tasks. If overridden, the parent method must always be called through the super keyword, passing the received parameters. Parameters idData (Object): an object containing the module configuration parameters (see idData property above). shellController (ShellController): a reference to the controller class of the AdminTool shell (see shellCtrl property above). |
| none |
destroy() Called when the module is unloaded. This method can be overridden to perform specific tasks on module unload (i.e. remove event listeners). If overridden, the parent method must always be called through the super keyword. |
| none |
onExtensionCommand(cmd, data) Called when a response from the server side request handler is received. This method must be overridden to execute the custom module logic. Parameters cmd (String): the identifier of the response. data (SFSObject): an object containing the response payload. |
| none |
onUptimeUpdated(values) Called by the AdminTool shell controller once per second. This method can be overridden to display the uptime inside the custom module or make calculations based on the server uptime. Parameters values (Array): an array containing, in order, the uptime days, hours, minutes and seconds. |
| none |
sendExtensionRequest(command, data = null) Sends a request to the module's server side request handler. Parameters command (String): the identifier of the request. data (SFSObject): an optional object containing the parameters accompanying the request. |
ShellController class
| Type | Method |
|---|---|
| none |
hideOffcanvas() Hides all currently opened offcanvas panels. |
| none |
logMessage(message, type = 'log') Prints a message in the browser's console. Parameters message (String): the message to be printed in the browser's console. type (String): the message importance level among info, warn, error, log (default). |
| none |
removeDialog() Removes all currently opened dialog or alert panels. |
| none |
showConfirmWarning(text, confirmHandler, cancelHandler = null) Opens a modal panel and displays a warning message, asking for a confirmation. The panel shows an Ok button to confirm and a Cancel button to deny. The title of the panel is always "Warning". Parameters text (String): the message to be displayed in the modal panel. confirmHandler (Function): a function to be called if the Ok button is clicked. cancelHandler (Function): a function to be called if the Cancel button is clicked (default: null). |
| none |
showNotification(title, message) Shows a notification in the upper right corner of the AdminTool interface. The notification is removed automatically after a few seconds. The message is also logged in the browser's console. Parameters title (String): the title of the notification. message (String): the message to be displayed in the notification; can contain html tags. |
| none |
showSimpleAlert(text, isWarning = true) Opens a modal panel and displays a message. Parameters text (String): the message to be displayed in the modal panel. isWarning (Boolean): if true (default), the title of the modal panel will be "Warning", otherwise it will be "Information". |