Learn AI Scripting

by NeoSoft_Studios
created 05/13/04

Introduction

Welcome to the world that is AI scripting for Age of Mythology. Scripting for Age of Mythology is much more complicated than the random map scripts made for earlier Ensemble Studios games such as Age of Empires. In fact you are essentially programming in a cut down version of the C++ language called XS.

However this should not discourage potential AI Scripters as once you have learnt the basics of the AI library and have a grapple on the format in which you write the code you will find it extremely easy to produce scripts which do exactly as you want them to.

This chapter deals with the various things you must do in order to allow you to create and debug scripts within Age of Mythology. I should also point out that my guides are designed for the Titans Expansion pack as by now almost everyone that is infatuated with Age of Mythology would have the expansion pack. For those still running the original Age of Mythology, consult the documentation for AI setup.

Chapter 1: Essential Setup

Stage 1: Preparing Age of Mythology for AI Scripts

In order to set Age of Mythology up to enable editing and debugging of AI scripts you must alter the command line on the shortcut to the game. To do this right click the shortcut and select properties. Currently the shortcut’s target line should look something like this:

C:\Program Files\Microsoft Games\Age of Mythology\aomx.exe

This must be altered in order for the game to load the AI debugging tools at start-up. Append the target line to include the following statement.

+aiDebug +showAIEchoes +logAIErrors

Thus the final target line should look something like this:

"C:\Program …\aomx.exe" +aiDebug +showAIEchoes +logAIErrors

For those concerned with performance and others worried about what adding these tools to start-up will do to game play, I can only offer this comment. I have not noticed any difference in performance when running these tools at start-up nor do they seem to affect any part of the game itself. Essentially it just appears that the extra command line parameters simply make the tools visible rather than hidden.

Stage 2: Choosing an Environment for Scripting

While it is possible to build scripts using nothing more than notepad, ideally you want build in them in an editor which supports C++ files as it will format you code and help in finding syntax errors. I use Microsoft Visual Studio.NET 2003 however using a suitable version of Visual C++ such as 4, 5 or 6 should also be ok. You may also want to consider products by other companies such as Borland C++ Builder. The list below offers some free alternatives however I cannot vouch for them:

Once you have an editor to build your scripts you are ready to go!

Stage 3: AoM Enhanced Editor (Optional)

I also recommend you install the New X Editor by Reyk for Age of Mythology as it provides menu access from the Age of Mythology editor to most AI related tools within the game. You can download it from Age of Mythology Heaven here.

Also you will need the complete AOM AI Constant reference library for later use, download it from here.

Chapter 2: XS Variables

Introduction

The XS Language is not easy and unlike a language such as Visual Basic it is not immediately apparent what the code is actually doing. Fortunately for AOM AI Scripters a large amount of this does not apply as all you are essentially doing is sending commands to an AI library and because most of the time you are just interacting with this library the amount of work a scripter has to do compared to a full C++ programmer is significantly less.

This chapter focuses on learning what a variable is, how you declare them and their purpose in AI Scripts.

Variables

A variable is a combination of a user defined name e.g. “myVariable” and a specified data type such as int (a number with no decimal places) or string (a collection of letters and other characters).

Variables are used to hold information gathered from external sources or from the result of code operations. The purpose of having variables is so we can refer to the information they hold simply by using the name of the variable.

Creating a variable is extremely easy, we choose the data type in this example we will use an Int and give it a name in this case “myIntegerVariable”. Finally we must assign a default value for the variable to hold in most cases where this is unknown we use -1 for the int data type or for the string data type we use 2 speech marks. Variables in XS are written in the following format:

[DATATYPE] [VARIABLE NAME] = [DEFAULT VALUE];

Thus our example would look something like this:

int myIntegerVariable = -1;

Most editors will also format the information you have just entered using different colours e.g. the colour light blue is often used for identifying data types.

Next we add the extern XS keyword to the beginning of all variables (existing C++ programmers may complain about this but for the purpose of AOM Scripting it will reduce the chances of users encountering compilation errors). The keyword should also be recognised by most editors and its colour changed. You should now have something like this:

