Marauroa Chat Tutorial/Server: Difference between revisions
imported>Hendrik Brummermann m header and footer |
imported>Hendrik Brummermann added missing call to world.initialize() as reported by maxgmer |
||
| (26 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
{{Navigation for Marauroa Top|Using}} |
{{Navigation for Marauroa Top|Using}} |
||
{{Navigation for Marauroa Users}} |
{{Navigation for Marauroa Users}} |
||
{{Marauroa Tutorial}} |
{{Marauroa Chat Tutorial}} |
||
| Line 18: | Line 18: | ||
if (instance == null) { |
if (instance == null) { |
||
instance = new World(); |
instance = new World(); |
||
instance.initialize(); |
|||
} |
} |
||
return instance; |
return instance; |
||
} |
} |
||
@Override |
|||
public void onInit() { |
public void onInit() { |
||
super.onInit(); |
super.onInit(); |
||
| Line 38: | Line 40: | ||
<!-- Please, see details here http://stendhal.game-host.org/wiki/index.php/Refactoring_Database_Access_in_Marauroa. --> |
<!-- Please, see details here http://stendhal.game-host.org/wiki/index.php/Refactoring_Database_Access_in_Marauroa. --> |
||
<source lang="java"> |
<source lang="java"> |
||
import java.util.List; |
|||
import java.sql.SQLException; |
import java.sql.SQLException; |
||
| Line 71: | Line 72: | ||
} |
} |
||
@Override |
|||
public void setContext(RPServerManager rpman) { |
public void setContext(RPServerManager rpman) { |
||
manager = rpman; |
manager = rpman; |
||
} |
} |
||
@Override |
|||
public boolean checkGameVersion(String game, String version) { |
public boolean checkGameVersion(String game, String version) { |
||
return game.equals("Chat"); |
return game.equals("Chat"); |
||
} |
} |
||
@Override |
|||
public synchronized void onTimeout(RPObject |
public synchronized void onTimeout(RPObject character) { |
||
onExit( |
onExit(character); |
||
} |
} |
||
@Override |
|||
public synchronized boolean onExit(RPObject |
public synchronized boolean onExit(RPObject character) { |
||
world.remove( |
world.remove(character.getID()); |
||
return true; |
return true; |
||
} |
} |
||
@Override |
|||
public synchronized boolean onInit(RPObject |
public synchronized boolean onInit(RPObject character) { |
||
IRPZone zone = world.getRPZone(new IRPZone.ID("lobby")); |
IRPZone zone = world.getRPZone(new IRPZone.ID("lobby")); |
||
zone.add( |
zone.add(character); |
||
return true; |
return true; |
||
} |
} |
||
@Override |
|||
public synchronized void beginTurn() { |
public synchronized void beginTurn() { |
||
} |
} |
||
@Override |
|||
public boolean onActionAdd(RPObject caster, RPAction action, List<RPAction> actionList) { |
public boolean onActionAdd(RPObject caster, RPAction action, List<RPAction> actionList) { |
||
return true; |
return true; |
||
} |
} |
||
@Override |
|||
public synchronized void endTurn() { |
public synchronized void endTurn() { |
||
} |
} |
||
@Override |
|||
public void execute(RPObject caster, RPAction action) { |
public void execute(RPObject caster, RPAction action) { |
||
if (action.get("type").equals("chat")) { |
if (action.get("type").equals("chat")) { |
||
RPObject |
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())); |
IRPZone zone = world.getRPZone(new IRPZone.ID(caster.getID().getZoneID())); |
||
zone.assignRPObjectID( |
zone.assignRPObjectID(chatEntry); |
||
zone.add( |
zone.add(chatEntry); |
||
} |
} |
||
} |
} |
||
@Override |
|||
public AccountResult createAccount(String username, String password, String email) { |
public AccountResult createAccount(String username, String password, String email) { |
||
TransactionPool transactionPool = TransactionPool.get(); |
TransactionPool transactionPool = TransactionPool.get(); |
||
| Line 134: | Line 145: | ||
} |
} |
||
@Override |
|||
public CharacterResult createCharacter(String username, String |
public CharacterResult createCharacter(String username, String characterName, RPObject template) { |
||
TransactionPool transactionPool = TransactionPool.get(); |
TransactionPool transactionPool = TransactionPool.get(); |
||
DBTransaction trans = transactionPool.beginWork(); |
DBTransaction trans = transactionPool.beginWork(); |
||
CharacterDAO characterDAO = DAORegister.get().get(CharacterDAO.class); |
CharacterDAO characterDAO = DAORegister.get().get(CharacterDAO.class); |
||
try { |
try { |
||
if (characterDAO.hasCharacter(trans, username, |
if (characterDAO.hasCharacter(trans, username, characterName)) { |
||
return new CharacterResult(Result. |
return new CharacterResult(Result.FAILED_CHARACTER_EXISTS, characterName, template); |
||
} |
} |
||
IRPZone zone = world.getRPZone(new IRPZone.ID("lobby")); |
IRPZone zone = world.getRPZone(new IRPZone.ID("lobby")); |
||
RPObject |
RPObject character = new RPObject(template); |
||
character.put("nick", characterName); |
|||
zone.assignRPObjectID( |
zone.assignRPObjectID(character); |
||
characterDAO.addCharacter(trans, username, |
characterDAO.addCharacter(trans, username, characterName, character); |
||
transactionPool.commit(trans); |
transactionPool.commit(trans); |
||
return new CharacterResult(Result.OK_CREATED, |
return new CharacterResult(Result.OK_CREATED, characterName, character); |
||
} catch (Exception e1) { |
} catch (Exception e1) { |
||
transactionPool.rollback(trans); |
transactionPool.rollback(trans); |
||
return new CharacterResult(Result.FAILED_EXCEPTION, |
return new CharacterResult(Result.FAILED_EXCEPTION, characterName, template); |
||
} |
} |
||
} |
} |
||
| Line 171: | Line 183: | ||
== Deployment == |
== Deployment == |
||
So, we have two files, World.java and Rule.java, which contain the classes mentioned above. |
So, we have two files, World.java and Rule.java, which contain the classes mentioned above. |
||
On Windows, you can compile them using command |
|||
<pre> |
<pre> |
||
javac -cp marauroa.jar;log4j.jar;. *.java |
javac -cp marauroa.jar;log4j.jar;. *.java |
||
</pre> |
|||
On Linux and MacOSX, you have to replace the ";" with ":". |
|||
<pre> |
|||
javac -cp marauroa.jar:log4j.jar:. *.java |
|||
</pre> |
</pre> |
||
| Line 206: | Line 222: | ||
<pre> |
<pre> |
||
java -cp marauroa.jar;h2.jar;log4j.jar;. marauroa.server.marauroad -c server.ini |
java -cp marauroa.jar;h2.jar;log4j.jar;. marauroa.server.marauroad -c server.ini |
||
</pre> |
|||
Again, on Linux and MacOSX, you have to replace the ";" with ":". |
|||
<pre> |
|||
java -cp marauroa.jar:h2.jar:log4j.jar:. marauroa.server.marauroad -c server.ini |
|||
</pre> |
</pre> |
||
| Line 212: | Line 232: | ||
== Next Steps == |
== Next Steps == |
||
In the next section of this tutorial, we will write the '''[[Marauroa Tutorial/Text Client|client]]''' which will connect to our server. |
In the next section of this tutorial, we will write the '''[[Marauroa Chat Tutorial/Text Client|client]]''' which will connect to our server. |
||
[[Category:Marauroa]] |
[[Category:Marauroa]] |
||
{{#breadcrumbs: [[Marauroa]] | [[Navigation for Marauroa Users|Using]] | [[Marauroa Tutorial|Tutorial]] | [[Marauroa Tutorial/Server|Server]]}} |
{{#breadcrumbs: [[Marauroa]] | [[Navigation for Marauroa Users|Using]] | [[Marauroa Chat Tutorial|Tutorial]] | [[Marauroa Chat Tutorial/Server|Server]]}} |
||
Latest revision as of 22:38, 18 September 2020
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 <source lang="java"> 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();
instance.initialize();
}
return instance;
}
@Override
public void onInit() {
super.onInit();
MarauroaRPZone zone = new MarauroaRPZone("lobby");
addRPZone(zone);
}
} </source>
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 <source lang="java"> 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); } }
} </source>
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. {{#breadcrumbs: Marauroa | Using | Tutorial | Server}}