Skip to content

Extension recipes

In this document we have collected a number of common uses cases that you may encounter when developing games with SmartFoxServer. You can use them as recipes to implement a number of different oft-needed functionalities.

For convenience you can use the right side menu to jump between each recipe.

Setting up an external DB

SmartFoxServer allows to connect to databases in a way that is very similar to its predecessors. All you need to do is downloading the JDBC connector from your Database vendor (e.g. MySQL, Postgres) and drop the .jar under server/extensions/_shared/.

Here's a list of popular RDBMS and their JDBC download pages:

In case you're using a non relational database, such as MongoDB or Redis things can be a little more complicated as you will find both native APIs and JDBC-based API (which may be non-official). We recommend consulting the vendor's documentation to evaluate which is the best approach based on your use case.

The next step is opening the AdminTool, launch the Zone Configurator module, select your Zone and click on the Database manager tab to edit the configuration.

Database config

  • Database driver class: the fully qualified name of the Driver's class. It is provided by the vendor's documentation
  • Connection string: this is also provided by the vendor. The last parameter in the string should be the name the dabase you want to connect to
  • Username: the user name for connecting to the DB
  • Password: the password for connecting to the DB
  • Test SQL: a short, valid SQL expression used to test the validity of a connection
  • Maximum # of active connections: the maximum amount of connections open at the same time. When this value is reached the current thread will block when trying to acquire a new connection until some of the busy connections are released.
  • Interval for keep-alive signal: an interval in minutes use to keep the connections parked in the connection pool

Note:

SmartFoxServer uses a connection pooling mechanism to keep unused connections alive and ready to be reused by a different Extension, to optimize access times.

After configuring the access you can restart SmartFoxServer. You can check the logs to verify no error occurred during the boot. Any issues connecting to an external DB will result in one or multiple error messages.

Querying the DB

Once the database is accessible we can start interacting with it. SmartFoxServer offers two main ways to perform queries:

  1. Using the DBManager interface, provided by SmartFoxServer, it simplifies the operations of querying, inserting, updating and removing records. When using this system the life cycle of the connection is auto-managed behind the scenes.
  2. Using the JDBC API, which give more in-depth and fine grained access to your interaction with the DB. When using this approach the developer is responsible for managing the life-cycle of the connections.

Given a simple database table called accounts with fields id, name, email, is_active, let's see how we can query it using both approaches:

Using the DBManager

    void searchUsers(boolean active)
    {
        var dbManager = getParentZone().getDBManager();
        var sql = "SELECT * FROM accounts WHERE is_active=? LIMIT 0,10";

        try
        {
            ISFSArray rows = dbMan.executeQuery(sql, new Object[]{ active });

            if (rows.size() < 1) {
                trace("No records were found");
            } else {

                for (int i = 0; i < rows.size(); i++)
                {
                    ISFSObject row = rows.getSFSObject(i);
                    trace("Id: " + row.getLong("id"));
                    trace("User: " + row.getString("name"));
                }
            }       
        }
        catch (SQLException ex)
        {
            getLogger().warn("Unexpected database error", ex);
        }
    }

We start by getting a reference to the parent Zone's DBManager (the one we configured via the AdminTool). Next we declare the query using a '?' placeholder for any dynamic parameter. The final query will be composed by the executeQuery() method where we specify an array of parameters, following the same order of the placeholders in the query.

executeQuery() returns an ISFSArray containg all the rows found. Each row is a different ISFSObject containing the record data.

With a similar approach you can also use:

  • DBManager.executeInsert() to insert new rows in the table
  • DBManager.executeUpdate() to update existing records
  • DBManager.executeQuery() with a DELETE statement to delete records from the table

Using the JDBC API

    void searchUsers(boolean active)
    {
        var dbManager = getParentZone().getDBManager();
        var sql = "SELECT * FROM accounts WHERE is_active=? LIMIT 0,10";

        PreparedStatement stmt = null;

        try (Connection conn = dbManager.getConnection())
        {
            stmt = conn.prepareStatement(sql);
            stmt.setObject(1, active);

            ResultSet rs = stmt.executeQuery();

            while (rs.next()) {
                long userId = rs.getInt(1);
                String userName = rs.getString(2);

                trace("User id: " + userId)
                trace("User name: " + userName)
            }
        }
        catch(SQLException sqlex)
        {
            sqlex.printStackTrace();
        }
    }

This is similar to the previous example but JDBC uses a PreparedStatement to compile the query. Similarly we're using the same placeholder approach, however note how dynamic parameters are indexed starting at 1 (rather than zero). The returned ResultSet can be navigated in a Iterator style loop: at each iteration we get a different record whose indexes start at 1 as well.

To learn more about JDBC:

Custom Login

Implementing a custom login on the server-side is a simple process. SmartFoxServer triggers the following two login events:

  • USER_LOGIN: when a client requests to join a Zone. Here we can validate the client credentials and decide if the User can continue the login process. At this stage the client is represented by a Session object, and becomes a User object if the process succeeds.

  • USER_JOIN_ZONE: notified when a client has successfully joined a Zone (the client is now represented by a User object).

