Skip to content
Max S edited this page Mar 21, 2017 · 4 revisions

Room Manager

Location: snipe/packages/room/

This package adds client room manager to the slave servers. If needed, rooms can be persistent and store/restore data in data blocks. The room instances can also run a "tick" method periodically that can be used for server-side quasi real-time logic.

You can include this package into any slave server with:

  override function initModules()
    {
      loadModules([
        snipe.packages.room.SlaveModule,
        ]);
    }

You can include this package into the cache server with:

      loadModules([
        snipe.packages.room.CacheModule,
        ]);

Slave server configuration variables:

  • "packages/room.logEnabled" - (0/1) Enables optional package logs. Disabled by default.

The Snipe server core includes a full example for a room-based tic-tac-toe game. It is located in "examples/tictactoeRoom/" directory. In this example users will be put into a game through the matchmaking queue (using the basic matchmaking package).

You will need to register the room type on the slave server. Let's see the code from the example:


class GameModule extends Module<TestClient, ServerTest>
{
  public function new(srv: ServerTest)
    {
      super(srv);
      name = "game";

      // tic-tac-toe room
      var module: snipe.packages.room.GameModule =
        server.getModule("packages/room");
      module.registerType({
        id: "tictactoe",
        roomClass: TicTacToeRoom,
        hasTick: true,
        tickTime: 1,
        replicated: [ "field", "turn" ]
        });
    }

  // ...
}

This code defines the "tictactoe" room type that is contained in the "TicTacToeRoom" class that runs its tick method once per second. The fields replicated to the client are "field" and "turn". Note that you can completely bypass the replication system and send the messages yourself.

The "TicTacToeRoom" class contains the room instance variables and methods:

class TicTacToeRoom extends Room
{
  var server: ServerTest;

  var lastMoveTS: Array<Float>;
  var field: Array<Array<Int>>; // 0 - empty, 1 - player 1, 2 - player 2
  var userID1isX: Bool; // who"s X
  var turn: Bool; // false - player 1 turn, true - player 2 turn

  public function new(s, t, vid)
    {
      super(s, t, vid);
      server = cast s;

      lastMoveTS = [ Sys.time(), Sys.time() ];
      field = [ [ 0, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ];
      userID1isX = true;
      turn = false; // for testing simplicity, user 1 always starts first
    }


// kill room on client leave
  override function leavePost(c: ClientInfo)
    {
      // [contents omitted for brevity]

      isDead = true;
      server.roomModule.remove(typeID, id);
    }


// room tick
  public override function tick(timePassed: Float)
    {
      // check if current player is taking too long to make a move
      // and auto-stop the game if he does
      var idx = (!turn ? 0 : 1);
      if (Sys.time() - lastMoveTS[idx] >= 60)
        leave(clients[idx]);
    }


// put chip on x,y
  public function move(c: TestClient, x: Int, y: Int): Dynamic
    {
      // [contents omitted for brevity]

      // send updated field to both clients
      updated("field");
      updated("turn");
      replicate();

      return null;
    }
}

We also need to create the room instance and get access to it when the user makes a move request. The relevant code is this:

class GameModule extends Module<TestClient, ServerTest>
{
  // ...

// call method from this module
  public override function call(client: TestClient, type: String, params: Params): Dynamic
    {
      var response = null;

      if (type == "game.move")
        response = move(client, params);

      return response;
    }


// ...


// NOTIFY: start a new game
  function start(msg: { ids: Array<Int> })
    {
      server.log("game", "start, msg: " + msg);

      var c1 = server.getClient(msg.ids[0], true);
      var c2 = server.getClient(msg.ids[1], true);
      // one of clients disconnected
      if (c1 == null || c2 == null)
        return;

      // one of clients is in wrong state
      if (c1.state != "" || c2.state != "")
        throw "game.start(): wrong state";

      var room = server.roomModule.create("tictactoe");
      room.join(c1);
      room.join(c2);
      c1.state = "game";
      c2.state = "game";

      // notify clients
      c1.response("game.start", { errorCode: "ok" });
      c2.response("game.start", { errorCode: "ok" });
    }


// CALL: make a move
  function move(c: TestClient, params: Params): Dynamic
    {
      var x = params.getInt("x");
      var y = params.getInt("y");

      if (c.state != "game")
        throw "game.move(): wrong state";
      if (x < 0 || y < 0 || x > 2 || y > 2)
        throw "game.move(): out of field bounds";

      var room: TicTacToeRoom = cast server.roomModule.getClientRoom(c);
      if (room == null)
        throw "game.move(): no game room";

      var ret = room.move(c, x, y);
      return ret;
    }
}

Note how the "start()" method is seemingly not called from anywhere. This is due to the example using the basic matchmaking package. Clients send requests to join the matchmaking queue and after they are successfully matched against one another, the cache server match handler sends the notification to the game server to call the "start()" method. It then creates a new room instance and notifies both clients.

If you take a closer look at the example, you will also notice that clients can be on different server threads and one of them will be migrated to the other when matched.

Persistent Rooms

Let's suppose we want to store the room state when both clients leave the server and automatically reload it when they return, in other words we want the rooms to be persistent. The room manager package supports this by using data blocks to store room state and load it when the room instance is created with the same ID.

First we will need to register the room cache data block type on the cache server:

class GameModuleCache extends ModuleCache<CacheServerTest>
{
  public function new(s: CacheServerTest)
    {
      super(s);
      name = "game";

      var module: snipe.packages.room.CacheModule =
        server.getModule("packages/room");
      module.registerCache("game");
    }
}

Note that registering this type will automatically create the database table "Roomsgame" if it does not exist already.

Now we can define the room type as follows:


class GameModule extends Module<TestClient, ServerTest>
{
  public function new(srv: ServerTest)
    {
      super(srv);
      name = "game";

      // persistent tic-tac-toe room
      var module: snipe.packages.room.GameModule =
        server.getModule("packages/room");
      module.registerType({
        id: "tictactoe",
        roomClass: TicTacToeRoom,
        hasTick: true,
        isPersistent: true,
        tickTime: 1,
        replicated: [ "field", "turn" ],
        persistent: [ "field", "turn" ]
        });
    }

  // ...
}

This is a simplified example. You will probably need to store more information about the game to fully restore the state.

Now calling the "Room.updated()" method will mark the fields as updated for updating the cache block on the cache server when the room is destroyed.

The room block is locked while the room instance exists on the slave server and unlocked after it is destroyed. You will have to keep the room ID somewhere in the user blocks to be able to connect to this room later on. Also keep in mind that by default all rooms on separate threads are created with IDs unique to the particular slave server thread. If you try to create the persistent room with the same ID on a different slave server, this operation will fail due to the room block already being locked. This is a limitation of persistent rooms package.

Clone this wiki locally