Marauroa Chat Tutorial/Server

From Arianne
Jump to: navigation, search


Marauroa Tutorial


Code

In order to create a Marauroa-based game server you must provide at least implementation of the marauroa.server.game.rp.IRPRuleProcessor interface and a class for zone management, i.e. marauroa.server.game.rp.RPWorld descendant. You can use the marauroa.server.game.rp.RPWorld itself, but it doesn't provide you with any RPZones, while you will almost certainly need at least one.

We will start with the following implementation of the RPWorld

import marauroa.server.game.rp.RPWorld;
import marauroa.server.game.rp.MarauroaRPZone;

public class World extends RPWorld {
  private static World instance;

  public static World get() {
    if (instance == null) {
      instance = new World();
    }
    return instance;
  }
  
  @Override
  public void onInit() {
    super.onInit();
    MarauroaRPZone zone = new MarauroaRPZone("lobby");
    addRPZone(zone);
  }
}

Don't forget to always implement the static get method which is used by Marauroa framework to retrieve an instance of your class.

onInit method is called when world is created. The only thing we do there is creating our zone (as we have a chat application we call it "lobby"). Zone is a general notion of Marauroa. It represents a game area. All clients residing in a certain area receive notification on data changes in this area only. Chat rooms are a good example, as if you are in a chat room you don't see the conversations in other rooms, i.e. you receive notifications on new things in your current room only.

Implementing IRPRuleProcessor will require more code, as we must implement all the methods of the interface. We will start with the following stub

import java.sql.SQLException;

import marauroa.common.crypto.Hash;
import marauroa.common.game.AccountResult;
import marauroa.common.game.CharacterResult;
import marauroa.common.game.IRPZone;
import marauroa.common.game.RPAction;
import marauroa.common.game.RPObject;
import marauroa.common.game.Result;
import marauroa.server.game.db.DAORegister;
import marauroa.server.game.db.AccountDAO;
import marauroa.server.game.db.CharacterDAO;
import marauroa.server.db.TransactionPool;
import marauroa.server.db.DBTransaction;
import marauroa.server.game.rp.IRPRuleProcessor;
import marauroa.server.game.rp.RPServerManager;


public class Rule implements IRPRuleProcessor {
  private static Rule instance;

  private World world = World.get();

  private RPServerManager manager;

  public static IRPRuleProcessor get() {
    if (instance == null) {
      instance = new Rule();
    }
    return instance;
  }

  @Override
  public void setContext(RPServerManager rpman) {
    manager = rpman;
  }

  @Override
  public boolean checkGameVersion(String game, String version) {
    return game.equals("Chat");
  }

  @Override
  public synchronized void onTimeout(RPObject character) {
    onExit(character);
  }

  @Override
  public synchronized boolean onExit(RPObject character) {
    world.remove(character.getID());
    return true;
  }

  @Override
  public synchronized boolean onInit(RPObject character) {
    IRPZone zone = world.getRPZone(new IRPZone.ID("lobby"));
    zone.add(character);
    return true;
  }

  @Override
  public synchronized void beginTurn() {
  }

  @Override
  public boolean onActionAdd(RPObject caster, RPAction action, List<RPAction> actionList) {
    return true;
  }

  @Override
  public synchronized void endTurn() {
  }

  @Override
  public void execute(RPObject caster, RPAction action) {
    if (action.get("type").equals("chat")) {
      RPObject chatEntry = new RPObject();
      chatEntry.put("text", action.get("text"));
      chatEntry.put("from", caster.get("nick"));
      chatEntry.put("turn", manager.getTurn());
      IRPZone zone = world.getRPZone(new IRPZone.ID(caster.getID().getZoneID()));
      zone.assignRPObjectID(chatEntry);
      zone.add(chatEntry);
    }
  }