Note:

The reason why we use two different objects to represent a client is that Session models the connection of the client, while User represents the logged in client. From a User object you can still access its Session via User.getSession()

1. Configure the Zone

Launch the AdminTool, open the Zone Configurator module and enable Use custom Login setting; then restart SmartFoxServer.

You can also do this programmatically:

@Override
public void init()
{
    getParentZone().setCustomLogin(true);
}

This will set the Zone parameter, allowing login events to trigger in our Extension.

2. Handling the login events

Create a new server-side Extension with an init() method that looks like this:

@Override
public void init()
{
    trace("My Custom Login extension"); 

    // Register for login event
    addEventHandler(SFSEventType.USER_LOGIN, this::onLogin);
}

private void onLogin(ISFSEvent event) throws SFSException
{
    String name = (String) event.getParameter(SFSEventParam.LOGIN_NAME); 

    if (name.equals("Gonzo") || name.equals("Kermit"))
    {
        // Create the error code to send to the client
        SFSErrorData errData = new SFSErrorData(SFSErrorCode.LOGIN_BAD_USERNAME);
        errData.addParameter(name);

        // Fire a Login exception
        throw new SFSLoginException("Gonzo and Kermit are not allowed in this Zone!", errData);
    }
}

Essentially our onLogin handler must verify if the user name and password sent by the client are valid, and throw a specific Exception in case we want to reject the request.

In a real-world scenario you will likely use a database or remote service to validate the credentials and use the same mechanism to deny access, when it's needed.

When firing an SFSLoginException we provide a message (logged on the server-side) and an SFSErrorData containing the error code (LOGIN_BAD_USERNAME in this case) with the bad name itself.

Typical error codes used in this context are LOGIN_BAD_USERNAME and LOGIN_BAD_PASSWORD, both taking an additional parameter which is the wrong name or password.

If no exception is thrown the system will accept the user and continue the login process. This will check for other potential error conditions before allowing the User in. For example:

  • the Zone is full and no further clients are allowed in
  • another User with the same name is already logged in
  • the User is currently banned
  • the User name contains bad words that can't be accepted (depending on your Bad Words Filter configuration)

Once all checks are passed the client is logged in and the Zone Extension receives a USER_JOIN_ZONE event, if an event handler was subscribed to it.

Typically this is used when specific actions needs to be executed after the user has logged in, such as setting User Variables, joining a specific Room, etc.

Note:

TIP: When working with events such as USER_LOGIN and USER_JOIN_ZONE it’s a bit tricky to maintain the state of the current transaction, since events trigger asynchronously.

A convenient way to maintain the state is to use the Session object, as it allows to store custom parameters as key-value pairs using the Session.getProperty() and Session.setProperty() methods (see the JavaDoc for more details).

3. Secure passwords

The user password is never transmitted in clear from the client to the server. In order to be able to compare the encrypted password with your database original password we provide a convenient method called checkSecurePassword().

    getApi().checkSecurePassword(session, clearPass, encryptedPass);

The method call will return true if the passwords match.

Further discussion: we expand on this topic in a more in depth article that takes in consideration how credential checks should be done in production. Highly recommended.

4. Changing the user name at login time

There are cases where we need to change the name provided by the User at login time, with another one extracted from the database. An example is when the user logs in with an email address, and the Users' nickname is stored in the database.

There is a simple convention that allows us to provide an alternative name to the login system. In the USER_LOGIN event we are passed an empty SFSObject that can be used to return custom data to the client. We just need to provide the name in that object using a specific (and reserved) key name.

public void onLogin(ISFSEvent event) throws SFSException
{
    String loginName = (String) event.getParameter(SFSEventParam.LOGIN_NAME);
    ISFSObject outData = (ISFSObject) event.getParameter(SFSEventParam.LOGIN_OUT_DATA);

    // ... login logic ...

    // Generate a new name for the client
    String newName = getUserNameFromDB(loginName)
    outData.putShortString(SFSConstants.NEW_LOGIN_NAME, newName);
}

5. User permissions

If our application requires different levels of access (for different User profiles) we can configure a User’s permission id during the login phase. There are a number of preset User levels such as “Guest”, “Registered”, “Moderator”, “Admin”, plus new ones can be added if necessary.

We recommend this article to learn more about how the Privilege Manager works.

During the login phase we can setup the User’s permissions like this:

public void onLogin(ISFSEvent event) throws SFSException
{
    ISession session = (ISession) event.getParameter(SFSEventParam.SESSION);

    // ... login logic ...

    session.setProperty(SFSConstants.PROP_USER_PERMISSION, DefaultPermissionProfile.MODERATOR);
}

With the User permissions configured, every request received from that client will be checked against the User's profile and rejected in case the permissions don't support it. Denied requests are logged as errors in the server's log files.