The m4nfo User manual and Technical Report

Functions for stations

Using functions for railway stations



In m4nfo, stations are using four types of functions:

Functions for tile layout definition


For station tiles, the tile layout defines what sprites should be displayed, where they are to be displayed, and in what order.

Because in TTD, the tile layout for stations is somewhat different from that for buildings and objects, this has to be reflected by m4nfo as well. The main reason for this difference is that for stations, the tile layout is kept apart from the real sprites defined in a spriteblock() and accessed by spriteset() functions, and in this way, a layout can easily handle different sprite sets with the same layout structure.

     [label,] <groundsprite> {<buildingsprite>(<xoffset, yoffset, zoffset> <xextent, yextent, zextent>) |
     <buildingsprite>(<xpixeloffset, ypixeloffset>)}


For station tiles, there are two types of sprites. The first type establishes a new 3D bounding box for use by the sprite sorter. The second type shares the 3D bounding box of the previous sprite. It must not be larger than the sprite which established the bounding box, nor must any part of it be outside this box. For simplicity, it might have the exact same dimensions as the sprite it shares the bounding box with.

The 3D bounding box is used by TTD's sprite sorter to figure out the order in which to draw the sprites, as well as telling it which sprites to draw, because those whose bounding box falls outside the currently updated screen rectangle will not be drawn. Make sure the 3D bounding box is large enough to contain the entire sprite, but not so large that the sprite sorter can't determine which sprites should be in front.

This means that the order in which the sprites are specified is irrelevant. Sprites will always get drawn from back to front, in the order which the sprite sorter determines as correct, from their bounding boxes. There are two exceptions, however:

Like for houses, industry tiles and objects, m4nfo provides the same auxiliary functions to be used in tile layout definitions, but with a slightly different syntax than for the above mentioned features:

Layout term m4nfo function
<tile>tile() | xtile()
<groundsprite>ground() | NOSPRITE
<buildingsprite>regular() | notransparency() | glass() | recolour() | NOSPRITE
<xoffset, yoffset, zoffset>xyz()
<xoffset, yoffset>xy()
<xextent, yextent, zextent>dxdydz()
<xpixeloffset, ypixeloffset>xyoff()


This function sets up the tile layout for a certain station-ID. It needs at least two calls of function tile() (or one of xtile()): one for the tile in x- and the second one for the y-direction. The calls to tile()/xtile() are consecutively numbered internally, i.e. the first station tile gets numbers 0/1 (x/y-direction), the second one gets 2/3 and so forth. These are the tile numbers to be referenced by callback CB_LAYOUT from the makestation() function when drawing all station tiles in the proper way. (It is of course possible to use 'labels' instead of those numbers given implicitily, see next paragraph.)

Layout structures could be used multiple times, by referencing different sets of graphics sprites.

tile([<label>,] <block>)

This function takes a block of sprite definitions, either for ground sprites or building sprites, see below. A tile may be labeled, to reference it later, e.g. in a CB_LAYOUT chain:

		regular(21, xyz(0,0,0), dxdydz(5,16,12))
		regular(24, xyz(0,0,0), dxdydz(16,16,35))

