Introduction

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ý.

What to modify

There are two possible scenarios:

  1. Adding a new command/message to the Pogamut core.
  2. Adding a new project-specific command message without modifying the Pogamut core (currently fully supported only for Pogamut UDK)

Adding command/message to the Pogamut core

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).

Adding project specific command

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();
    }
}

Adding project specific message

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:

  1. Create a class the represents the message
  2. Extend Yylex class to parse the message
  3. Create a custom Guice module that replaces the built-in parser with the new one
  4. Modify our bot so that it uses the custom module

Creating a class for the message

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) + " | "
                + "";
 
    }
 
 
}

Extending the Yylex parser

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);            
        }            
}

Creating the Guice module

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);                                
			}
 
		});
	}
 
}

Running the bot

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();