You may also want to check the topic: Adding new type of message to GameBots (UnrealScript). This guideline covers how to add new message/command definition to Pogamut in Java. Before starting this tutorial, you should have overall knowledge of how the GameBots protocol works.
This tutorial was written for Pogamut 3.2.5, most parts are by Martin Černý.
There are two possible scenarios:
This is the easier way, however it should not be used if the new command/message is project specific.
It is in fact quite easy. You need to checkout Pogamut from SVN. There find project PogamutUT2004 (or other project you want to add a message/command to). In src/gb2004/ folder there are xml message and commands Pogamut definitions. To add a new message or commands simply add new xml file that will define it and afterwards Clean and Build the project. The message/command will be added. Use other message and commands definitions to grasp the syntax (it is fairly easy).
This is also very easy. You only need to create a java class that extends CommandMessage and write the toString method such that it transforms the command contents into GameBots format (which is quite similar to standard Java toString syntax). Best practice (as in all built-in commands) is to let the command have a no-argument constructor which sets the default values for all properties and then modify the properties with public setters that return “this”.
An example message from SpyVsSpy project follows.
import cz.cuni.amis.pogamut.base.communication.messages.*; /** Corresponding GameBots command is CHANGEDOOR. */ public class ChangeDoor extends CommandMessage { /** Creates new instance of command ChangeDoor. Corresponding GameBots message for this command is CHANGEDOOR. @param DoorFrom @param DoorTo @param Open */ public ChangeDoor( String DoorFrom, String DoorTo, boolean Open) { this.DoorFrom = DoorFrom; this.DoorTo = DoorTo; this.Open = Open; } /** Creates new instance of command ChangeDoor. Corresponding GameBots message for this command is CHANGEDOOR. <p></p>WARNING: this is empty-command constructor, you have to use setters to fill it up! */ public ChangeDoor() { } protected String DoorFrom = null; public String getDoorFrom() { return DoorFrom; } public ChangeDoor setDoorFrom(String DoorFrom) { this.DoorFrom = DoorFrom; return this; } protected String DoorTo = null; public String getDoorTo() { return DoorTo; } public ChangeDoor setDoorTo(String DoorTo) { this.DoorTo = DoorTo; return this; } protected boolean Open = false; public boolean isOpen() { return Open; } public ChangeDoor setOpen(boolean Open) { this.Open = Open; return this; } /** * Cloning constructor. */ public ChangeDoor(ChangeDoor original) { this.DoorFrom = original.DoorFrom; this.DoorTo = original.DoorTo; this.Open = original.Open; } @Override public String toString() { return toMessage(); } public String toMessage() { StringBuilder buf = new StringBuilder(); buf.append("CHANGEDOOR"); if (DoorFrom != null) { buf.append(" {DoorFrom ").append(DoorFrom).append("}"); } if (DoorTo != null) { buf.append(" {DoorTo ").append(DoorTo).append("}"); } buf.append(" {Open ").append(Open).append("}"); return buf.toString(); } }
This is the most tricky part, but in the end not very difficult. As of version 3.2.5 this is only supported by Pogamut UDK.
Let's see how this works. Pogamut internally uses a framework called Guice for dependency injection. This allows us to replace the original message parser class with a custom one. The built-in parser exposes few protected methods that allow us to easily extend it to parse additional messages. Thus adding the message involves four steps:
The only limitation to message class is that it extends InfoMessage class. However the best practice is to let it extend either GBEvent or GBObjectUpdate (for events that contain state information about a WorldObject). There are also bunch of marker interfaces that modify the processing of a message and some other useful interfaces. See the built-in messages classes in project PogamutUDK, package cz.cuni.amis.pogamut.udk.communication.messages.gbinfomessages to get some feel about what to use when. The class should also have a no-arg constructor, public getters for all fields. Setting the fields should not be public - the built in messages don't have setters for the fields, but the fields are protected. You then place the extended parser and your message classes in the same package. Here is an example of a custom message created for the SpyVsSpy project:
import cz.cuni.amis.pogamut.base.communication.worldview.event.*; import cz.cuni.amis.pogamut.base.communication.translator.event.*; import cz.cuni.amis.pogamut.udk.communication.messages.*; /** Asynchronous message. Sent when state of any door in the environment changes. Corresponding GameBots message is DOORSTATECHANGED. */ public class DoorStateChanged extends GBEvent implements IWorldEvent, IWorldChangeEvent { /** Creates new instance of command DoorStateChanged. Asynchronous message. Sent when state of any door in the environment changes. Corresponding GameBots message for this command is . @param Opened Whether the door is open after the perceived change @param DoorFrom @param DoorTo */ public DoorStateChanged(boolean Opened, String DoorFrom, String DoorTo) { this.Opened = Opened; this.DoorFrom = DoorFrom; this.DoorTo = DoorTo; } /** Whether the door is open after the perceived change */ protected boolean Opened = false; /** Whether the door is open after the perceived change */ public boolean isOpened() { return Opened; } protected String DoorFrom = null; public String getDoorFrom() { return DoorFrom; } protected String DoorTo = null; public String getDoorTo() { return DoorTo; } @Override public long getSimTime() { // NOT IMPLEMENTED FOR UDK return 0; } /** * Cloning constructor. */ public DoorStateChanged(DoorStateChanged original) { this.Opened = original.Opened; this.DoorFrom = original.DoorFrom; this.DoorTo = original.DoorTo; } /** * Used by Yylex to create empty message then to fill it's protected fields (Yylex is in the same package). */ public DoorStateChanged() { } @Override public String toString() { return super.toString() + " | " + "Opened = " + String.valueOf(Opened) + " | " + "DoorFrom = " + String.valueOf(DoorFrom) + " | " + "DoorTo = " + String.valueOf(DoorTo) + " | " + ""; } }
The built in parser is created by JFlex and has a rather nasty internals. However, unless you are trying to achieve something special, you will not need to look into it. Whenever the built-int parser encounters a message name it does not recognize, it calls protected method tryParsingUnprocessedMessage
if it returns a message instance, the parser treats the message as recognized and passes all subsequent parameters of the message to tryParsingUnprocessedMessageParameter
method. Note that, by design of gamebots protocol, all parameters are optional, so you cannot rely on the fact that all message parameters will be set, neither can you rely on the order of the parameters. Once the parser encounters the end of a message, it sends the message object as is for further processing (usually to the WorldView).
An example parser, that allows to parse the above message follows:
import cz.cuni.amis.pogamut.base.communication.messages.InfoMessage; import cz.cuni.amis.pogamut.udk.communication.messages.gbinfomessages.Yylex; /** * Extended Yylex to parse SpyVsSpy specific messages * @author Martin Cerny */ public class SpyVsSpyYylex extends Yylex{ @Override protected InfoMessage tryParsingUnprocessedMessage(String messageName){ if(messageName.equals("DOORSTATECHANGED")){ //if the message was recognized, return a freshly created message object return new DoorStateChanged(); } else { //message not recognized, delegate to super return super.tryParsingUnprocessedMessage(messageName); } } @Override protected boolean tryParsingUnprocessedMessageParameter(String paramName, String wholeParamText){ //Get the type of currently parsed object - this way our parser needs no internal state if(getParsedObject() instanceof DoorStateChanged){ DoorStateChanged doorStateChanged = (DoorStateChanged)getParsedObject(); if(paramName.equals("DoorFrom")){ doorStateChanged.DoorFrom = stringValue(wholeParamText); //returning true to signal that the parameter was recognized return true; } else if(paramName.equals("DoorTo")){ doorStateChanged.DoorTo = stringValue(wholeParamText); return true; } else if(paramName.equals("Opened")) { doorStateChanged.Opened = booleanValue(wholeParamText); return true; } } //parameter not recognized, delegate to super return super.tryParsingUnprocessedMessageParameter(paramName, wholeParamText); } }
This is easy, an example (from the SpyVsSpy project) follows:
import cz.cuni.amis.pogamut.spyvsspy.communication.SpyVsSpyYylex; import com.google.inject.AbstractModule; import cz.cuni.amis.pogamut.udk.agent.params.UDKAgentParameters; import cz.cuni.amis.pogamut.udk.bot.IUDKBotController; import cz.cuni.amis.pogamut.udk.communication.parser.IUDKYylex; import cz.cuni.amis.pogamut.udk.factory.guice.remoteagent.UDKBotModule; public class SpyVsSpyBotModule<PARAMS extends UDKAgentParameters> extends UDKBotModule<PARAMS> { public SpyVsSpyBotModule(Class<? extends IUDKBotController> botControllerClass) { super(botControllerClass); } @Override protected void configureModules() { super.configureModules(); addModule(new AbstractModule() { @Override public void configure() { //bind our parser instead of the built-in one bind(IUDKYylex.class).to(SpyVsSpyYylex.class); } }); } }
Now you can run you bot with following code (it is nearly the same as the default startup code, only the module is replaced with our custom module):
UDKBotRunner runner = new UDKBotRunner(new SpyVsSpyBotModule(<<YOURBOTCLASS>>.class), "SpyVsSpy"); runner.setMain(true); runner.startAgent();