AoMR Random Map Scripting
This is an introduction to writing random map scripts in Age of Mythology Retold. It’s not a full step-by-step guide, because I think the best way to learn is to copy from existing scripts. All the game’s random map scripts are in plain-text, sitting in a folder on your computer (for me they’re inC:/Program Files (x86)/Steam/steamapps/common/Age of Mythology Retold/game/random_maps
). You can also install mods and copy from their map scripts.
Instead what I want to do in this post is give you a head start on messing with map scripting. I want you to skip the steep part of the learning curve that many of us modders struggled with, and get to the fun part of making cool maps. First, I’ll tell you how to get started on your own random map mod. Then I’ll to explain the core concepts of map generation, and give you a typical script structure to use as a guide. Finally I’m going to give you a bunch of tips and tricks to help your scripting. I’ll leave the details up to you.
If there’s enough interest in a more thorough guide, or about the details of the scripting language, I might do a follow up. Enough housekeeping, let’s dive in!
Getting Started
Each random map involves two files. map_name.xs
and map_name.xml
. The .xml
file just contains metadata like the map description and thumbnail. The .xs
file is the interesting part, containing all the code that generates the map.
Directory Structure
As I mentioned earlier, you can find all the game’s built in random map scripts in <path to steam games...>/Age of Mythology Retold/game/random_maps
. You can just add your own map script in here, but I don’t recommend it. You’re better off adding it to the mods folder, so you can publish it later.
You can find your mods folder by opening the game and going to Mods > Mod Manager > Open Directory. For me it’s at C:/Users/<user>/Games/Age of Mythology Retold/<steam id>/mods
. The mods directory should contain a local
directory, and a subscribed
directory (if local
doesn’t exist, create it). Obviously local
is for your own locally developed mods, and subscribed
is for mods installed from the store. Open the local
directory and create a new folder, naming it whatever you want your mod to be called.
The way mods work is that your new mod directory will be essentially merged with the <path to steam games...>/Age of Mythology Retold
directory I mentioned above (the directories aren’t actually merged, but the game pretends they are). That means that your map script should be placed in <path to your mod>/game/random_maps
. It also means that you can still import any of the util files in the random_maps/lib
folder, as if they were in your own mod’s random_maps/lib
folder, which is handy. I haven’t tried importing things from other mods, but I don’t think the mod store supports dependencies anyway.
Hello World
The easiest way to get started is to copy one of the scripts from <path to steam games...>/Age of Mythology Retold/game/random_maps
to <path to your mod>/game/random_maps
. Pick a map that’s similar to what you’re trying to build, copy the .xml
and .xs
files across, and rename the files to your map’s name. The most plain vanilla map, IMO, is Alfheim, so copy that one if you’re not sure.
Open up the .xml
file. You should see something like this:
<?xml version = "1.0" encoding = "UTF-8"?>
<mapinfo
displayNameID = "STR_MAP_ALFHEIM_NAME"
details = "STR_MAP_ALFHEIM_DESC"
loadDetails="STR_MAP_ALFHEIM_DESC_LOAD"
imagepath = "resources\maps\map_picker\alfheim.png"
cannotReplace = ""
loadBackground="resources\maps\previews\alfheim.png"
landmap="">
</mapinfo>
These fields are all pretty self explanatory, except for the fact that the displayNameID
, details
, and loadDetails
, are referred to by ID instead of being plain text. The real strings are in some resource file elsewhere (to make translation easier). Change the keys to displayName
, detailsText
, and loadDetailsText
, and put in your own map’s details (the loading screen details can be more verbose than the other details).
imagepath
and loadBackground
are preview images for your map. imagepath
is shown in the list of maps in the map picker, and by convention this is a view of the minimap. loadBackground
is the image shown on the loading screen, and is usually an in-game screenshot of the terrain. Both paths are relative to <path to your mod>/game/ui_myth
, so the imagepath
above points to <path to your mod>/game/ui_myth/resources/maps/map_picker/awesomeland.png
.
<?xml version = "1.0" encoding = "UTF-8"?>
<mapinfo
displayName = "Awesomeland"
detailsText = "Like Alfheim, but more AWESOME!"
loadDetailsText = "Like Alfheim, but each unclaimed settlement is guarded by a Titan!"
imagepath = "resources\maps\map_picker\awesomeland.png"
loadBackground = "resources\maps\previews\awesomeland.png"
landmap="">
</mapinfo>
cannotReplace
is deprecated, so just ignore it. As forlandmap
/watermap
, I assumed they told the AI whether it should build ships etc, or determined whether the map was included in the Land Maps or Naval Maps randomizer. Turns out, neither of these claims are true. The AI will still build ships on a landmap
, and those random map packs are based on a hard coded list of maps. All these tags actually do is determine whether the map is included in the Land Maps or Naval Maps filters in the map picker.
landmap
/watermap
tags actually affectRunning your script
Don’t make any edits to the XS script yet! We’re still getting our development environment set up. For now we’re just going to run the unchanged script to make sure it’s working.
The easiest way to run your script during development is to set AoMR to run in a window (you can find that option in the graphics settings), and have your script open in your favorite text editor next to it (Notepad++ is a good one to start with if you haven’t used a programmer’s text editor before).
In AoMR, open the map editor (≡ button in the top right > Editor). Then hit the new button in the top left, and you should be able to find your new map in the “Type” dropdown. Try generating your map script, and make sure it works. During development, you’ll be flipping back and forth between your code editor, and AoMR, making edits and generating new maps.
Running under the debugger
The editor has a script debugger that is essential when writing random map scripts, but it’s not enabled by default. To enable it, close the game, and open up C:/Users/<user>/Games/Age of Mythology Retold/<steam id>/config
(this is near to your mod folder). Create a new file in this folder called user.cfg
, and fill it with this:
debugRandomMaps
debugTriggers
GenerateTRConstants
GenerateRMConstants
Now open AoMR again, and try running your map script in the editor again. The debugger UI only appears if there is a problem with the script, so you probably won’t see it until you start modifying the map script. It’s a bit overwhelming at first, but it’s the best way to diagnose problems with your scripts. I’ll talk about it more in the tips and tricks section.
Now that you’ve set up your development environment, you’re almost ready to start writing your map script. But before you dive in, you’ll save yourself a lot of headaches if you at least read about areas, object defs, and constraints in the core concepts section.
Core Concepts
The most important concepts in map generation are areas, object defs, and constraints. You can’t make a map without understanding these!
You’ll also see classes and mixes used in pretty much every map. Paths and area defs are less common.
Areas
All the terrain of a map is made of areas. A lake is an area filled with water, forests are areas filled with trees, and cliffs are areas with higher elevation and painted with impassable rock. Areas don’t need to be separated, they can overlap if you want. They can also be nested entirely inside other areas (aka parenting). Areas can even be invisible, only being used to enforce constraints (eg, a clearing in the center of a forest map).
The usual way of creating an area is to call rmAreaCreate("area_name")
, which gives you an int
ID that represents the area. Then you can set various properties on the area, such as size, shape, location, height, terrain, constraints etc. Finally you can build the area using rmAreaBuild(id)
, or create a bunch of areas and build them all at once using rmAreaBuildAll()
.
I’ll leave you to figure out the details of what the area properties do, but some non-obvious properties I want to mention here are coherence, blobs, and blob distance. These control the area shape. Imagine flicking blobs of paint at a canvas, and you’ve got the general idea. Have a play with these parameters and see what they do.
ObjectDefs
An object def (short for definition) is used to place objects in your world. This includes buildings, animals, trees, gold mines, and decorations (basically everything that isn’t terrain).
Similarly to areas, object defs are created using rmObjectDefCreate(“object_name”)
, which returns an int
ID. You can add objects to the def using rmObjectDefAddItem
, which adds one or more copies of one kind of object to the def (eg, an object def that contains 4 wolves). You can add multiple kinds of object to the def, and they’ll all be placed together (eg, relics are often placed using an object def that also contains ruins for decoration). You can also add constraints to the def.
Once your def is set up, there are various functions for placing it on the map, such as rmObjectDefPlaceInArea
, placeObjectDefPerPlayer
, placeObjectDefInCircle
, or addObjectLocsAtOrigin
to name a few. Some of these will place the objects immediately, others will just define object locations which must be generated using generateLocations
. So if you’re wondering why your objects aren’t showing up, try calling generateLocations
.
Note: Although trees are objects, and basically every map uses object defs to place random trees around the map, forests are treated like areas, and are created using the ordinary area functions (or the handy
forestGen...
utils found inrandom_maps/lib/rm_forests.xs
).
Constraints
Constraints control how objects and areas are placed relative to each other. You add constraints to areas or object defs when you’re setting their other properties, and they affect how the area/object is placed in the world. Constraints do not affect things that are already placed. Once something is placed, it stays put.
It’s possible to over-constrain your area/object so that there’s nowhere it can legally be placed, in which case it simply won’t be placed. Most placement functions will tell you if they fail (eg rmAreaBuild
returns a bool
indicating whether the area was built). In fact these functions can fail even if there are valid places to put the area/object. They seem to just try a few times, then give up (this makes sense, since constraint solving is a hard problem, and we want these scripts to run in a reasonable amount of time). So it’s a good idea to place your more important areas/objects first, so they’re less likely to fail to be placed. Some scripts will continually place areas/objects in the world until placement fails a few times in a row (eg, to completely fill a map with random islands).
There are many types of constraint, but the most common type is distance constraints. Distance constraints tell the map generator “please keep X at least D meters away from Y”. For example, to keep cliffs from being placed too close to each other, to keep gold mines away from settlements, or to keep land dwelling objects out of the water. (Technically the constraint only contains the “keep at least D meters away from Y” part, and the X is simply whichever object/area you apply the constraint to).
Constraints are created using functions like rmCreateTypeDistanceConstraint
, rmCreateWaterDistanceConstraint
, or rmCreateAreaDistanceConstraint
. Each of these functions defines a constraint in one go (no need to use the object/area pattern where we create, set properties, then build). As usual they return an int
ID that identifies the constraint.
There are heaps of constraints defined for you already, so you might not need to define any yourself. Check out the initDefaultConstraints()
function in random_maps/lib/rm_util.xs
.
Classes
Classes group together objects and areas, so that constraints can be defined more easily. For example, you can add all your lake areas to a lake class, and then define a constraint that tells things to stay a certain distance away from the lakes.
The rmClassDefine(“class_name”)
function creates the class and returns an int
ID. When creating areas and objects, use the rmAreaAddToClass(areaId, classId)
and rmObjectDefAddToClass(objectDefId, classId)
functions to add them to the class. Then you can define constraints using that class, using functions like rmCreateClassDistanceConstraint
.
Mixes
Mixes are one of my favorite additions to the map scripting system in Retold. In older editions of AoM, scripts would add a bit of interest to their terrain by creating patches of terrain that were painted slightly differently to the base terrain. This was a bit of a hassle, and the results weren’t great.
Now instead we have mixes, which do this for us more simply and with way better results. A mix defines layers of terrain that will be painted onto an area using a noise function (eg perlin or simplex noise, but don’t worry about the details).
Mixes are created using rmCustomMixCreate(“mix_name”)
, which returns (you know the drill by now) an int
ID. Then you define the parameters using rmCustomMixSetPaintParams
, and add terrain layers to it using rmCustomMixAddPaintEntry
. You can use the mix to initialize the whole map, or just paint an area.
It’s important to note that the order you add the terrain layers matters. Think of the noise function as generating a random height map, with valleys and mountains. The terrain layers divide that map into height bands, and paint all the land within that band, like a contour map. The first layer you add only paints the lowest valleys of that height map, and the last layer only paints the peaks of the mountains. So the order matters because you’ll usually want to make the layers blend nicely by putting similar terrains next to each other (unlike the example below).
Paths
Paths are drawn between locations on the map. They don’t really do anything by themselves, but you can define areas using paths that can paint terrain etc on the path. For example, many maps connect up settlements with a path, then paint that path by defining an area over it filled with road terrain, to create roads linking the settlements. For a more advanced example, the Jotunheim map is created by filling the map with cliffs, creating snowy areas for the players, then joining those areas with a path, and filling in that path with another snowy area to create the mountain pass.
A path is created using rmPathDefCreate
, which returns an int
ID, then you can set its parameters, and build it using rmPathBuild
. Then you can build an area, and instead of setting its shape using blobs, you can tell it to base its shape on the path using rmAreaSetPathSegment
.
AreaDefs
An area def is just a convenience tool (though I never really use them). If you have to create a lot of similar areas, then instead of repeatedly creating an area and setting its properties, you can create an area def, set properties on the def, then create many areas from that one area def.
But programming languages already have lots of tools to help you avoid repetitive work, so I don’t really see the need for area defs. If I need to create a lot of similar areas, I usually just use a for loop, or write a helper function to factor out common elements.
You create an area def using rmAreaDefCreate
, which gives you an int
ID, and then you can set the same properties that you’d set on an area. Then you can create an area from an area def using rmAreaDefCreateArea
. After you create an area from a def, you still can still set more properties on it before you build it. To see an example of area defs, check out the Oasis map script, which defines the central oasis areas using these defs.
Typical script structure
Map generation starts by running your generate()
function. Inside that function, most scripts follow something like this structure, building the map elements from highest priority to lowest:
- Set the map size (it’s important that this happens first, because a lot of utils depend on the map size)
- Initialize the map terrain as water or land.
- Place the player locations. This doesn’t do anything visible on the map, but these locations can be used later to place object defs containing their town center etc, or create player islands on a water map.
- Call
postPlayerPlacement()
, which does some internal initialization stuff. - Set the nature civ, which affects what certain objects look like (eg the ruins that often appear next to relics).
- Set up the lighting.
- If you initialized the map to water or impassable land, create the passable land areas here.
- Call
placeKotHObjects()
. In King of the Hill mode this places a plenty vault etc in the center of the map (this function does nothing in other modes). Make sure to use constraints throughout the rest of the script to avoid placing anything too close to the KotH objects. - Place player starting objects.
- Place unclaimed settlements.
- Place terrain such as cliffs and lakes.
- Place starter resources near the players, like small gold patches, berry bushes, herdables and huntables.
- Create forests.
- Create more distant resources like large gold and more animals.
- Place relics.
- Place decoration, like random trees (aka stragglers), rocks, grass, and birds.
Tips and Tricks
As much as I love random map scripting, the whole process is, to put it charitably, very quirky. Here’s what I’ve learned:
- There are two types of errors a map script can have: compile errors, and runtime errors. Either will cause the debugger to show up if you have it enabled.
- Compile errors will cause the map script to fail to load. These are things like mistyping a function name, or other syntax errors. You can get more information about the problem by running the script in the debugger. The panel on the left is your script’s source code. The panel on the right is the debug output. Ignore everything except the last few lines of the debug output. It shows the filename, the line number (in brackets), and a description of the error. In this example I have an error on line 38, where I mistyped
rmSetLighting
.
- Runtime errors are more minor issues where the script loads ok, but something goes wrong during generation. The game mostly ignores these errors and generates the map anyway. But if you’re running under the debugger, it will pause when it hits one of these problems and tell you what and where the error is. Again just look at the bottom of the debug output. It has a description of the error, but doesn’t tell you the line number. Instead the line is highlighted in the source code panel. In this case I have an error on line 63, where I tried to use a terrain type as a constraint. If I run the script without the debugger (or click Run (Skip Breakpoints) in the debugger), that constraint is simply ignored, and the map still generates ok.
- Be careful when running under the debugger. If you close the debugger while the script is paused at a breakpoint (eg due to a runtime error), you’ll be dropped onto the loading screen and be soft locked.
- Some of the functions used in random scripting are defined in the
random_maps/lib
directory, so if you want to know how to used them you can just read them. Others (the ones that start withrm
orxs
) are built into the game, so there’s no code to look at. Fortunately, AoMR comes with API documentation for all the functions you’ll need for scripting. The documentation is in<path to steam games...>/Age of Mythology Retold/doxygen_retail.7z
. Unzip it using 7zip (somewhere other than that game directory, to be safe). This documentation also includes the functions for AI scripting and triggers, which we don’t care about right now. The most useful things to look at for map scripting arerandommapfuncs_8cpp.html
andxsfuncs_8cpp.html
. Hat-tip to the devs for including short descriptions of each function, not just the function signatures. - There’s one other piece of documentation you’ll need: all the constants for object types, terrain types, water, lighting, etc. That’s what the
GenerateTRConstants
andGenerateRMConstants
settings are for in youruser.cfg
file. When you run a map script, these settings tell the game to generate log files containing all the constants available to that script. You’ll find these logs inC:/Users/<user>/Games/Age of Mythology Retold/temp/Logs
, and the most important one isMythRMConstants.txt
. Most of the constants are assets likecTerrainGreekGrass1
, and you can match these up with the names of assets in the editor to find what you’re looking for. Just note that a few of the constants are things like the current map size, which vary between script runs, and you’re just seeing whichever value they had in the last run.
//=======================================================================
// Map Size constants.
//=======================================================================
const int cMapSizeStandard = 0;
const int cMapSizeLarge = 1;
const int cMapSizeGiant = 2;
const int cMapSizeCurrent = 2; // Only constant within current run.
- For objects there’s another trick you can use to find the right constant. You can place an object in the editor, select it, then open the Objects > Object Info menu. If you look at the proto name field, the constant in your map script should be
cUnitType<proto name>
. For example, an Ajax (Hero) unit has a proto name of Ajax, so if you want give every player an Ajax you’d add acUnitTypeAjax
to their starting unit object def. - XS is a C-like language, so C++ syntax highlighting does a great job (I’ve used it throughout this post). In fact, XS is so similar to C/C++ that I use clang-format to tidy up my code for me. Most editors have C++ syntax highlighting and plugins for clang-format. If you’re a VSCode user, there’s an interesting looking file here:
<path to steam games...>/Age of Mythology Retold/vscodeextensionretail.7z
. I haven’t tried it (I’m a Sublime guy), but if you have, let me know what it does and if it’s any good. - That said, there are a lot of small differences between C++ and XS. I’m not going to list them all here, but the one that bites me most often is the way you define functions. It’s mostly the same, except that while C++ function arguments can have default values, in XS every argument must have a default value. That is, every argument to every function is optional, and can be omitted. If you forget to define a default value for an argument, you’ll get the dreaded “failed to load” error.
// Valid in C, but not valid in XS.
int myCoolMathFunction(int x, int y, int z) {
return (x + y) * z;
}
// You MUST define default values for EVERY arg.
int myCoolMathFunction(int x = 0, int y = 0, int z = 0) {
return (x + y) * z;
}
- Just like your area and object def IDs, the IDs in the constants like
cUnitTypeSettlement
andcTerrainGreekGrass1
areint
s. But for some reason the lighting IDs, likecLightingSetRmAlfheim01
, arestring
s. ¯\_(ツ)_/¯ - Currently multiplayer games only send the root map script to the other players. So if the other players don’t have your mod installed, they won’t see the
imagepath
orloadBackground
. More importantly, if you did the good software engineering practice of factoring out your common code into a shared library, that library won’t be transferred, and your map won’t work. So it’s easiest to write your map script as a single monolithic file. - If, like me, you really need to put shared code in a library, you’ll need to write a merging script (eg in Python), which prepends your library onto your map scripts. But don’t put that script in the mod folder, because the mod store has filename restrictions, and you won’t be able to publish your mod. Advanced users may also want to inline any internal libraries the script uses, because these change and break (due to game updates) more often than
rm
/xs
functions. Your merging script will need to rename some duplicated global variables though. - If you don’t want your map to support KotH mode, just don’t call
placeKotHObjects()
. Users will still be able to select this mode when setting up the game, but it won’t do anything. - There’s an internal function that sets up sudden death mode called
applySuddenDeath
. This function is called automatically, so you generally don’t need to know about it. But if you want to disable sudden death mode, you can override this function with an empty implementation (I think themutable
keyword is related to the overriding behavior).
mutable void applySuddenDeath() {
// Do nothing.
}
- Once you’re happy with your map script, you can publish your mod. Just open it up in the mod manager, hit publish, and enter some details. Updating your mod after it’s published is a bit more fiddly.
- One last tip: Did you know you can add weather to a random map? If you place a
cUnitTypeVFXWeatherRain
somewhere on your map, you’ll get rain over the whole map, and if you place acUnitTypeVFXSnow
you’ll get snow. Place more than one to get a stronger effect. One quirk is that they only work while they’re in the player’s line of sight (hopefully the devs fix this), so maybe place them at player 1’s starting location? Also you can use acUnitTypeRevealer
to reveal part of the map to the the player that owns the relvealer, or acUnitTypeRevealerToAll
to reveal to everyone. Might help with that weather visibility quirk? 😉
If you have any questions, the best place to look for answers is the modder’s discord: https://discord.gg/th6UB4ZRhs. We’ve also started a wiki, with more info about AoMR modding: https://aomr-mod-dev.github.io/.
If you’re interested in a deeper dive into the scripting language, let me know and I might write a follow up post. For example, did you know the XS language has C++ style classes? I’m not talking about rmClassDefine()
, I mean class Foo {
with fields and methods. You can use them to really clean up your map scripts and make them easier to understand. But they’re also very… quirky.
Also, check out my random map mod, called Metamar. It’s sort of like Nomad, but with dozens of biomes and a bunch of different terrain shapes (from mountains to river deltas to canyons to forests), as well as a bunch of other improvements. All the pretty in-game pictures in this post are from that script.