extern int myIntegerVariable = -1;

Finally it is important to remember that at the end of almost every line of code you must add a semicolon ( ‘;’ ) so the script knows it can move to the next line and consider it a new statement. This is most certainly the case when we DECLARE (what we have just done) a variable.

Below are some more examples, in almost every case with AOM scripting you will just be using the int, string and float data type.

extern int firstVariable = -1;
extern int secondVariable = 11;
extern string thirdVariable = "test string";
extern string fourthVariable = "";
extern float fifthVariable = 0.45;
extern float sixthVariable = 0.1;

The first variable is called “firstVariable” and is of type int with a value of -1
The second variable is called “secondVariable” and is of type int with a value of 11
The third variable is called “thirdVariable” and is of type string with a value of “test string”
The fourth variable is called “fourthVariable” and is of type string which is blank
The fifth variable is called “fifthVariable” and is of type float with a value of 0.45
The sixth variable is called “sixth Variable” and is of type float with a value of 0.1

As you now have probably guessed the float data type is used to hold decimal figures and aside from supporting a decimal, is no different to the int data type.

The final AOM data type worthy of mention is a unique data type for AOM which will not be recognised by C++ editors. It is the vector data type which contains three values which make up an X, Y and Z co-ordinate to specify the exact location of an object such as Hoplite Soldier on an AOM Map. It is written like so:

extern vector myVariable = vector(xvalhere, yvalhere, zvalhere)

However in most cases you won’t actually work out the exact vector, you will use a library function to work it out for you. For the moment you simply need to know that it exists, when it comes to using them in the script code I will cover them in more detail.

Chapter 3: XS Methods

Introduction

A method is a subroutine which is performed from start to finish on a line by line basis. A compiler (in our case AOM) will run each line of code until it has reached the end of the method at which point the subroutine is terminated.

A method contains certain information in its constructor (or heading). Fortunately for AOM you will most likely only write methods which use one formula.

Writing Methods In Code

We will begin by creating a method which MUST be present in any AOM script and must be written before any other methods. This is called the main method and is written like so:

void main (void)
{
    … your code is written here …
}

Lets now analyse what we have written – the first word (void) means that the method will run its code but will not return any information back to the rest of the script (in almost all cases this is what is necessary for most methods).

The next word is the user defined name of the method just like in the last chapter when we decided on “myVariable”. As I stated earlier every AOM script requires a main method which is the first method AOM will run when a scenario starts (however when creating your own methods you can use any name e.g. “myMethod”).

The final word is also called void and it is enclosed in brackets. This means that the method takes no parameters (additional information used by the method). In your own methods you can change this so you can include say an int variable which is used by your method. All parameters for every method are enclosed with brackets.

You will also notice an opening and closing curly bracket ( {} ), this is used to signify the start and end of the code for this method e.g. everything you write between these brackets will be performed when you run the method.

That is pretty much it for explaining methods, below are some more examples using different types so that you can get a grasp of other method types.

void myMethod (void) {…}
void myMethod (int anIntParameter = -1) {…}
void myMethod (int anIntParameter = -1, string aStringParameter = "") {…}
int myMethod (int anIntParameter = -1, string aStringParameter = "") {…}

The first example is a method called “myMethod” which takes no parameters (void) and returns no information (void).
The second example is a method called “myMethod” which takes a single int parameter called “anIntParamater” and returns no information (void).
The third example is a method called “myMethod” which takes an int parameter called “anIntParamater”, a string parameter called “aStringParameter” and returns no information (void).
The fourth example is a method called “myMethod” which takes an int parameter called “anIntParamater”, a string parameter called “aStringParameter” and when completed supplies the script with a int number.

Note: When creating your parameters (variables) in your method’s constructor (header) you must not add a semicolon at the end as shown in the second chapter. If you do, your script will not compile (succeed). Instead simply write them without the semicolons as shown below.

void myMethod (int anIntParameter = -1; ) {…} // WRONG
void myMethod (int anIntParameter = -1) {…} // RIGHT