def(3) plt_num(
		reftile(__roofs) if(4)
		reftile(__roofs+1) if(3)
		reftile(__roofs+2) if(2)
		reftile(__roofs+3) if(1)
		reftile(0) else

Function xtile() might be used to generate sprite definitions for the tile in y-direction automatically, derived from the tile in x-direction. Using this method needs a number of additional requirements and is more limited than using tile(): building sprites have to be ordered in such a way that their sprite numbers are +1 for the tile in y-direction, and for ground sprites the difference must be -1 for original TTD sprites and +1 for custom ground sprites. 3D coordinates will be swapped accordingly. Graphics sprites associated with the x- and y-direction of a xtile() layout have to be symmetric to assure correct bounding boxes, and you cannot use function xyoff() because graphics sprite coordinates are totally unknown inside tile layouts.

ground(<sprite> [,CUSTOM])

This function defines the ground sprite to use for the tile in question. There may only be one ground sprite defined by ground() in a tile block. In case of multiple (stacked) ground tiles these must be supplied by using function regular() prior to definition of any bounding box.

You may either define original TTD ground sprites, or custom ground sprites, defined in a spriteblock(). In this case, you'll need the second parameter set to CUSTOM.

If no ground sprite is needed you should use macro NOSPRITE instead calling function ground(). This might be useful for displaying icon graphics or when drawing custom foundations.

regular(<tile-id>, (<xyz()>, <dxdydz()>) | <xyoff()> [,TTD])

This function defines a 'normal' building sprite (or an additional ground sprite). First parameter is the index into the spriteblock associated with the current layout, remaining parameters are either offsets from the northern tile corner and size of sprite, or offsets relative to the previous sprite (see pictures below). In the latter case, this sprite will share its bounding box with the current bounding box. Setting the optional parameter "TTD" allows to access original TTD sprites.

notransparency(<tile-id>, (<xyz()>, <dxdydz()>) | <xyoff()> [,TTD])

This function works the same as regular(), just with its sprite being displayed normally even in "transparent buildings" mode.

compcol(<tile-id>, (<xyz()>, <dxdydz()>) | <xyoff()>)

Again the same behaviour as regular(), but this time using the default company colour translation.

recolour(<tile-id>, (<xyz()>, <dxdydz()>) | <xyoff()>, <recolour-sprite>)

Yet again the same behaviour as regular(), but this time with colour translation defined by a recolour table given by the last parameter.

glass(<tile-id>, (<xyz()>, <dxdydz()>) | <xyoff()> [,<recolour-sprite>])

Same behaviour as regular(), but sprite is drawn in 'transparent' mode, either using the default 'glass' mode, or using a custom transparent recolouring when supplied an additional parameter. This parameter must be the address of the recolour map to be used for the glass effect.

xyz(<byte>, <byte>, <byte>)

This function defines the x/y/z-offsets of the current sprite from the northern tile corner. Note that coordinates are 3D coordinates with x running from top-right to bottom-left and y running from top-left to bottom-right (see picture below), with tile dimensions being 16x16 px for x and y and 8 px for one height level. This means the x and y values should always be within 0 .. 15.

dxdydz(<byte>, <byte>, <byte>)

This function defines the size of the current sprite in x/y/z-direction. Coordinates given are equal to those above.

xyoff(<byte>, <byte>)

This function defines the x/y-offsets of a child sprite in relation to the previous sprite setting the bounding box. Coordinates refer to the upper left corner of the preceding sprite, i.e. it is not a 3D-coordinate.

Advanced sprite layout

In newer versions of OpenTTD, a so-called 'advanced format for sprite layout' (ASL) is supported, which allows 'dynamic' layout modifications by using registers.

m4nfo formal specification for this format is as above, except there are two additional parameters in m4nfo functions ground(), regular(), recolour(), and glass(), namely <flags> and <registers>. E.g., ground(<sprite> [,CUSTOM]) gets ground(<sprite> [,CUSTOM], <flags>, <registers>), or regular(<tile-id>, (<xyz()>, <dxdydz()>) | <xyoff()> [,TTD]) gets regular(<tile-id>, (<xyz()>, <dxdydz()>) | (<xyoff()> [,TTD]), <flags>, <register>.)

Function aslflags() takes a quoted list of following flags as its parameter:

Flags in advanced sprite layouts
Value Label parent sprite ground/child sprite register(s)
1SKIPSkip bounding box including child spritesSkip sprite1
2OFFSET_SPRITEAdd offset to sprite number, disable default usage of construction stage or railtype-offset1
4OFFSET_RCSPRITEAdd offset to recolour sprite number
8CUSTOM_RCSPRITERecolour sprite is from custom spriteblock, OFFSET_SPRITE may be used as welln/a
16OFFSET_XYAdd 3D offset in <x> and <y> 2
32OFFSET_ZAdd 3D offset in <z> 1
48PIXOFFSET_XY Add pixel offset in <x> and <y>2
64RESOLVE_SPRITEResolve sprite by using spritetype() with a specific value1
128RESOLVE_RCSPRITEResolve recolour sprite by using spritetype() with a specific value1

The needed registers are set by function registers(), also as a quoted list. The order of registers must be the same as the given flags, (see examples). For flags not being used, no register is specified. Since offsets for <x> and <y> can only be enabled together, two registers are needed. The register for SKIP uses a value of "1" to draw the sprite, and "0" to skip it. Flag CUSTOM_RCSPRITE uses no register at all.

Last two flags are only valid for stations. For stations, the chain of control is resolved multiple times and sprites may be part of different sprite blocks (ground sprites, custom foundation sprites, normal sprites) already without using the advanced sprite layout format. Unlike the first 6 flags, for both station-only flags the parameter in function registers() is not a register but a value in the range 0 .. 7, which is accessed by function spritetype() while resolving the sprites. Remember that some values might already being used, e.g. '2' for resolving custom foundation sprites. Even if a sprite is not from a spriteblock but an original sprite, the value still defines which chain of control defines the values for the referenced registers.

For more information, see Example 4 below.


In TTD, 'normal' station tiles (there might be exotic tiles consisting only of a ground sprite or an additional single platform sprite) are usually assembled from three sprites: the track base (ground sprite), a 'background' sprite and a 'foreground' sprite. These sprites are arranged to build up a station tile in such a way that trains run over the background and the ground sprites, while the foreground sprite covers everything. See picture below.

In case of an additional station roof or overpass, these sprites would be either added last or could be integrated into the foreground sprite.

In addition, station tiles always have to be defined twice (even for the building menu icons), i.e. in x- and y-direction.

sprite arrangement

Example 1 (simple 2-platform station tile, see pic above):
    ground(1012) // track x
    regular(1, xyz(0,0,0),dxdydz(16,5,8)) 
    regular(3, xyz(0,11,0),dxdydz(16,5,8))
    ground(1011) // track y
    regular(2, xyz(0,0,0),dxdydz(5,16,8))
    regular(4, xyz(11,0,0),dxdydz(5,16,8))

In m4nfo, the need for defining both x- and y-tiles might be avoided by exchanging function tile() with function xtile(), which will generate tiles for y-direction automatically, in case the tiles are mirror images of each other, graphics-wise. I.e., in the example above, only the first tile needs to be given, and by using xtile(), a second tile with ground sprite #1011 and normal sprites #2 and #4 will be added automatically, together with their coordinates being swapped accordingly.


Example 2 (simple 2-platform station tile with child sprites):
// modern glass 'tinted green'
    compcol(502, xyz(0,0,0),dxdydz(16,5,11))
    glass(538, xyoff(0,0), GREEN) 
    compcol(504, xyz(0,11,0),dxdydz(16,5,11)) 
    glass(540, xyoff(0,0), GREEN)
    compcol(503, xyz(0,0,0),dxdydz(5,16,11))
    glass(539, xyoff(0,0), GREEN) 
    compcol(505, xyz(11,0,0),dxdydz(5,16,11)) 
    glass(541, xyoff(0,0), GREEN)


Example 2 shows the tile layout of the 'modern glass' platform from NewStations. It consists of five sprites: the ground sprite, two platform sprites to be drawn in 'company colour', each with a transparent 'glass' sprite which are defined as 'child sprites', i.e. sprites sharing their bounding box with the preceding sprites using a bounding box (the platform sprites).

Details to be represented in one of the 8 available company colours have to be drawn using a special colour, take a look on TTD palettes. However, areas representing the transparent glass effect may be drawn in any colour (red in this example). Also, this example shows how to use a 'custom glass effect' (tinted green glass). This needs setting up a custom colourtable to get the desired effect to work.

multiple ground sprites

Example 3 shows how to use multiple ground sprites for a tile. This is useful if you want to use the usual rail/grass/concrete ground sprite, but still need to add features to it without using a new bounding box. To do so, the syntax of sprites sharing the previous bounding box is used, but before the first bounding box definition has been defined.

The example defines two types of waypoints, the first one made up from ground sprites only, the second one with an added building sprite.

Example 3 (using multiple ground sprites):
// w/o building
      ground(1012) // track
      regular(0, xyoff(0,0)) // overlay 
      ground(1011) // track
      regular(1, xyoff(0,0)) // overlay 

// w building
      ground(1012) // track
      regular(0, xyoff(0,0)) // overlay 
      regular(2, xyz(0,0,0),dxdydz(16,5,10)) // building
      ground(1011) // track
      regular(1, xyoff(0,0)) // overlay 
      regular(3, xyz(0,0,0),dxdydz(5,16,10)) // building

Note that <xpixeloffset> and <ypixeloffset> in OpenTTD refer to the usual spot of groundtiles, but are ignored in TTDPatch and set to zero. So, in case of developing a newGRF that needs to be compatible with both programs, you should always keep <xpixeloffset> and <ypixeloffset> zero to get the same effect in both.

Note however that due to a bad specification in plain nfo, both implementations do not consider the setting of GROUNDSPRITES in property function flags(), hence these 'multiple ground sprites' have to be always part of the building sprites set and cannot be part of a different sprite set for ground sprites. Therefore, you cannot check for multiple ground sprites by function spritetype().

custom foundations

Unlike custom foundations for houses, industry tiles and objects, foundations for stations do not need any special handling inside the tile layout definition.

Instead, the needed foundation sprites are defined in a special spriteblock and accessed inside the graphics chain by use of function spritetype(). Please note that this needs to set flag NOFOUNDATION in property function flags().

advanced sprite layout

Example 4 shows how to benefit from the 'advanced sprite layout', in case of repetitive station layouts. A good example would be a station layout with a need to repeat itself, only for different cargo sprites, which could be represented by recolouring a sample cargo sprite. The advanced sprite layout for stations gives the possibility to simply include recolour sprites in their own sprite block, and easily access them by the layout functions.

Example 4 (use of registers and recolour sprite handling):

    recolour(0, xyz(8,8,0), dxdydz(8,8,8), 0, aslflags({OFFSET_RCSPRITE,

// building sprites
    sprite(advtest.pcx 10 10 09 22 32 -14 -16)

def(0) spriteset(little(0))

// recolour sprites - orange and blue
      62 .. 67, C0 .. C5,
      62 .. 67, 92 .. 97,

def(1) spriteset(little(0))

def(2) spritetype(
	ref(1) if(4) // recolour sprites
	ref(0) else  // building sprites

def(3) setregisters(11,1,ref(2)) // 2nd recolour entry: blue

def(4) callback(
	cbr(0) if(CB_LAYOUT)
	ref(3) else // graphics


Note that registers used in the advanced sprite layout must be set inside the graphics' chain, not in the callback chain. This is done in def(3), where function setregisters() sets register '11' to a value of '1', i.e. the second recolour sprite entry. In def(2), the graphics' chain is split into a chain for the building sprites, def(0), and an additional chain for the recolour sprites, def(1).

In the layout, a sprite is defined to be recoloured by using function recolour(), with first parameter setting the building sprite, and the 4th parameter setting the recolour sprite, both '0' in this case. Parameter 5 specifies flags OFFSET_RCSPRITE, CUSTOM_RCSPRITE, and RESOLVE_RCSPRITE, and parameter 6 specifies the register to be used for defining the recolour sprite's offset, and the graphic's chain for resolving the recolour sprites (4).

copylayout(<Station-ID>, <Station-ID> | Range::<Station-ID>)

This function allows to copy a tile layout definition. First argument is the station-ID for which the tile layout got defined, remaining arguments are station-ID(s) to copy the tile layout to. These IDs must be a range of consecutive station-IDs.

copytilelayout(<Station-ID>, <Station-ID> | Range::<Station-ID>)

Same as above, but this function allows to copy a custom TTD tile type layout definition. First argument is the station-ID for which the custom TTD tile type layout got defined, remaining arguments are station-ID(s) to copy the custom tile layout to. As before, these IDs must be a range of consecutive station-IDs.

Functions for station performance

These functions are used to evaluate game-intrinsic variables, and make them accessible to the station's activation function. A typical application would use a multitude of these functions linked together to form a "chain", connecting the graphics' sprites of an station with its activation function. See here for an example.

Function Description
anim_frame([<Coordinate> | REGISTER,] <block>)Get animation frame
anim_trigger(<block>)Get current animation trigger
callback(<block>)Check type of callback
cargohist(<Cargo-ID>,<block>)Get cargo history
cargotrigger(<block>)Get cargo type for animation trigger
cargowaiting(<Cargo-ID>, <block>)Amount of cargo waiting
elseThis is really a void statement
exclusiverights(<block>)Number of months exclusive transport rights will be effective
iswaypoint(<block>)A waypoint station will not receive any cargo
pbsinfo(<block>)Path-based signalling info
plt_axis([<shift-mask>,] <block>)
plt_even([<shift-mask>,] <block>)
plt_index([<shift-mask>,] <block>)
plt_indexrev([<shift-mask>,] <block>)
plt_length([<shift-mask>,] <block>)
plt_midnum([<shift-mask>,] <block>)
plt_midpos([<shift-mask>,] <block>)
plt_num([<shift-mask>,] <block>)
plt_numrev([<shift-mask>,] <block>)
plt_pos([<shift-mask>,] <block>)
plt_posrev([<shift-mask>,] <block>)
plt_tiletype([<shift-mask>,] <block>)
plt_total([<shift-mask>,] <block>)
Information about current platform tile(s)
randombits(<block>)Random bits from CB_ACONTROL
servicedbytype(<block>)Type(s) of vehicle(s) servicing this station
spritetype(<block>)Check for sprite type
stationage(<block>)Station age in years since 1920
test_tiletype([<shift-mask>,] <block>)
Tile type check for CB_TILETYPE
Platform checks for CB_SLOPE
test_slope(<block>)Slope check for CB_SLOPE
timesinceload/timesinceunload(<block>)Times since the last cargo load operation happened at this station, in 185 ticks (~2.5 days) units
tinfo_flatwater(<Coordinate> | REGISTER, <block>)Check if tile is a flat/full water tile
tinfo_grfid(<Coordinate> | REGISTER, <block>)Check grf ID of station tile
tinfo_lclass(<Coordinate> | REGISTER, <block>)Tile info: landscape class of tile
tinfo_slope(<Coordinate> | REGISTER, [<shift-mask>,] <block>)Tile info: slope of tile
tinfo_statid(<Coordinate> | REGISTER, [<shift-mask>,] <block>)Tile info: station ID and other info
tinfo_terrain([<Coordinate> | REGISTER,] <block>)Tile info: terrain type
tinfo_trackconnect([<shift-mask>,] <block>)Tile info: check for connected tracks
tinfo_trackexist([<shift-mask>,] <block>)Tile info: check for existing tracks
tinfo_tracktype(<block>)Tile info: check track type
tinfo_water(<Coordinate> | REGISTER, <block>)Check if tile is a water/coast tile
tinfo_waterclass(<Coordinate> | REGISTER, <block>)Returns water "class"
yearbuilt(<block>)Year the station was built
random(<list::trigger>, <randombit> <list::ref()>)Get random reference
randomcb(<label>, <list::trigger>, <randombit>, <list::reference>)
rerandom(<label>, <block>)
re-randomisation in callback chains


anim_frame([<Coordinate> | REGISTER,] <block>)

This function returns the actual value of the animation counter of the current tile to decide the frame to be displayed. The return value is between zero and the number of animation frames specified in the station definition's property anim_info().

If an additional coordinate parameter is given, the actual value of the animation counter of the given tile is returned. Please note that coordinates must be given by auxiliary function pos(<x>, <y>,). In case of REGISTER as the first parameter, the coordinate is not explicitly given but taken as the result from a preceding calculation.


This function returns the current animation trigger when in a CB_ACONTROL callback.

Example (handle station animation):
def(8) anim_trigger(
	animcontrol(0, SND_HORN) if(BUILT) // start animation on construction
	ref(6) if(NEWCARGO)     // probably start of animation when cargo arrives 
	animcontrol(A_NOP) else // do nothing

	ref(8) if(CB_ACONTROL) // animation control
	ref(7) if(CB_AFRAME)   // animation frames
	ref(1) else	       // graphics


This function checks for a callback incidence, and if so, returns the type of the incurred callback. See example above.


This function returns a 4-bit value about history of the given cargo type having been accepted on the station in the past:

01Set if the given cargo was ever accepted at this station
12Set if the given cargo was accepted last month
24Set if the given cargo was accepted this month
38Set if the given cargo was accepted since last periodic processing


This function returns the cargo type for animation triggers NEWCARGO or NOCARGO being set.

If the newGRF has installed a cargo translation table, the cargo type will be an index into that table, or "255" (0xFF) if the cargo isn't included in the table. If there's no cargo translation table, the cargo type will simply be the climate-dependent cargo type.


This function returns the number of months exclusive transport rights, having been bought in the area, will still be in effect. If nonzero, the station will not receive any cargo.


Path-based signalling (PBS) is a feature that allows multiple trains to share a signal block, as long as their paths do not interfere. This greatly enhances entrances and exits of stations as well as junctions.

As a train approaches a path-based junction, it will reserve a path through it. If another train's desired path would cross the path of that train, it waits at the signal to reserve a path before entering the junction.

This function returns the following value:

7111PBSRESERVEDreserved track
4100PBSUNRESERVEDunreserved track

The bits are defined this way to allow easy fallback to non-PBS cases. If you need graphics to show the "unreserved" state in non-PBS cases, use bit 0, but if you need the "reserved" state in non-PBS cases, use bit 1. To explicitly check whether PBS is active or not, use bit 2.

All other bits are reserved and must not be used.

At the moment, PBS on/off refers to the switch setting, in a future alpha version it will actually refer to whether the switch is on and the current block actually uses PBS.


This set of functions returns information about the current tile, which platform it is on and how far along the platform. Coordinates are defined as shown in the picture below:

Depending on the type of station, above functions consider different sections of the station in question. For regular stations, the whole station is evaluated; and for irregular stations, the entire length and all adjacent platforms are being counted.

The term "counting" here refers to starting at the tile in question, and counting tiles in all four directions. The two directions aligned with the station direction will be the length of the station, and the other two directions give the number of platforms, see picture. For all of the above functions, this counting stops at the edge of the station, i.e. the first non-station tile.

As shown in above picture, functions either count from the northern most edge of the station, or from its southern most edge (plt_numrev(), plt_posrev()). For functions plt_midpos() and plt_midnum(), the position is counted from the middle, i.e. the center tile has "number of platform" = 0 and "position along platform" = 0. For even lengths and numbers of platforms, the middle tile is at position length/2 resp. number of platforms/2, e.g. for length = 6, it is tile 3 (i.e. the fourth tile).

To difference between counting number of platforms and position on platforms over the whole station, or only about a section of station tiles being built in association with the current tile, m4nfo uses bracket function self(). See example below.

Example (check platform position and numbers):
// single or multi tile?
def(6) plt_total(
	ref(24) if(1) // single
	ref(5) else   // not single

// position at platform
def(7) plt_pos(
	ref(6) if(0) // edge back 
	ref(3) else  // edge front or middle 

// index of station tiles
def(8) plt_index(
		cbr(2) if(nibble(0,0)) // x=0, y=0
		cbr(4) if(nibble(0,1))
		cbr(6) if(nibble(0,2))
		cbr(8) if(nibble(1,0))
		cbr(10) if(nibble(1,1))
		cbr(12) else

plt_edges() examines the location of the current tile inside a station with respect to the station's edges. The function returns a nibble with bits set for each station edge associated with the current tile. Bit counting is clockwise starting at the north-eastern edge, see picture:

Example (use of plt_edges to locate station tiles):
def(2) plt_edges(
	cbr(0) if(0)
	cbr(2) if(1)
	cbr(4) if(2)
	cbr(6) if(3)
	cbr(8) if(4)
	cbr(10) if(5)
	cbr(12) if(6)
	cbr(14) if(7)
	cbr(16) if(8)
	cbr(18) if(9)
	cbr(20) if(10)
	cbr(22) if(11)
	cbr(24) if(12)
	cbr(26) if(13)
	cbr(28) if(14)
	cbr(30) else


This function returns the type(s) of vehicles(s) having been visiting this station in the past, either TRAIN, BUS, TRUCK, AIRCRAFT, SHIP, or any combination:

Example (check vehicle types having visited):
def(5) servicedbytype(
	ref(4) if(BUS + TRUCK) // service by rvs
	ref(3) else


This function returns information about the current sprite type, i.e. whether it is a ground sprite (GROUNDSPRITE), a custom foundation sprite (FOUNDATION) or a building sprite (BUILDING). For this to work, the appropriate flag(s) have to be set in the station's flags() property function.

Example (check slope for building):
// ground sprite, foundation or building?
def(8) spritetype(
	ref(__groundsprites) if(GROUNDSPRITE) // ground sprite overlays
	ref(__foundations) if(FOUNDATION)     // custom foundations
	ref(__buildingsprites) else           // building sprites

def(9) callback(
	ref(7) if(CB_LAYOUT)
	ref(8) else

When using the Advanced Sprite Layer feature of OpenTTD, the allowed range of values are 0 .. 7.


This function returns the station age in years since 1920. The result is returned as a WORD:

Example (check age of station):
def(14) stationage(
	ref(2) if(<40)
	ref(4) if(41 .. 59)
	ref(6) if(60 .. 79)
	ref(10) else

test_pos(<block>), test_posrev(<block>), test_num(<block>), test_numrev(<block>), test_length(<block>), test_total(<block>), test_axis(<block>), test_position(<block>), test_size(<block>), test_tiletype([<shift-mask>,] <block>)

These functions return information about individually to be built station sections when in a CB_TILETYPE callback. Same format as plt_* functions above, but because these functions deal only with a station section yet to be built, no call to self() for "individually built sections" would be needed.

Functions test_position() and test_size() require auxiliary function nibble() to extract single return values.

Example (handle tile (0,0) as a non-track tile):
	nontrack(TTD_ROOFBOTH) // TTD tile types 4/5 and 6/7


def(3) test_position(
	cbr(4) if(nibble(0,0)) // make tile x=0/y=0 non-track
	cbfail() else

// menu
def(13) callback(
	cbr(18) if(CB_LAYOUT)
	ref(3) if(CB_TILETYPE)
	ref(ALL_ICON) else


These functions allow access of platform info when in a CB_SLOPE callback, i.e. before the station tile is built. All functions are counting platform numbers and lengths from the northern most corner of the station, as shown here.


Like above, but this function evaluates the to be built tile's slope in association with a "land slope check" callback (CB_SLOPE). Returned slope values are the same as in tinfo_slope().

Example (build station on slopes):
def(45) test_slope(
	ref(44) if(NORTH+WEST, NORTH+EAST) // in x: back + front
	ref(44) if(SOUTH+WEST, SOUTH+EAST) // in y: back + front

tinfo_flatwater(<Coordinate> | REGISTER, <block>)

This function returns a value of "32" if the tile given by <coordinate> is a flat/full water tile, else the slope value for this tile is returned in the lower 5 bits.

The coordinate parameter defines the offset relative to the current tile. Both nibbles are considered signed. Negative offsets move north/westwards, positive ones south/eastwards, accordingly. Coordinate parameter needs to be given by the auxiliary function pos(<x>, <y>,). In case of REGISTER as the first parameter, coordinates are not explicitly given but taken as the result from a preceding calculation, see calculate(). Both prerequisites also hold for functions tinfo_lclass(), tinfo_slope(), tinfo_statid(), tinfo_terrain(), tinfo_water(), and tinfo_waterclass().

tinfo_grfid(<Coordinate> | REGISTER, <block>)

This function returns the grf ID of the station tile given by <coordinate>, or "0" if the tile's station is a default station, or "0xFFFFFFFF" if the selected tile is not a station tile. Note that the result has to be checked by using auxiliary function label() in function if(). See example there.

tinfo_lclass(<Coordinate> | REGISTER, <block>)

This function returns the landscape class of the tile at the given coordinate (s.a.):

0LC_TERRAINbare land, grass, rocks, fields
1LC_RAILrailway track, with and w/o signals, fences
2LC_ROADroad, level crossings, depots
3LC_HOUSEtown buildings
4LC_TREESclimate-dependent trees
5LC_STATIONTILErailway station, airport, lorry and bus station, ship dock
6LC_WATERwater, coast, river bank, ship depot
7LC_VOIDinvisible border at bottom edges of map
8LC_INDUSTRYTILEindustry tile types
9LC_TUNNELBRIDGErailway or road tunnel, bridge
10LC_OBJECTtransmitter, lighthouse, statue, company-owned land, headquarter

tinfo_slope(<Coordinate> | REGISTER, [<shift-mask>,] <block>)

This function returns byte-packed slope information for the given station's tile. Given information is in relation to the lowest corner of the tile, i.e. WEST means that the western corner is above the lowest corner, etc.

This figure illustrates which kind of slope belongs to which corners raised:

You may as well check for so-called "steep slopes" (i.e. the corner opposite to the lowest one is two units higher) by using the value STEEP together with any three other corners, but keep in mind that stations may not be placed on a steep slope.

Extra parameter <shift-mask> might be applied to further refine the function's result. For this to work, the first parameter of function shiftmask() (the <shift> parameter) has to be set to zero (0), i.e. no shifting at all. The <mask> parameter should be set to a reasonable value, depending on the aim to achieve with result masking.

Example (check slope for station tile):
def(11) tinfo_slope(pos(0,0), shiftmask(0, NORTH+WEST+SOUTH+EAST),
	cbr(10) if(0)	// flat: ticket machine left
	cbr(12) if(NORTH+WEST, NORTH+WEST+EAST, NORTH+WEST+SOUTH) // ticket machine left
	cbr(10) if(SOUTH+EAST, SOUTH+EAST+WEST, SOUTH+EAST+NORTH) // fence left
	cbr(40) else	// fence left and right

In this example, slopes to check for are limited to any combinations of NORTH, WEST, SOUTH, and EAST, i.e. STEEP slopes would be automatically excluded from the "else" branch.

tinfo_statid(<Coordinate> | REGISTER, [<shift-mask>,] <block>)

This function returns the station ID at the given tile coordinate in relation to the current station tile in its lower Byte. In addition, it returns additional information in its high Byte (s.a.):

0 .. 1Tile membership (0: original TTD, 1: defined in current newGRF, 2: defined in foreign newGRF)
2Tile belongs to current station if bit set
3Tile is parallel (bit clear) or perpendicular (bit set) to the current one
4 .. 5TTD type platform info (0: plain platform; 1: platform with building; 2: platform with roof, left side; 3: platform with roof, right side)
6 .. 7Reserved, do not use

You should use auxiliary function shiftmask() to access the needed bits. E.g., when checking a tile's ID, you should always check if it was defined in the current newGRF, hence you'd have to check not only for the ID itself, but for the value 0x100 + ID. In case you want to check for a foreign newGRF, you should use 0x200 + ID.

If the tile is not a station tile at all, the return value will be 0xFFFF, i.e. all bits set.

Please note that this function's first parameter has to be given by the auxiliary function pos(<x>, <y>)

Example (check for station "roof" tiles):

def(1) tinfo_statid(pos(0,-1), shiftmask(0,_THISGRF+ROOFS),
	cbr(1) if(_THISGRF+ROOFS) // station-ID is 0x0E
	cbr(2) if(0xFFFF)         // not station
	cbr(0) else		  // other station-ID

Sometimes it is needed to check for a station tile's "tile type" as well (since the IDs might be the same). This can be done as well:

Example (check also for station tile types):
define(_THISGRF_TT, 0x3100) // mask "tiletype" and "defined in this GRF"

def(17) tinfo_statid(pos(1,0), shiftmask(0,_THISGRF_TT+0xFF),
	cbr(2) if(0x2100+PTRACK_OPASS) // tiletype for overpass is "4" (2)
	cbr(6) if(0x3100+PTRACK_OPASS) // tiletype for overpass is "6" (3)
	cbfail() else

tinfo_terrain([<Coordinate> | REGISTER,] <block>)

This function returns the terrain type for the current tile of the station (s.a.). Return values are NORMAL, DESERT, RAINFOREST, and SNOW (on or above snowline).

If an additional coordinate parameter is given, the terrain type of the given tile is returned. Please note that coordinates must be given by the auxiliary function pos(<x>, <y>,)

tinfo_trackconnect([<shift-mask>,] <block>)

This function returns bit-coded information whether the rail tracks continue in the eight tiles adjacent to the station tile.

BitValueSet if rail continues in direction of:
40x10+Length, +Platforms
50x20-Length, +Platforms
60x40+Length, -Platforms
70x80-Length, -Platforms

The following picture illustrates which bits represent which tile for the two possible station orientations:

Bits 0 to 3 are set if there is track on the given tile, and it has a connection to the station tile. For bits 2, 3, the station of course has itself no connection to those tiles, but this doesn't matter for this variable. Bits 4 to 7 check connections to the neighbouring platform tile, i.e. bits 4 and 5 resp. 6 and 7 indicate a connection from that tile to tile 2 resp. 3.

Extra parameter <shift-mask> might be applied to further refine the function's result bits. For this to work, the first parameter of function shiftmask() (the <shift> parameter) has to be set to zero (0), i.e. no shifting at all. The <mask> parameter should be set to a reasonable value, depending on the aim to achieve with bit masking.

Example (determine buffer direction):
def(24) tinfo_trackconnect(
	ref(21) if(1)   // top
	ref(22) if(2)   // bottom
	ref(25) if(4,8) // sideways - only on (false) slope
	ref(23) else    // double buffer

tinfo_trackexist([<shift-mask>,] <block>)

This function returns bit-coded information whether there are rail tracks on the tile and disregard whether the track is connected to the station or not entirely. Bit-coded results are handled in the same way as for tinfo_trackconnect().


This function returns the railway track type, with 0 = regular rail, 1 = electrified rail, 2 = monorail, 3 = maglev, and any additional rail according to its slot number.

tinfo_water(<Coordinate> | REGISTER, <block>)

This function returns a value of "1" if the tile given by <coordinate> is a full water or a coast tile, else "0" is returned. Again, the coordinate parameter needs to be given by the auxiliary function pos(<x>, <y>,)

Example (check for water near station):
def(47) tinfo_water(pos(-1,0), // back
	ref(2) if(1)           // is water
	ref(46) else

tinfo_waterclass(<Coordinate> | REGISTER, <block>)

This function returns the "water class" of the tile given by <coordinate>. Water class values are as follows:

0WC_LANDUndefined / land
1WC_SEASea, ocean

Unlike the landscape class "water", the "water class" is not changed when a water tile is built over, e.g. by an object showing a ship. Using tinfo_waterclass() might come in handy in this case.

Again, the coordinate parameter needs to be given by the auxiliary function pos(<x>, <y>)


This function returns the year the station was built. The base year is 1920.

Example (check waypoint building years):
def(4) yearbuilt(
	cbr(2) if(<1940)
	cbr(4) if(1940 .. 1959)
	cbr(6) if(1960 .. 1979)
	cbr(8) if(1980 .. 1999)
	cbr(10) else

random(<list::trigger>, <randombit>, <list::reference>)

Unlike the performance functions above, whose results are always determined by a predictable decision, one can also use random functions to pick one of several graphics sets or callback results.



Trigger Meaning
BUILT when constructed (no trigger)
NEWCARGO new cargo waiting
NOCARGO no more cargo
ARRIVE train arrives (starts unloading/loading)
LEAVE train leaves (done unloading & loading)
LOADING train loads or unloads cargo
PBSRESERVE train reserves platform (using PBS)
ALLTRIGGERS re-randomize only if all triggers have occurred

Also note that none of the above triggers will actually trigger unless property function setcargotriggers() has at least one trigger set. NEWCARGO will be triggered for any of the cargo types set in setcargotriggers(), but NOCARGO will only be triggered if all of those cargo types have no more cargo waiting. Triggers ARRIVE, LEAVE and PBSRESERVE are triggered no matter what cargo types the train transports, as long as at least one trigger has been set.

Triggers ARRIVE, LEAVE, LOADING and PBSRESERVE only affect the platform on which they occur, as well as the random bits of the station, but not other platforms.

Because already occured triggers are only stored once per station (not distinguishing tiles or cargo types), adding ALLTRIGGERS to random triggers does not make much sense for stations.


Stations have 16 random bits (bits 0 .. 15) shared for the whole station, and 4 random bits (bits 16 .. 19) per station tile. TTDPatch actually only implements 8 shared random bits, i.e. bits 8 .. 15 are always zero.

Setting randombit determines the first bit to be re-randomized, as well as basing the random graphics on. Only those bits that actually get triggered will be re-randomized. This will make it possible to have independent sets of bits for independent triggers. The total number of bits used is the 2-logarithm of the number of references used, e.g., for 16 references, 4 bits are used.

To get tile-based randomness, therefore one has to use randombit=16 and a number of references of no more than 16 (since only 4 random bits are available per tile).


The number of referenced sets to choose from must be a power of 2, i.e. 2, 4, 8, 16 etc.

Example (4 different cargo states):
def(13) random(ARRIVE,16,ref(1),ref(2),ref(2),ref(3))

def(13) random({ARRIVE, LEAVE},16,ref(1),ref(2),ref(2),ref(3))


Note that re-randomisation, both in TTDPatch and in OpenTTD, only takes place in the graphics chain, but not for callbacks. I.e., if a random action is used inside a callback chain and it should re-randomise the trigger bits, there has to be added a "dummy" random action into the graphics chain as well, for the only purpose to re-randomise the bits.

Therefore, for using random functions in callback chains, m4nfo supplies two special functions:

randomcb(<label>, <list::trigger>, <randombit>, <list::reference>)

This function is used in callback chains like the normal random() function is used in the graphics chain, except that it won't ever re-randomize the specified trigger(s). Instead, this is done by function

rerandom(<label>, <block>)

which has to be placed in the graphics chain. By specifying different labels, different randomisations can be used in parallel. See example.

Example (2 different re-randomisations):
// top platform
def(6) randomcb(_TOP, ARRIVE, 16, ref(1), ref(2), ref(2), ref(3))

// bottom platform
def(7) randomcb(_BOTTOM, {ARRIVE, NOCARGO}, 16, ref(1), ref(2))

def(8) plt_num(
		ref(6) if(0) // top platform
		ref(7) else  // bottom


def(9) rerandom(_TOP,
	ref(0) // graphics

def(10) rerandom(_BOTTOM,
	ref(0) // graphics

def(11) plt_num(
		ref(9) if(0) // top platform
		ref(10) else // bottom

// check callbacks
def(12) callback(
	ref(8) if(CB_LAYOUT) // callback
	ref(11) else	     // graphics/re-randomisation

Auxiliary functions

Auxiliary functions are used in the context of some of the functions above. As such, they don't get a def() nor do they ref() anything else. They're mainly used to evaluate special parameters for performance functions.


This function adjusts a performance function result to a more useful range. The first parameter defines the value to add to the result, and the second parameter specifies the modulo (remainder of division by) on the sum of the performance function result and the first parameter.

Example (checking platform number):
def(10) plt_midnum(addmodulo(1,2),
	ref(5) if(1)
	ref(6) else


This function is used to check a returned byte-packed coordinate value in the usual form (<x>, <y>). Parameter range is [-8 ... +7], both for x- and y-offsets. For an example see here.


This function supplies the given coordinates in a byte-packed form to another function. Parameter range is [-8 ... +7], both for x- and y-coordinates. Please note that the coordinate (0,0) refers to the current tile itself. For an example see below.


This function defines a hierarchy level. Instead of the whole station, only the section of station tiles being built in association with the current tile are addressed by the appropriate performance function, see here for examples.


This function adjusts a performance function result to a more useful range. The first parameter defines the value to right-shift the result, and the second parameter gives the value with which to AND the result after shifting.

Please note that parameter <mask> might be of WORD-size in a given context.

Example (checking tile's own slope):
//  (middle)
def(8) tinfo_slope(pos(0,0),shiftmask(0,NORTH+SOUTH),
	ref(5) if(NORTH)
	ref(6) if(SOUTH)
	ref(7) else