  @Override
  public AccountResult createAccount(String username, String password, String email) {
    TransactionPool transactionPool = TransactionPool.get();
    DBTransaction trans = transactionPool.beginWork();
    AccountDAO accountDAO = DAORegister.get().get(AccountDAO.class);
    try {
      if (accountDAO.hasPlayer(trans, username)) {
        return new AccountResult(Result.FAILED_PLAYER_EXISTS, username);
      }
      accountDAO.addPlayer(trans, username, Hash.hash(password), email);
      transactionPool.commit(trans);
      return new AccountResult(Result.OK_CREATED, username);
    } catch (SQLException e1) {
      transactionPool.rollback(trans);

      return new AccountResult(Result.FAILED_EXCEPTION, username);
    }
  }

  @Override
  public CharacterResult createCharacter(String username, String characterName, RPObject template) {
    TransactionPool transactionPool = TransactionPool.get();
    DBTransaction trans = transactionPool.beginWork();
    CharacterDAO characterDAO = DAORegister.get().get(CharacterDAO.class);
    try {
      if (characterDAO.hasCharacter(trans, username, characterName)) {
        return new CharacterResult(Result.FAILED_CHARACTER_EXISTS, characterName, template);
      }
      IRPZone zone = world.getRPZone(new IRPZone.ID("lobby"));
      RPObject character = new RPObject(template);
      character.put("nick", characterName);
      zone.assignRPObjectID(character);
      characterDAO.addCharacter(trans, username, characterName, character);
      transactionPool.commit(trans);
      return new CharacterResult(Result.OK_CREATED, characterName, character);
    } catch (Exception e1) {
      transactionPool.rollback(trans);

      return new CharacterResult(Result.FAILED_EXCEPTION, characterName, template);
    }
  }
}

We again implement the static get() method which is used by Marauroa to retrieve the RuleProcessor instance.

Most of the functions are simple stubs which one can replace with code that actually does something.

You can use checkGameVersion() to reject clients with outdated version. In our case we just require game name to be Chat.

Function onInit() is invoked each time a player logins successfully into the game. His character is loaded from the database. We add the player to the lobby zone immediately, because everybody who connects to the chat gets to the lobby first.

The most important function is execute() which is invoked each time server receives action from one of the clients. In this case we are waiting for a "chat" action. As soon as we receive one - a new object is created that represents one chat message. We also set some other properties of the message: the nickname of the person who sent this message, the turn number when the message was sent.

Functions for creating account and character should find out whether to create a new account/character or not. In our case we just always do it (not for duplicates of course). Result of this actions is instantly written to the database. Note that client can provide a template for the avatar object (an RPObject associated with the character). It is up to you how to use it while constructing the actual avatar object. We take what client provides, add a "nick" property (the same as character name) and use the resulting one as an avatar object.

Deployment

So, we have two files, World.java and Rule.java, which contain the classes mentioned above.

On Windows, you can compile them using command

javac -cp marauroa.jar;log4j.jar;. *.java

On Linux and MacOSX, you have to replace the ";" with ":".

javac -cp marauroa.jar:log4j.jar:. *.java

This command assumes that you have source files, marauroa.jar and log4j.jar in the same directory. You will receive World.class and Rule.class output files.

In order to run the server you will also need server.ini file:

database_adapter=marauroa.server.db.adapter.H2DatabaseAdapter
jdbc_url=jdbc:h2:~/clientserverchat/database/h2db;AUTO_RECONNECT=TRUE;DB_CLOSE_ON_EXIT=FALSE
jdbc_class=org.h2.Driver

# TCP port
tcp_port=5555

# World and RP configuration. Don't edit.
world=World
ruleprocessor=Rule

turn_length=300

Please run the following command to generate a key pair which will be used to encrypt the login information. Simply copy the output at the end of server.ini.

java -cp marauroa.jar marauroa.tools.GenerateKeys


Now you are all set to get your server up and running. To start the server I usually use the following command

java -cp marauroa.jar;h2.jar;log4j.jar;. marauroa.server.marauroad -c server.ini

Again, on Linux and MacOSX, you have to replace the ";" with ":".

java -cp marauroa.jar:h2.jar:log4j.jar:. marauroa.server.marauroad -c server.ini

Of course, make sure that all the jars are in the current directory. Marauroa framework will parse server.ini file, find and load your classes World and Rule.

Next Steps

In the next section of this tutorial, we will write the client which will connect to our server.