{"id":1403,"date":"2020-05-19T09:33:09","date_gmt":"2020-05-19T09:33:09","guid":{"rendered":"https:\/\/smartfoxserver.com\/blog\/?p=1403"},"modified":"2020-05-19T09:33:09","modified_gmt":"2020-05-19T09:33:09","slug":"developing-custom-admintool-modules","status":"publish","type":"post","link":"https:\/\/smartfoxserver.com\/blog\/developing-custom-admintool-modules\/","title":{"rendered":"Developing custom AdminTool modules"},"content":{"rendered":"\n<p>With the release of SmartFoxServer 2X v2.16 and the final HTML5 version of its <strong>Administration Tool<\/strong>, we decided to introduce a long-awaited feature: support for <strong>custom administration modules<\/strong>.<\/p>\n\n\n\n<p>In this article we will provide a brief introduction on the subject and a walkthrough of the development and integration workflow of the modules.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2>\u00bb&nbsp;Introduction<\/h2>\n\n\n\n<p>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&#8217;s modules (in particular&nbsp;<strong>Zone Monitor<\/strong>&nbsp;and&nbsp;<strong><a href=\"https:\/\/www.smartfoxserver.com\/products\/analytics#p=intro\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\"Analytics (opens in a new tab)\">Analytics<\/a><\/strong>) provide at a global level, based on the realtime server status or the inspection of the logs.<br>But oftentimes developers also need to manage their application &#8220;on-the-fly&#8221;, or monitor its status through specific parameters which are inner to the server side Extension and to which the default modules don&#8217;t have access (and wouldn&#8217;t know how to treat anyway).<\/p>\n\n\n\n<p>A few use cases are:<\/p>\n\n\n\n<ul><li>show statistics on quests, items, weapons, spells, etc, to drive future developments and changes to the gameplay;<\/li><li>control the status of Non-Player Characters (NPC); add or remove them, or adjust the rules controlling their behavior;<\/li><li>trigger game-specific events for a selection of users, or all of them;<\/li><li>monitor the behavior of users to identify specific cheating attempts;<\/li><li>etc.<\/li><\/ul>\n\n\n\n<p><strong>Custom AdminTool Modules<\/strong>&nbsp;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.<\/p>\n\n\n\n<p>An AdminTool module is made of two separate parts: a&nbsp;<strong>server side request handler<\/strong>&nbsp;and the&nbsp;<strong>client side user interface<\/strong>. In the following paragraphs we will briefly go through the development of a hypothetical <em>Game Manager<\/em> example module. As we will refer to some concepts borrowed from <strong>SFS2X Extensions<\/strong> development, some knowledge of the subject is recommended.<\/p>\n\n\n\n<p>A detailed tutorial, including the source code and the description of the API, is available in the <a href=\"http:\/\/docs2x.smartfoxserver.com\/GettingStarted\/admintool-custom-modules\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\" (opens in a new tab)\">AdminTool documentation<\/a>.<\/p>\n\n\n\n<h2>\u00bb&nbsp;Module definition<\/h2>\n\n\n\n<p>A custom module is defined alongside the standard modules, in the <strong>\/config\/admin\/admintool.xml<\/strong> XML file, under the main SmartFoxServer folder. The following is an example entry for our <em>Game Manager<\/em> module: <\/p>\n\n\n<pre class=\"brush: xml; light: true; title: ; notranslate\" title=\"\">\n&lt;module id=&quot;GameManager&quot; name=&quot;Game Manager&quot; description=&quot;Management tool for our game&quot; className=&quot;my.namespace.GameManagerReqHandler&quot;&gt;\n<\/pre>\n\n\n\n<p>In particular, the <strong>className<\/strong> 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:&nbsp;<em>my.namespace.GameManagerReqHandler<\/em>.<\/p>\n\n\n\n<p>The module also requires a vector icon in SVG format,&nbsp;saved under the <strong>\/config\/admin\/icons<\/strong> folder. The following is an example using the same color scheme of the standard modules on a dark background.<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><img loading=\"lazy\" width=\"50\" height=\"50\" src=\"https:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2020\/05\/GameManager.png\" alt=\"\" class=\"wp-image-1415\"\/><\/figure><\/div>\n\n\n\n<h2>\u00bb&nbsp;Server side request handler<\/h2>\n\n\n\n<p>The <strong>request handler<\/strong> 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.<\/p>\n\n\n\n<p>In Eclipse we create a new Java project, adding the <strong>sfs2x-admin.jar<\/strong>, <strong>sfs2x-core.jar<\/strong> and <strong>sfs2x.jar<\/strong> files from the SmartFoxServer&#8217;s <strong>\/lib<\/strong> folder to the project&#8217;s libraries. We can then create a new Java class named&nbsp;<strong>GameManagerReqHandler<\/strong>&nbsp;(as declared in the module definition) inside the <strong>my.namespace<\/strong>&nbsp;package (again as declared before).<\/p>\n\n\n\n<p>The following snippet shows the boilerplate code which turns the newly created class into the request handler that we need.<\/p>\n\n\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\npackage my.namespace;\n \n@MultiHandler\n@Instantiation(InstantiationMode.SINGLE_INSTANCE)\npublic class GameManagerReqHandler extends BaseAdminModuleReqHandler\n{\n    public static final String MODULE_ID = &quot;GameManager&quot;;\n    private static final String COMMANDS_PREFIX = &quot;gameMan&quot;;\n \n    public CustomToolReqHandler()\n    {\n        super(COMMANDS_PREFIX, MODULE_ID);\n    }\n \n    @Override\n    protected void handleAdminRequest(User sender, ISFSObject params)\n    {\n        String cmd = params.getUtfString(SFSExtension.MULTIHANDLER_REQUEST_ID);\n    }\n}\n<\/pre>\n\n\n\n<p>The class must use the <strong>@MultiHandler<\/strong> and <strong>@Instantiation<\/strong>&nbsp;annotations and extend the&nbsp;<strong>BaseAdminModuleReqHandler<\/strong> class, which takes care of validating the requests and provides other useful methods as described in the documentation.<br>The <strong>MODULE_ID<\/strong>&nbsp;constant must match the ID set for the module in its definition, while the <strong>COMMANDS_PREFIX<\/strong> is the prefix string which identifies all the commands (requests) directed to our module. Both constants must be passed to the parent class constructor.<\/p>\n\n\n\n<p>The&nbsp;<strong>handleAdminRequest<\/strong>&nbsp;method is the core of the module&#8217;s server side handler. This is where all commands coming from the client are processed. With the help of an&nbsp;<em>if-else<\/em>&nbsp;statement we can execute different actions based on the actual command sent by the client.<\/p>\n\n\n\n<p>As mentioned in the introduction, when creating a custom module the actual benefit comes from being able to&nbsp;<strong>access our game Extension<\/strong>&nbsp;to extract informations on the game state, send commands and more.<br>Communicating with an Extension from the request handler can be easily achieved through the Extension&#8217;s&nbsp;<strong>handleInternalMessage<\/strong>&nbsp;method, described in the&nbsp;<a href=\"http:\/\/docs2x.smartfoxserver.com\/ExtensionsJava\/overview#anatomy\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\" (opens in a new tab)\">Java Extensions in-depth overview<\/a>.<\/p>\n\n\n\n<p>For example we want to display some overall stats about &#8220;spells&#8221; cast by players in the game, for example the total amount per spell type. Let&#8217;s assume this information is available in the Zone Extension attached to our <em>MyGame<\/em> Zone, and it is requested by the module&#8217;s client through a generic &#8220;stats&#8221; command (which may include other stats too).<br>This is what we should add to the <strong>handleAdminRequest<\/strong> method:<\/p>\n\n\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\n@Override\nprotected void handleAdminRequest(User sender, ISFSObject params)\n{\n    String cmd = params.getUtfString(SFSExtension.MULTIHANDLER_REQUEST_ID);\n     \n    if (cmd.equals(&quot;stats&quot;))\n    {\n        \/\/ Get a reference to the Zone Extension\n        ISFSExtension ext = sfs.getZoneManager().getZoneByName(&quot;MyGame&quot;).getExtension();\n         \n        \/\/ Extract stats about &quot;spells&quot; from Extension\n        ISFSObject spellsObj = (ISFSObject) ext.handleInternalMessage(&quot;spells&quot;, null);\n         \n        \/\/ Send response back to client\n        ISFSObject outParams = new SFSObject();\n        outParams.putSFSObject(&quot;spells&quot;, spellsObj);\n        \/\/outParams.putSFSObject(&quot;others&quot;, otherObj); \/\/ Other stats collected by the Extension\n         \n        sendResponse(&quot;stats&quot;, outParams, sender);\n    }\n\n    \/\/ else if (...)\n}\n<\/pre>\n\n\n\n<p>We identify the new request coming from the client, then we get a reference to the Extension attached to our&nbsp;<em>MyGame<\/em>&nbsp;Zone and get the data we need using the&nbsp;<em>handleInternalMessage<\/em> method, passing an identifier of the data we need to extract (&#8220;spells&#8221;). For convenience, the call to the Extension returns an SFSObject, which we can immediately send back to the&nbsp;<em>sender<\/em>&nbsp;of the request (aka the client part of our module) using the <strong>sendResponse<\/strong> service method made available by the&nbsp;<em>BaseAdminModuleReqHandler<\/em>&nbsp;parent class. The first parameter passed to the&nbsp;<em>sendResponse<\/em>&nbsp;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 (&#8220;stats&#8221;), but this is not mandatory.<\/p>\n\n\n\n<p>For the sake of completeness, the following code shows how the&nbsp;<em>handleInternalMessage<\/em>&nbsp;method could look like inside the Extension code:<\/p>\n\n\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\n@Override\npublic Object handleInternalMessage(String cmdName, Object params)\n{\n    if (cmdName.equals(&quot;stats&quot;))\n    {\n        ISFSObject spellsObj = new SFSObject();\n        spellsObj.putInt(&quot;teleport&quot;, this.getTeleportSpellCount());\n        spellsObj.putInt(&quot;fireball&quot;, this.getFireballSpellCount());\n        spellsObj.putInt(&quot;protection&quot;, this.getProtectionSpellCount());\n        spellsObj.putInt(&quot;heal&quot;, this.getHealSpellCount());\n         \n        return spellsObj;\n    }\n     \n    return null;\n}\n<\/pre>\n\n\n\n<p>Assuming our handler is now complete, we can deploy it in SmartFoxServer exporting it as a <strong>JAR file<\/strong> in the&nbsp;<strong>\/extensions\/__lib__<\/strong> folder. We can then start SmartFoxServer.<\/p>\n\n\n\n<h2>\u00bb&nbsp;Client side view and controller<\/h2>\n\n\n\n<p>On the client side, the AdminTool application is based on the&nbsp;<a rel=\"noreferrer noopener\" href=\"https:\/\/developers.google.com\/web\/fundamentals\/web-components\/customelements\" target=\"_blank\">Custom Elements<\/a>&nbsp;web standard. This means that our custom module must follow the same approach: in a nutshell, we have to declare a custom&nbsp;<strong>html tag<\/strong>&nbsp;and the&nbsp;<strong>JavaScript class<\/strong>&nbsp;defining it \u2014 in other words, the view and its controller.<\/p>\n\n\n\n<p>The module&#8217;s <strong>html<\/strong> 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&nbsp;<strong>-module<\/strong>. The tag must also have the CSS class&nbsp;<em>module<\/em>&nbsp;applied to it.<br>We can then add more elements to the view: a button to request the stats to the Extension, an output area and some styling.<\/p>\n\n\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\n&lt;style&gt;\n    game-manager-module {\n        padding: 1rem;\n    }\n \n    .gm-output {\n        padding: 1rem;\n        margin-top: 1rem;\n        background-color: #ddd;\n        border-radius: .5rem;\n    }\n&lt;\/style&gt;\n \n&lt;game-manager-module class=&quot;module&quot;&gt;\n    &lt;div&gt;\n        &lt;button id=&quot;gm-spellsBt&quot; type=&quot;button&quot; class=&quot;gm-button&quot;&gt;Get spells stats&lt;\/button&gt;\n    &lt;\/div&gt;\n    &lt;div id=&quot;gm-outputArea&quot; class=&quot;gm-output&quot;&gt;Click on the button.&lt;\/div&gt;\n&lt;\/game-manager-module&gt;\n<\/pre>\n\n\n\n<p>After saving the html file with the name&nbsp;<strong>game-manager.html<\/strong>&nbsp;(module ID in lowercase and kebab-case), we can move on to&nbsp;the actual JavaScript logic of our module.<\/p>\n\n\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\nexport default class GameManager extends BaseModule\n{\n\tconstructor()\n\t{\n\t\tsuper('gameMan');\n\t}\n\t\n\tinitialize(idData, shellController)\n\t{\n\t\t\/\/ Call super method\n\t\tsuper.initialize(idData, shellController);\n \n\t\t\/\/ Add button click listeners\n\t\tdocument.getElementById('gm-statsBt').addEventListener('click', () =&gt; this.sendExtensionRequest('stats'));\n\t}\n\t\n\tdestroy()\n\t{\n\t\t\/\/ Call super method\n\t\tsuper.destroy();\n\t}\n\t\n\tonExtensionCommand(cmd, data)\n\t{\n\t\t\/\/ Clear output area\n\t\tdocument.getElementById('gm-outputArea').innerHTML = '';\n \n\t\t\/\/ Handle response to &quot;stats&quot; request\n\t\tif (cmd == 'stats')\n\t\t{\n\t\t\tconst spellStats = data.getSFSObject('spells');\n\t\t\tconst spells = spellStats.getKeysArray();\n \n\t\t\tfor (let spell of spells)\n\t\t\t\tdocument.getElementById('gm-outputArea').innerHTML += `${spell}: ${spellStats.getInt(spell)}&lt;br&gt;`;\n\t\t}\n\t\t\n\t\t\/\/ Handle other responses\n\t\t\/\/ else if (cmd == '....')\n\t}\n}\n<\/pre>\n\n\n\n<p>This is a JavaScript&nbsp;<strong>class<\/strong>&nbsp;as introduced by ECMAScript 2015 standard. Its name must be equal to the module ID as per its definition. The&nbsp;<em>export<\/em>&nbsp;and&nbsp;<em>default<\/em>&nbsp;keywords make it possibile to load this class dynamically when needed. The class must extend a class provided by the AdminTool itself, called&nbsp;<strong>BaseModule<\/strong>. Similarly to what happens on the server side, this class provides some useful methods to override or just call. Note the &#8220;gameMan&#8221; string passed to the parent constructor, which must be equal to the <strong>COMMANDS_PREFIX<\/strong> constant on the server side.<\/p>\n\n\n\n<p>The <strong>initialize<\/strong> and <strong>destroy<\/strong> methods are called by the AdminTool when the module is loaded or unloaded respectively, while the <strong>onExtensionCommand<\/strong> method is the one that gets called when a response (or more generically a &#8220;command&#8221;) is sent by our server side request handler. In this simple example, we handle the response to our &#8220;stats&#8221; request by displaying the returned data in the UI.<\/p>\n\n\n\n<p>We can now save the JS file with the name&nbsp;<strong>game-manager.js<\/strong>&nbsp;(module ID in lowercase and kebab-case) and deploy the client.<br>The html file (<strong>game-manager.html<\/strong>) goes to the&nbsp;<strong>\/www\/ROOT\/admin\/modules<\/strong>&nbsp;folder, and the JavaScript file we just saved to the&nbsp;<strong>\/www\/ROOT\/admin\/assets\/js\/custom-modules<\/strong>&nbsp;folder.<\/p>\n\n\n\n<p>We can now finally launch the AdminTool (local url:&nbsp;<a rel=\"noreferrer noopener\" href=\"http:\/\/localhost:8080\/admin\" target=\"_blank\">http:\/\/localhost:8080\/admin<\/a>), click on the newly added module to open it and click on the button in the UI to test the data retrieval:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"730\" height=\"303\" src=\"https:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2020\/05\/custom-modules-example.png\" alt=\"\" class=\"wp-image-1430\" srcset=\"https:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2020\/05\/custom-modules-example.png 730w, https:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2020\/05\/custom-modules-example-300x125.png 300w, https:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2020\/05\/custom-modules-example-624x259.png 624w\" sizes=\"(max-width: 730px) 100vw, 730px\" \/><\/figure>\n\n\n\n<p>For a detailed description and the source code of this example, please also check the full tutorial available in the <a href=\"http:\/\/docs2x.smartfoxserver.com\/GettingStarted\/admintool-custom-modules\">AdminTool documentation<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[23,1],"tags":[79,123,125,8,124,83,7],"_links":{"self":[{"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/posts\/1403"}],"collection":[{"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/comments?post=1403"}],"version-history":[{"count":20,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/posts\/1403\/revisions"}],"predecessor-version":[{"id":1447,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/posts\/1403\/revisions\/1447"}],"wp:attachment":[{"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/media?parent=1403"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/categories?post=1403"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/tags?post=1403"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}