This is an .XS/.XTS - limited TypeScript language - code transpiler. It allows you to write random maps using the advantages of TypeScript: syntax highlighting, autocomplete, code formatting and IntelliSense, as well as RM functions documentation and terrain enums. It features a converter for the scripts both ways, defining XML map info as a const
straight from the script, type checking for RM functions/constants and JSDoc documentation for them.
-
Install Node.js. Chances are, if you want to use TypeScript you already have it.
-
Unpack the .ZIP and, if you like, move the contents of the included
workdir
to your desired location.
- For example, if you're working on a mod you can move them straight to your game's random maps folder (i. e.
Age of .../RM3
). Note that this can change the game's CRC, rendering it incompatible for multiplayer, so you should only do this temporarily, for production. - If you're making TAD custom maps, you could move them to the
Documents/My Games/Age of .../RM3
. - Or you could just leave it as is and only change the
outDir
in the config file. Just make sure that thesourceDir
has alib
folder as a "sibling" for the TypeScript imports to work properly.
- Edit the
config.json
accordingly. Here are the options:
-
sourceDir
- your .XTS map scripts folder. TypeScript code will be saved here after the conversion. Has to be a valid, existing path with the includedlib
folder as its "sibling". -
outDir
- your resulting .XS maps folder. Transpiled maps and their .XML info files will be saved here. Has to be a valid, existing path. -
libDir
- full or relative to thesourceDir
path to the includedlib
folder with.d.ts
type declarations. The default value is alright provided thislib
folder is a sibling of thesourceDir
. -
ignoreWarnings
- do not show non-critical errors and warnings. -
ignoreErrors
- suppress even the critical errors, such as .XS syntax errors -
forceOpen
- if a file can't be found, look for it in thesourceDir
andoutDir
folders. -
extensions
- a list of extensions for the transpiler. For now only contains additional type declarations:- Vanilla - terrain types for the vanilla Age of Empires III maps
- TWC - terrain types for The Warchief maps
- TAD - terrain types for The Asian Dynasties maps (includes TWC and Vanilla types, too)
- UHC - functions for mods that use UHC (or UHC2)
- TNE - terrain types for The Native Empires mod maps (includes TAD types and UHC functions, too)
Just leave the default value unless you want to make some very specific map. If you make a map for a mod, you can add your own extension - just explore the structure of
extensions\TNE\index.xts
,extensions\TNE\Terrain.d.ts
andworkdir\lib\extensions\TNE\Terrain.d.ts
(the last two are the same).
- To use the potential of XTS, use an IDE such as Visual Studio Code for syntax highlighting and autocomplete. If you are using VS Code, set .XTS file extensions to TypeScript language by default. Alternatively, select the language manually in the bottom right.
In the command line, write node [path to index.js] [your map name(s)]
.
Examples (click to expand)
- With the command prompt in the XScript dir, convert the Great Plains from game's RM3 folder:
C:\Users\Jeremy\Saved Games\Tools\XScript> node index.js 'C:\Users\Jeremy\Saved Games\Age of Empires III\RM3\great plains.xs'
- Having moved the command prompt to the custom maps folder, convert two maps - My New Map and UnknownEnh:
C:\Users\Jeremy\Saved Games\Tools\XScript> cd 'C:\Users\Jeremy\Documents\My Games\Age of Empires III\RM3'
C:\Users\Jeremy\Documents\My Games\Age of Empires III\RM3> node 'C:\Users\Jeremy\Saved Games\Tools\XScript\index.js' 'My New Map.xs' 'unknownenh.xs'
- With the command prompt in the XScript dir, convert an included
mercenaries.xs
script:
C:\Users\Jeremy\Saved Games\Tools\XScript> node index.js 'C:\Users\Jeremy\Saved Games\Age of Empires III\RM3\mercenaries.xs'
The transpiler will generate .XTS files in TypeScript and save them to the specified sourceDir
. It will also run through the dependencies, such as mercenaries.xs
, trying to convert them as well if necessary (they are saved to the lib\includes
folder), and look for the map's .XML data (with the name, description etc) to write this data in the beginning of the resulting .XTS.
If you're unfamiliar with TypeScript, read the basics here.
Here's the brief structure of a valid .XTS script:
.XTS example (click to expand)
/**
* -----------------------------------------------------
* THIS SCRIPT WAS GENERATED FROM AN .XS SCRIPT
* Original script: `../great plains.xs`
* -----------------------------------------------------
*/
import {int, float, double, vector, [...]} from "../lib/Types";
import {River, Ocean, Lake, Cliff, [...]} from "../lib/Terrain";
[...]
/**
* XML map info. Don't use in the code, it will only be used
* to generate the corresponding XML file.
*/
const mapinfo = {
details: 28599,
imagepath: "ui/random_map/great_plains",
[...]
};
function min(a: float=-1.0, b: float=-1.0): float { if(a<=b) { return(a); } return(b); }
function max(a: float=-1.0, b: float=-1.0): float { if(a>=b) { return(a); } return(b); }
// GREAT PLAINS
import {chooseMercs} from "../lib/includes/mercenaries";
[...]
/**
* Main entry point for random map script
*/
function main(): void {
rmSetStatusText("", 0.01);
// Chooses which natives appear on the map
var subCiv0: int = -1;
[...]
// Choose which variation to use. 1=southeast trade route, 2=northwest trade route
var whichMap: int = rmRandInt(1, 2);
var map_type: string = "greatPlains";
if (rmAllocateSubCivs(6) == true) {
subCiv0 = rmGetCivID("Comanche");
rmEchoInfo("subCiv0 is Comanche " + subCiv0);
if (subCiv0 >= 0)
rmSetSubCiv(0, "Comanche");
[...]
}
// Picks the map size
var playerTiles: int = 11000;
if (cNumberNonGaiaPlayers > 4)
playerTiles = 10000;
if (cNumberNonGaiaPlayers > 6)
playerTiles = 8500;
var size: int = 2.0 * sqrt(cNumberNonGaiaPlayers * playerTiles);
rmEchoInfo("Map size=" + size + "m x " + size + "m");
rmSetMapSize(size, size);
rmSetMapType(map_type as MapType);
rmSetMapType("land");
[...]
}
You can see imports at the top - they contain RM functions and terrain enums for the JSDoc and syntax highlighting. You don't have to change these.
After that, there's the const mapinfo
which describes the map's name, icon etc. You can edit these directly here - this info will be included into the resulting .XML file. You may remove it if you want to edit the .XML manually.
From this point on, your actual random map script starts.
Then there are two functions declared. Often you don't need user-defined functions, but nothing stops you from using them, just follow these rules:
- Explicitly define the parameters' type and default value.
- Explicitly define the function's return type (including
void
). - Rather than arrow functions or
const
notation, use the standard
function name(param: int = 0): int { ... }
Most of the following script doesn't need much explanation. Keep these .XTS limitations in mind:
- Explicitly define any variable's type and initial value - as you would with the .XS script.
- Instead of the TypeScrpit's
number
, useint
,float
,double
orlong
types. - Avoid using
'
and"
simultaneously, as ins = "don't do this";
var
variable declaration is preferred. You may uselet
orconst
, but they will all be transpiled to the .XS'svar
-like (function scope) variable declarations, without runtimeconst
behavior.- Keep in mind that all you write should have an XS equivalent. Don't use advanced TypeScript stuff such as:
- arrays, objects, classes
- string methods (such as
.replace
), string templates for ... of
,for ... in
yield
,async
, etcMath
,Object
, etc===
,!==
,![...]
undefined
,null
,NaN
console.log()
, etcdeclare
,export
,require
, etc- custom types/interfaces
- composite types, like
[type1]|[type2]
- Always write
for
cycles with the following notation:
for (var i = 0; i < num; i++) { ... }
so as to allow conversion into the .XS equivalent:
for (i = 0; < num)
XTS is not just TypeScript – XS is a very simple language, so a very limited subset of TypeScript functionality is available in XTS.
Sometimes using as
is necessary - you can notice in the example above that as
is supported, just don't combine types like [type1]|[type2]
:
rmSetMapType(map_type as MapType);
Again, just run the following command: node [path to index.js] [your map name(s)]
.
Example:
- With the command prompt in the XScript dir, transpile the Great Plains from the specified
sourceDir
and My New Map from the desktop:
C:\Users\Jeremy\Saved Games\Tools\XScript> node index.js 'great plains.xts' 'C:\Users\Jeremy\Desktop\My New Map.xts'
If your code is alright, this command will strip it of the unnecessary bits and transpile to an XS script. If the const mapinfo
is present, it will read it to generate an XML map data file, too.
Both will be saved to the specified outDir
, possibly replacing an older file.
Most likely an RM function has not been declared in the lib
files. It is difficult to find all the RM functions and documentation on them, so it is very likely that I overlooked some. If this is the case, please, contact me so that I can fix this.
Meanwhile, you can add the declaration for this function to the corresponding .d.ts
file in the lib
folder and manually add the import in the beginning of your .XTS script.
Some RM functions are hardcoded to accept a limited set of possible argument values. For example,
rmSetAreaMix(areaID: int, mixName: Mix): void
as you see, only accepts a Mix
-type second argument (such as bayou_grass
) rather than any strings. Thus, any variable you want to pass as an argument either has to be of the corresponding type or has to be forced to this type like this:
Types example (click to expand)
rmSetAreaMix(areaID, "bayou_grass"); // Works
var proper_mix: Mix = "bayou_grass";
rmSetAreaMix(areaID, proper_mix); // Works
var another_mix: string = "bayou_forest";
rmSetAreaMix(areaID, another_mix as Mix); // Works, but not recommended
var non_existent_mix: string = "no such mix actually exists";
rmSetAreaMix(areaID, non_existent_mix); // Doesn't work
rmSetAreaMix(areaID, non_existent_mix as Mix); // Works, but only do this if you're absolutely sure
Here are all the types for which this is the case:
River, Ocean, Lake, Water (=River|Ocean|Lake), Cliff, MapType, Mix, Forest
The allowed values for these types depend on the extensions you've enabled.
Then you can either use as
or, to get proper autocomplete, make an extension for your mod - just explore the structure of extensions\TNE\index.xts
, extensions\TNE\Terrain.d.ts
and workdir\lib\extensions\TNE\Terrain.d.ts
(the last two are the same) and make a copy with your mod's terrain.
There are no guarantees, but provided DE's RMS structure stays the same, it should work. Some terrain types and function declarations must be missing though, unless somebody makes an extension for DE.
Make sure the paths specified in your config.json
exist and comply with the location of your workdir
.
Yes, they are kept intact (unless there are some bugs).
Yes, for example, you can write something like
C:\Users\Jeremy\Documents\My Games\Age of Empires III\RM3> node 'C:\Users\Jeremy\Saved Games\Tools\XScript\index.js' 'unknownenh.xs' 'unknownenh.xts'
to first convert to XTS and then immediately back to XS. The reason you may want this is to tidy up an existing map with the included Prettier code formatter.
Please open an issue here or contact me on Discord, via e-mail or on moddb.