SFS2X Docs / ExtensionsJS / extension-development
» JavaScript Extensions development
In this article we're going to take a deeper dive in all aspects of JavaScript Extension development and touch on a number of different topics:
- The Java environment
- Modules
- Dynamically attaching Extensions
- Extension interoperability
- Scheduled tasks
- Http requests
- File system
- Reading exceptions
» The Java Environment
SmartFoxServer 2X is a Java application running in the JVM (Java Virtual Machine), so any server side JavaScript code is also running in the JVM.
More specifically JavaScript runs in the Nashorn JavaScript engine, a powerful ECMAScript 5 interpreter/compiler provided with JDK 8 and higher. This means that the server code interacts with the SFS2X Java API and the Java Runtime: in fact many of the objects provided by the server JavaScript API are actually Java objects.
Why is this relevant, you may ask? It is important because you will find references in the JSDoc to objects and classes that come from the "top level" API, in other words from Java. The following diagram shows how a JavaScript Extension runs in SFS2X:
Essentially server-side JavaScript is wrapped by a Java Extension running a Nashorn compiler and exposing the native SFS2X API to the JS environment. The JavaScript API acts as a glue between the two runtimes and gets compiled, with the Extension scripts, to JVM bytecode.
If you're interested in a more low-level discussion of this topic with a performance comparison to native Java you can learn more in our advanced guide.
» Modules
A good way to streamline Extension code is to use modules, which essentially are different JavaScript files (.js) organizing the whole game code. Instead of writing a monolithic Extension file you can create the main file (containing the init() method at least) and a series of different modules that are imported where needed.
This is accomplished via the include() global function. For example:
include ("lobby.js"); include ("chat.js"); function init() { // Initialization code here... }
Modules can also be organized in sub-folders if necessary, and can be loaded as in the example using paths relative to the current Extension folder.
» Dynamically attaching Extensions
In the Quick Start guide we've outlined the process to assign an Extension to a Zone or Room via the AdminTool, but there's also a dynamic way to assign Extensions to Rooms. When a Room is created from either client or server side we can specify which Extension should be associated with the new Room. Here are two quick examples of how this is done from both sides.
Client Side
private void CreateRoom() { RoomSettings cfg = new RoomSettings(); cfg.Name = "Hello World Room"; cfg.MaxUsers = 15; cfg.MasSpectators = 5; cfg.IsGame = true; cfg.Extension = new RoomExtension("MyJavaScriptExtension", "SimpleTest.js"); sfs.Send(new Sfs2X.Requests.CreateRoomRequest(cfg)); }
function createRoom() { var cfg = new SFS2X.RoomSettings(); cfg.name = "Hello World Room"; cfg.maxUsers = 15; cfg.masSpectators = 5; cfg.isGame = true; cfg.extension = new SFS2X.RoomExtension("MyJavaScriptExtension", "SimpleTest.js"); sfs.send(new SFS2X.CreateRoomRequest(cfg)); }
function createRoom() { var cfg:RoomSettings = new RoomSettings(); cfg.name = "Hello World Room"; cfg.maxUsers = 15; cfg.masSpectators = 5; cfg.isGame = true; cfg.extension = new RoomExtension("MyJavaScriptExtension", "SimpleTest.js"); sfs.send(new CreateRoomRequest(cfg)); }
Server Side
function createRoom() { var cfg = new CreateRoomSettings(); cfg.setName("Hello World Room"); cfg.setMaxUsers(15); cfg.setMaxSpectators(5); cfg.setGame(true); cfg.setExtension(new CreateRoomSettings.RoomExtensionSettings("MyJavaScriptExtension", "SimpleTest.js")); getApi().createRoom(getParentZone(), cfg, null, false, null, true, true); }
A typical use for dynamic Room Extensions is to create a new game Room and associate it to a specific game logic, based on the type of game being launched.
» Scheduled tasks
Often times in games we need to delay an action by a certain amount of time or create a timer task that goes off at regular intervals.
SFS2X provides a Scheduler object that allows to manage multiple delayed or repeating activities. Let's see a couple of examples of use.
var runner = function() { trace("Hello, this is a delayed action!"); } var scheduler = getApi().newScheduler(); scheduler.schedule(runner, 1000);
All we need is to create a scheduler and pass a custom function and a time interval expressed in milliseconds. In this case our runner function will be executed with one second of delay.
Let's now take a look at a slightly more complex example, using a repeating task.
var stepCount = 0; var scheduler; var taskHandle; function init() { scheduler = getApi().newScheduler(); taskHandle = scheduler.scheduleAtFixedRate(runner, 3000); trace("Simple JS Example inited"); } function destroy() { if (taskHandle != null) taskHandle.cancel(true); trace("Simple JS Example destroyed"); } function runner() { stepCount++; trace("I was called:", stepCount,"times"); }
Here we create a repeating task, running once every 3 seconds and keeping a counter updated. To do so we create a scheduler and use the scheduleAtFixedRate() method to start the task. Notice how we assign the task to the taskHandle global variable. We need to keep a reference to our task object so that if the Extension is reloaded we can stop it in the destroy() method.
Failing to do so would leave the previous task running, causing a task leak.
In case we're running dozens of tasks in the same Extension we can use a different strategy to shut down all tasks at once, by calling the destroy() method on the scheduler object itself.
NOTE
By default the getApi().newScheduler() method returns a new Scheduler backed by a single thread. If you need a larger thread pool you can pass the number of threads to the method. Typically you won't need more than 1 thread unless you plan to run tasks that take long time to execute.
» Handling Exceptions
There is one extra good practice we need to learn when using schedulers. If an exception occurs in the task function the task will stop. To avoid this issue it is highly recommended to handle any exception inside our function. Let's rewrite our previous runner function accordingly:
function runner() { try { stepCount++; trace("I was called:", stepCount, "times"); } catch(err) { trace("An error occurred:", err); } }
REMINDER
Always make sure to wrap the code of your task function in a try/catch block. This is the only way to make sure that Exceptions are captured by the task itself. Failing to do so will make the task quit.
» Extension interoperability
There can be situations where two or more Extensions need to exchange data among them: for instance a Zone Extension wants to get a certain object owned by a Room Extension.
An easy way for this cross communication is via the handleInternalMessage() method which can be implemented on the server side, when needed. Let's see an example:
function createRoom(argument) { var cfg = new CreateRoomSettings(); cfg.setName("Hello World Room"); cfg.setMaxUsers(15); cfg.setMaxSpectators(5); cfg.setGame(true); cfg.setExtension(new CreateRoomSettings.RoomExtensionSettings("MyJavaScriptExtension", "SimpleTest.js")); var theRoom = getApi().createRoom(getParentZone(), cfg, null, false, null, true, true); var ext = theRoom.getExtension(); res = ext.handleInternalMessage("getGame", ""); trace("Game id:", res.id, "-- type:", res.type); }
Here we reuse the code from a previous example to create a dynamic Room with an Extension and then talk to that Extension to exchange data. Specifically, we want to call the Room Extension just created to obtain a Game object from it.
The handleInternalMessage() function takes two parameters:
- a command name
- a parameter object, containing one or more parameters required by the command
In this case we want to retrieve an object from the Room so we just need a command name and no extra parameters.
Let's see what how this is works on the receiving end, i.e. the Room Extension:
var Game = function(id, type) { this.id = id; this.type = type; }; var myGame; function init() { trace("Extension inited"); myGame = new Game(55, "Platformer"); } function handleInternalMessage(cmd, params) { if (cmd == "getGame") return myGame; else trace("Internal message:", cmd, params); }
The code should be self explanatory. All we need, is implementing the function and respond to the commands that an Extension is expected to receive.
» Http Requests
The server side JavaScript API allows to talk to an external server via HTTP/HTTPS protocol by sending either GET or POST requests. Let's take a look at a basic example:
function httpTest() { var reqParams = { id: 25 }; var httpReq = getApi().newHttpGetRequest('http://some.host.com/', reqParams, httpCallback); httpReq.execute(); } function httpCallback(result) { if (result.error) trace("HTTP call failed: " + result.error); else { trace("HTTP call success: " + result.statusCode); trace(result.html); } }
The newHttpGetRequest(...) method takes three parameters:
- the url of our http service
- an object containing the parameters we need to send
- a callback function that handles the server response, asynchronously
When our callback function is called we're passed an object with three parameters:
- error: if not null, it contains an error message
- statusCode: the HTTP status code of the call
- html: the raw data sent back by the server (HTML, JSON, XML, etc)
In order to send a POST request (rather than GET as in this example) we just need to change one line, from:
var httpReq = getApi().newHttpGetRequest('http://some.host.com/', reqParams, httpCallback);
to:
var httpReq = getApi().newHttpPostRequest('http://some.host.com/', reqParams, httpCallback);
» File system
While browser-based JavaScript doesn't allow direct access to the local file system, Java Extensions do it via a simple API.
This is a quick overview of the file system API provided on the server side:
Method | Description |
readTextFile() | Reads a text file to a string |
writeTextFile() | Writes a string to a text file |
getCurrentFolder() | Returns the path to the current Extension folder |
copyFile() | Copy a file to another path |
moveFile() | Move a file to another path
|
deleteFile() | Delete a file |
deleteDirectory() | Deletes a directory with all of its files |
makeDirectory() | Create a new directory |
readBinaryFile() | Reads a binary file to a byte array |
writeBinaryFile() | Writes a byte array to file |
isFile() | Checks whether a certain path is a file |
isDirectory() | Checks whether a certain path is a directory |
getFileSize() | Returns the size of a file |
Here are a couple of examples on how to work with the file system API.
» Read a text file
function readFile() { try { var path = getFileApi().getCurrentFolder(); var text = getFileApi().readTextFile(path + "tutorial05.js"); trace(text); } catch (ex) { trace("File Error: " + ex.getMessage()); } }
» Write a text file
function writeFile() { var filePath = getFileApi().getCurrentFolder() + "MyTextFile.txt"; var text = ""; for (var i = 0; i < 10; i++) text += "Line: " + i + "\n"; try { getFileApi().writeTextFile(filePath, text); trace("File written. Size: " + getFileApi().getFileSize(filePath)); } catch (ex) { trace("File Error: " + ex.getMessage()); } }
» Reading exceptions
You may encounter several different errors while developing and testing server side Extensions. Depending on what caused the error you will find different exception outputs. Here we provide a few examples to help you understand where to look in your code.
1) JavaScript native error
function onUserLogin(event) { event.nonExistentFunction() }
When this function gets called it will cause a runtime error because the invoked method does not exist:
16:20:10,491 WARN [SFSWorker:Ext:1] Extensions - <eval>:24 TypeError: { USER_JOIN_ZONE, Params: [ZONE, USER] } has no such function "nonExistentFunction" in Extension: TutorialJS, File: tutorial02.js
The message tells us immediately that we have a problem in a file called tutorial02, at line 24.
2) Syntax errors
function onUserLogin(event) { var myObj = { name: "Object", id: 30 isActive: true } }
This code will cause a JavaScript syntax error because of the missing comma after the property called id:
15:50:42,845 INFO [main] Extensions - {TutorialJS}: javax.script.ScriptException:
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Exception: javax.script.ScriptException
Message: <eval>:27:2 Expected comma but found ident
isActive: true
^ in <eval> at line number 27 at column number 2
Description: Error while loading Javascript extension
+--- --- ---+
Stack Trace:
+--- --- ---+
jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(NashornScriptEngine.java:455)
jdk.nashorn.api.scripting.NashornScriptEngine.compileImpl(NashornScriptEngine.java:522)
jdk.nashorn.api.scripting.NashornScriptEngine.compileImpl(NashornScriptEngine.java:509)
jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:397)
jdk.nashorn.api.scripting.NashornScriptEngine.eval(NashornScriptEngine.java:152)
javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
com.smartfoxserver.v2.extensions.JavascriptExtension.loadScript(JavascriptExtension.java:138)
com.smartfoxserver.v2.extensions.JavascriptExtension.init(JavascriptExtension.java:58)
com.smartfoxserver.v2.entities.managers.SFSExtensionManager.createExtension(SFSExtensionManager.java:303)
com.smartfoxserver.v2.entities.managers.SFSZoneManager.createZone(SFSZoneManager.java:426)
com.smartfoxserver.v2.entities.managers.SFSZoneManager.initializeZones(SFSZoneManager.java:239)
com.smartfoxserver.v2.SmartFoxServer.start(SmartFoxServer.java:292)
com.smartfoxserver.v2.Main.main(Main.java:14)
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Here we get a full Java stack trace but fear not, what is relevant for us is only the first part at the top, before the stack trace.
3) Java error
function onUserLogin(event) { event.getParameter("test"); }
This function will raise a Java exception when executed because the getParameter() method expects an SFSEventParam enum instead of a plain string. This in turn causes a cast error in the Java runtime which ultimately results in this error:
16:15:18,866 WARN [SFSWorker:Ext:4] Extensions - java.lang.ClassCastException:
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Exception: java.lang.ClassCastException
Message: Cannot cast java.lang.String to com.smartfoxserver.v2.core.ISFSEventParam
Description: Java Error invoking Javascript function: $$handleServerEvent
+--- --- ---+
Stack Trace:
+--- --- ---+
java.lang.invoke.MethodHandleImpl.newClassCastException(MethodHandleImpl.java:361)
java.lang.invoke.MethodHandleImpl.castReference(MethodHandleImpl.java:356)
jdk.nashorn.internal.scripts.Script$Recompilation$148$261A$\^eval\_.onUserLogin(<eval>:24)
jdk.nashorn.internal.scripts.Script$Recompilation$147$51334A$\^eval\_.$$handleServerEvent(<eval>:1007)
jdk.nashorn.internal.runtime.ScriptFunctionData.invoke(ScriptFunctionData.java:638)
jdk.nashorn.internal.runtime.ScriptFunction.invoke(ScriptFunction.java:229)
jdk.nashorn.internal.runtime.ScriptRuntime.apply(ScriptRuntime.java:387)
jdk.nashorn.api.scripting.ScriptObjectMirror.callMember(ScriptObjectMirror.java:192)
jdk.nashorn.api.scripting.NashornScriptEngine.invokeImpl(NashornScriptEngine.java:381)
jdk.nashorn.api.scripting.NashornScriptEngine.invokeFunction(NashornScriptEngine.java:187)
com.smartfoxserver.v2.extensions.JavascriptExtension.invokeFunction(JavascriptExtension.java:164)
com.smartfoxserver.v2.extensions.JavascriptExtension.handleServerEvent(JavascriptExtension.java:79)
com.smartfoxserver.v2.entities.managers.SFSExtensionManager.dispatchEvent(SFSExtensionManager.java:768)
com.smartfoxserver.v2.entities.managers.SFSExtensionManager.dispatchZoneLevelEvent(SFSExtensionManager.java:689)
com.smartfoxserver.v2.entities.managers.SFSExtensionManager.handleServerEvent(SFSExtensionManager.java:890)
com.smartfoxserver.v2.core.SFSEventManager$SFSEventRunner.run(SFSEventManager.java:65)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
java.lang.Thread.run(Thread.java:745)
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Here the actual cause of the error can look a bit more obscure because it is mixed in the Java method call chain. We have bolded the relevant line: what you want to look for is the <eval> which refers to the JavaScript code being evaluated. There you find the function that causes the problem and the line number at which it occurs.