Sitemap

AoMR Random Map Scripting

19 min readSep 27, 2024

--

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.

What the landmap/watermap tags actually affect

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

Areas can be separated, nested, or overlapped arbitrarily.

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.

Area shapes are a bit like blobs of paint splattered on a canvas.

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

Objects are essentially everything in your world 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 in random_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.

Constraints affect how objects and areas are placed relative to each other.

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

A mix showing how the terrain layers are applied in bands, like a contour map.

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:

  1. Set the map size (it’s important that this happens first, because a lot of utils depend on the map size)
  2. Initialize the map terrain as water or land.
  3. 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.
  4. Call postPlayerPlacement(), which does some internal initialization stuff.
  5. Set the nature civ, which affects what certain objects look like (eg the ruins that often appear next to relics).
  6. Set up the lighting.
  7. If you initialized the map to water or impassable land, create the passable land areas here.
  8. 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.
  9. Place player starting objects.
  10. Place unclaimed settlements.
  11. Place terrain such as cliffs and lakes.
  12. Place starter resources near the players, like small gold patches, berry bushes, herdables and huntables.
  13. Create forests.
  14. Create more distant resources like large gold and more animals.
  15. Place relics.
  16. 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.
A compile error on line 38
  • 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.
A runtime error on line 63
  • 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 with rm or xs) 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 are randommapfuncs_8cpp.html and xsfuncs_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 and GenerateRMConstants settings are for in your user.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 in C:/Users/<user>/Games/Age of Mythology Retold/temp/Logs, and the most important one is MythRMConstants.txt. Most of the constants are assets like cTerrainGreekGrass1, 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 a cUnitTypeAjax 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 and cTerrainGreekGrass1 are ints. But for some reason the lighting IDs, like cLightingSetRmAlfheim01, are strings. ¯\_(ツ)_/¯
  • 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 or loadBackground. 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 the mutable 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 a cUnitTypeVFXSnow 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 a cUnitTypeRevealer to reveal part of the map to the the player that owns the relvealer, or a cUnitTypeRevealerToAll 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.

--

--

Liam Appelbe
Liam Appelbe

Written by Liam Appelbe

Code monkey, board game hoarder, aspirant skeptic

Responses (2)