A complete sample method (very basic):

void sayHello (void)
{
    aiEcho("Hello!");
}

The method uses a existing library function (aiEcho) to send a message (hello) to the AI Debugger. If you do not understand this, don’t worry; it will be covered later.

Chapter 4: XS Conditions

Introduction

Condition statements allow you to perform checks on an equation and determine what cause of action to take based upon it, e.g. if there are 3 Cyclopes send them into battle, if there are only 2 do nothing.

There are 2 types of condition statements in the XS language, the IF-ELSE approach or the SWITCH approach. This chapter will focus upon both. You use the if-else approach when wanting to perform a calculation, you use the switch approach when checking a value and performing an action based on that.

XS IF-ELSE Approach

The if else approach allows you to specify a check and perform a result based upon it ELSE do something different. It is written like so:

if (SOMETHING == SOMETHINGELSE)
{
    …
}
else
{
    …
}

Note than when checking if something is equal to something else we use 2 equals ‘=’ signs. This is because one equal sign means that we are setting something to equal this value such as:

int myVariable = 3;
int mySVariable = 2;
myVariable = mySVariable; //this means that myVariable is now 2
if (myVariable == 3) … //this means that we are checking the value is 3

Also remember that you do not add semicolons at the end of “if else” statements because they are almost like sub methods.

The XS library supports a function called aiEcho. This sends a message (that you specify) to the debug window inside AOM (you will use the debug window to view errors and actions the script is performing). For the purposes of this chapter we will use this function to help explain what is happening. I have modified the condition to support this function and included an Int variable to assign a number to; this should help you understand how the statement works.

int myVariable = 1;
if (3 + myVariable == 6)
{
    aiEcho("The value is equal to 6!");
}
else
{
    aiEcho("The value is not equal to 6");
}

Essentially all you have to remember is the syntax. The statement is also not limited to just one else statement you can have as many as you like, but if you add more you have to supply further check statements and ideally end with a single else statement like so:

int myVariable = 1;
if (3 + myVariable == 6)
{
    aiEcho("The value is equal to 6!");
}
else If (3 + myVariable == 5)
{
    aiEcho("The value is equal to 5!");
}
else If (3 + myVariable == 4)
{
    aiEcho("The value is equal to 4!");
}
else
{
    aiEcho("The value is not 4, 5 or 6!");
}

You also do not have to have an else segment e.g.

if (3+3 == 6)
{
    aiEcho("Yes");
}

You can also include “nested” arguments (if statements inside if statements) e.g.

if (3+3 == 6)
{
    if (3+3 == 6)
    {
        aiEcho("Yes");
    }
}

XS SWITCH Approach

The SWITCH approach checks a value and performs a set of instructions based upon this value, it is written like so:

switch (VALUE TO CHECK - e.g. myVariable)
{
    case 3: //(RUN IF MATCHES "3")
    {
        aiEcho("The value is 3");
    }
    case 4:
    {
        aiEcho("The value is 4");
    }
}

So the first line sets the value we are checking, each case statement is basically a bunch of if statements without an else option. From a logical standpoint it is easier to use a switch statement with a lot of options rather than many complicated if statements.

Comments

The final item to cover is comments (they are completely unrelated to the above material). Comments are written by you to identify what is going on e.g. we have an int variable and want add three to it. To add a comment just type “//” without the speech marks and most editors should convert the text to green then you can type anything you want in any language (just remember that for each new line you must have a “//” at the beginning e.g.

// Add 3 to the variable’s value
// This is another comment
myVariable = myVariable + 3;

To create a long paragraph or header for your script you can start with a “/*” option. Until you create “*/” at the end, everything in between will be considered a comment so there is no need to keep adding a “//” for every line e.g.

/* this is a comment
continuing on next line
now finishing */
myVariable = 3;

Thanks to some readers for pointing out some bugs in the code!

Chapter 5: XS Scripts

Introduction

You now have a basic enough knowledge to build AI Scripts. The remainder of this series will talk about the various library functions you can use and will provide introductions to the various plans you can use such as attack, defend and scouting.

This chapter (and future chapters) will be accompanied with support files (a scenario and a script). These have and will be uploaded to the AI files section of the AOM Heaven download library. They can be downloaded here.

The file you are looking for is tutorial 1.

Analysing the Script

If you open the file “Training1.xs” you will see the code for our sample script.

As much of the syntax has already been discussed we will be looking at the new material only. If you have questions, review earlier chapters or post a message.
We will hence start inside the void main (void) method just below the “{” line.

The first three lines have aiEcho statements. These send the message enclosed in the double quotes to the debug window. (this can be accessed inside the AOM editor by pressing the ALT+Left Shift+D keys together). Thus:

aiEcho("Hello"); //sends the word hello to the debugger

The next three lines are system calls, if I am totally honest I am not sure what the last 2 do, however scripts seem to work a lot better with them in then out so put them in every script and forget about it.

Now we can begin our scouting plan. The map includes an already placed priest which will be our scout unit. First we have to create an int variable for each plan which houses the unique number AOM uses to reference the plan. Next we declare that we are going to create a plan, which is called “Explore Land” and is of type “cPlanExplore”. This is then assigned to our variable like so:

int planID = aiPlanCreate("Explore Land", cPlanExplore);

Next we set the parameters of the plan (NOTE that every time we want to set a parameter of a specific plan we have to reference it by using the int variable, in this case “planID”).

The next line as you might guess adds a free unit to the plan which will be used for scouting. Firstly we reference the plan (this parameter applies to “planID”), secondly we specific the type of unit to add “cUnitTypePriest”, thirdly we specify our must, need and want values for the number of priests we want in the plan. For this simple plan we set these all to 1.

Note: All units have a prefix of “cUnitType”, plans have “cPlan” and all technologies have a “cTech” prefix because they are constant values. There are also many other items in the reference file which contains these constant related prefixes.

aiPlanAddUnitType(planID, cUnitTypePriest, 1, 1, 1); //add 1 priest to the plan

The next parameter is very common, we are specifying a parameter of type boolean (true or false). To determine what type to use e.g. aiPlanSetVariableBool, aiPlanSetVariableInt etc. check the reference file mentioned at the beginning of this series for the parameter’s type e.g. the line in our code:

aiPlanSetVariableBool(planID, cExplorePlanDoLoops, 0, false);

is of type aiPlanSetVariableBool because cExplorePlanDoLoops is listed in the reference file as a function which returns a boolean result. (More on this later.)

Back to the code, we are setting the plan (planID) to use a boolean value for the result of cExplorePlanDoLoops. We set it to false meaning that we do not want the priest to do loops, just random searches. Just ignore the zero.

Finally we set the plan to be active (e.g. we have completed setting the parameters and we are ready for him to scout.

aiPlanSetActive(planID); Set the "planID" plan to be active

Lastly we send a debug message (aiEcho) saying we have finished. Simple, yes?

The next chapter will expand on this script by adding more functionality.

Chapter 6: More on XS Scripts

Introduction

In this chapter we will expand upon the scouting plan by adding a separate plan which sends all herdable animals that we find while scouting back to our town centre.

Analysing the Script

To achieve this function we add the following code to our script just after the last line of the scouting plan and just before our final debug message saying the end of the script has been reached.

// Send all herdable animals found on the scout back to the base
int herdPlanID = aiPlanCreate("Gather Herdable Plan", cPlanHerd);
aiPlanAddUnitType(herdPlanID, cUnitTypeHerdable, 0, 100, 100);
aiPlanSetVariableInt(herdPlanID, cHerdPlanBuildingTypeID, 0, cUnitTypeSettlementLevel1);
aiPlanSetActive(herdPlanID);

The first line is of course comment.

The second line is our plan creator function, we choose a herdable plan “cPlanHerd” and call it “Gather Herdable Plan”.

The third line tells the plan to gather all units of type “cUnitTypeHerdable”.

The fourth line tells the plan to send all animals to “cUnitTypeSettlementLevel1” (early in development it was not necessarily going to be called a town centre but this is what the AI considers a town centre as opposed to “cUnitTypeSettlement” which is just an uncolonised settlement.

The final line sets the plan to be active. Now if you update your script to include this and place some herdable animals on the map e.g. goats, the priest will seek them out and send them back to your town centre!

The next chapter will focus on creating a basic economy.

Chapter 7: Economy

Introduction

This will add some basic economy functions to your script which we will work on in the next chapter so that they become a true economy.

Note: You must add a cinematic block to the map and alter the script to the new block’s value (mentioned in the code). You also need to add some farms, villagers, gold and forests.

The Code

Add just below the gather herdable plan.

// ----- CREATE A BASIC ECONOMY -----

// For our early economy we will just create a huge base
// to encompass everything, as we proceed in later chapters
// this will be refined to be much more controlable.
kbBaseDestroyAll(1);
int myMainBase = kbBaseCreate(1, "Main Base", kbGetBlockPosition("685"), 75.0);

// NEW FUNCTION - kbGetBlockPosition, this returns a vector
// which represents an exact location of a Cinematic Block
// object which we place throughout the map for the AI to use
// as markers. You must create one for the script to use, in the
// AOM Editor use place object (cinematic block). Find the object
// info menu option and note the name value e.g. 1234. With this
// you can write the function kbGetBlockPosition("1234"). Note that
// this function is probably the most widely used XS function so
// remember it !!!!! Also remember if you do not add the block to your
// map and modify the script you will get some strange results.

// First we set that all resources go into a single account
kbEscrowSetPercentage(cEconomyEscrowID, cAllResources, 0.0);
kbEscrowSetPercentage(cMilitaryEscrowID, cAllResources, 0.0);
kbEscrowAllocateCurrentResources();

// Next we set all gathered resources and farms to be part of
// root account. We also set the economy to use this script only
// and not to base actions on cost. (You can ignore this and just)
// always use the default values.
aiSetAutoGatherEscrowID(cRootEscrowID);
aiSetAutoFarmEscrowID(cRootEscrowID);
aiSetResourceGathererPercentageWeight(cRGPScript, 1);
aiSetResourceGathererPercentageWeight(cRGPCost, 0);

// Set the importance of each resource (priorities)
// You can tailor these to your desire
kbSetAICostWeight(cResourceFood, 1.00);
kbSetAICostWeight(cResourceWood, 0.75);
kbSetAICostWeight(cResourceGold, 0.75);
kbSetAICostWeight(cResourceFavor, 2.00);

// Set percentages for each resource to be gathered by villagers
// To get this you determine how many villagers you want to chop
// wood (6) and then how many villagers you have in total (31).
// 6 / 31 = 0.19 then you assign that value to the appropriate line
// e.g. aiSetResourceGathererPercentage(cResourceWood, 0.19, false, cRGPScript);
aiSetResourceGathererPercentage(cResourceFood, 0.4, false, cRGPScript);
aiSetResourceGathererPercentage(cResourceWood, 0.2, false, cRGPScript);
aiSetResourceGathererPercentage(cResourceGold, 0.2, false, cRGPScript);
aiSetResourceGathererPercentage(cResourceGold, 0.2, false, cRGPScript);
aiNormalizeResourceGathererPercentages(cRGPScript);

// Finally we assign the types of each resource we collect
// Food - from farms, fish and herds
// Wood - from forests
// Gold - from mines
// Favor- from temples, town centres, monuments, fighting

// Note you must select a sub type for food, type Easy will not
// do anything, farming is usually the easiest.
aiSetResourceBreakdown(cResourceFood, cAIResourceSubTypeFarm, 1, 50, 1.0, myMainBase);
aiSetResourceBreakdown(cResourceWood, cAIResourceSubTypeEasy, 1, 50, 1.0, myMainBase);
aiSetResourceBreakdown(cResourceGold, cAIResourceSubTypeEasy, 1, 50, 1.0, myMainBase);
aiSetResourceBreakdown(cResourceFavor, cAIResourceSubTypeEasy, 1, 50, 1.0, myMainBase);