The m4nfo Tutorial

Callback tutorial

Introduction

Since TTDPatch 2.0.1 alpha 11, it is possible to use 'callbacks', in which the newGRFs can influence how TTDPatch features work. This is a lot more sophisticated than simply using feature property functions to choose various settings, in that callbacks can use the full capabilities of the diverse performance functions including randomisation.

How it works

When TTDPatch wants to use the value of certain properties, it can ask the newGRF what value to use, instead of simply looking it up in the property function. It does this if the corresponding callback type has been set in the vehicle's property function callbacks(). Then, the following happens:


Because callbacks are different from regular vehicle graphics, the last function in the chain must have a 'callback return value' (i.e. with bit 15 set), which is invalid for regular graphics. Therefore, at least one function in the chain has to be callback(), to decide whether this is a callback or the determination of graphics.

How to define callbacks

There are several things you have to do to get callbacks to work:

  1. If necessary, enable the callback. For vehicles, set the callback in the property function for the vehicle which should use callbacks
  2. Set a default for the value that the callback modifies, e.g. use property function loadamount() when using the load amount callback CB_LOAD
  3. Define a makevehicle() function for the vehicle if it doesn't have one already
  4. Add a callback() function that checks the callback type. See the list of train callbacks for the available types of callbacks
  5. The last performance function in the chain has to return a callback result when in a callback, or a regular set-id when not in a callback
  6. Make sure that the "default" check of the callback() function block (corresponding to unknown callbacks) points to a regular graphics sprite instead of a function that returns callback results, ideally using the same function or sprite as in the non-callback case. This way, unknown callbacks will fail instead of returning valid, but wrong, results.
As explained in the user manual, a callback result is something like 'cbr(6)' (with "6" being used as return value of the callback), instead of a regular function reference that would be for example 'ref(4)'.

Examples

Be aware that following examples don't result into 100% complete newGRFs, but instead concentrate on the most important code features. E.g., in the given train property functions you'd have to add even more properties to get a well-working newGRF, or, in case an example describes only one vehicle in detail, references to other vehicles, e.g. a locomotive, are given in a symbolic form ("xx", "nn") rather than implementing it in detail and giving correct veh-IDs or function references.

Example 1: using sound callbacks (CB_SOUND)

The first example is a very simple application. It will set new sounds for a locomotive by using CB_SOUND. The new sounds will be supplied by two .wav files using function soundtable(). The type of sound event (under which circumstances a particular sound should be played) is returned by function soundevent().

Example (using sound callbacks):
setfeature(_TRAIN)

grf_init(GRF_TUT1, ...)

// set engine (veh-ID 0) to use new sprites and enable for sound callback
definevehicle(0,{},
	newgraphics()
	callbacks(CB_SOUND)
)

// define sound files
soundtable(
	defsnd(_DEPART, br38s.wav) 
	defsnd(_TUNNEL, br38t.wav)
)

// engine sprites go here
spriteblock(
	set(
		sprite( ... )
		sprite( ... )
		sprite( ... )
		sprite( ... )
	)
)

def(0) spriteset(move(0),load(0)) // engine

// engine performance functions

def(1) soundevent(
	refsnd(_DEPART) if(SND_START)  // whistle: departure
	refsnd(_TUNNEL) if(SND_TUNNEL) // whistle: in tunnel
	cbfail() else		       // else CB fail
)

// switch between callback and graphics branch
def(2) callback(
	ref(1) if(CB_SOUND) // is sound callback, set sound
	ref(0)		    // graphics
)

makevehicle(0,
	default(ref(2))
)


Example 2: using Callbacks for load amount, capacity and additional text suffixes

Suppose you want to build passenger coaches with both first and second class, having different livery and capacity, loading amount, and additional text suffixes. For this to work we'll need callbacks CB_LOAD (load amount), CB_RCAP (refitted capacity) and CB_TSFX (cargo subtype display).

In addition, we'll use the function refitted() to be able to change livery, loading amount and capacity deliberately, even if coaches carry only one type of cargo, namely "passengers".

In the source code, the actual "refitting" is done by calling function refitted(), which reports how many times a vehicle was refitted to the same cargo type. In game, there will be two entries then in the refit menu containing these text suffixes generated by a defgrftext() function. Namely "passengers (1st class)" and "passengers (2nd class)" from which to choose the desired type of coach. Likewise, load amount and capacity are linked to function refitted() as well and will be changed accordingly.

Example (using callbacks for load amount, refitted capacity and text suffixes):
setfeature(_TRAIN)

grf_init(GRF_TUT2, ... )

// define additional text suffixes
defgrftext(0,ALL,
	" (1st class)"
	" (2nd class)"
)

// set coach (veh-ID 27) to use new sprites, 
// and enable for callbacks CB_LOAD, CB_RCAP, CB_TSFX 
definevehicle(27,{},
	newgraphics()
	callbacks(CB_LOAD, CB_RCAP, CB_TSFX)
)

// coach sprites go here
spriteblock(
	set(
	// 4 sprites for 1st class coach
		sprite(...)
		sprite(...)
		sprite(...)
		sprite(...)
	)
	set(
	// 4 sprites for 2nd class coach
		sprite(...)
		sprite(...)
		sprite(...)
		sprite(...)
	)
)

def(0) spriteset(move(0),load(0)) // 1st class
def(1) spriteset(move(1),load(1)) // 2nd class

// engine performance functions

// next 4 functions are checking the refitting possibilities:
// [1st class, 48 passengers, load amount = 6]  and
// [2nd class, 56 passengers, load amount = 8]

// set load amount
def(2) refitted(
	cbr(6) if(0) // 1st class = 6/tick
	cbr(8) else  // 2nd class = 8/tick
)

// set refitted capacity
def(3) refitted(
	cbr(48) if(0) // 1st class = 48 pass
	cbr(56) else  // 2nd class = 56 pass
)

// set text suffixes
def(4) refitted(
	grftext(0) if(0) // "1st class"
	grftext(1) else  // "2nd class"
)

// set livery
def(5) refitted(
	ref(0) if(0) // 1st class
	ref(1) else  // 2nd class
)

// switch between callbacks and graphics branch
def(6) callback(
	ref(2) if(CB_LOAD) // set load amount
	ref(3) if(CB_RCAP) // set refitted capacity
	ref(4) if(CB_TSFX) // set text suffix
	ref(5) else	   // graphics
)

makevehicle(27,
	default(ref(6))
)


Example3: using callbacks 11 (wagon length) and 16 (articulated engine)

This example will demonstrate how to use CB_ARTI for designing "articulated vehicles", e.g. locomotives with tenders. We'll set up two locomotives using two different tenders: <engine1> will use a standard short <tender1> and <engine2> will use the even shorter <tender2> by applying CB_WLEN.

Example (using callbacks for articulated vehicles and vehicle length):
setfeature(_TRAIN)

grf_init(GRF_TUT3, ... )

// set the generic tender (veh-ID 45) to use new sprites, 
// enable for callback CB_WLEN
definevehicle(45,{},
	climate(INACTIVE)  // don't show up in purchase menu
	newgraphics()      // use new sprites 
	callbacks(CB_WLEN) // enable callback for vehicle length
	shortening(2)      // make shorter by 25%
)

// tender sprites go here
spriteblock(
	set(
		... // 8 sprites for tender1
	)
	set(
		... // 8 sprites for tender2
	)
)

def(240) spriteset(move(0),load(0)) // tender1 (0xF0)
def(0) spriteset(move(1),load(1))   // tender2

// tender2 (0xF1) is shorter
def(241) callback(
	cbr(3) if(CB_WLEN) // make shorter by 37.5%
	ref(0) else	   // graphics
)

makevehicle(45,
	default(ref(240)) // standard tender
)

// set two engines (00 and 01) to use new sprites, 
// enable for callback CB_ARTI
definevehicle(0,{},
	newgraphics()      // use new sprites 
	callbacks(CB_ARTI) // enable callback for articulated vehicle
)

definevehicle(1,{},
	newgraphics()      // use new sprites 
	callbacks(CB_ARTI) // enable callback for articulated vehicle 
)

// engine sprites go here
spriteblock(
	set(
		... // 8 sprites for engine1
	)
	set(
		... // 8 sprites for engine2
	)
)

def(0) spriteset(move(0),load(0)) // engine1
def(1) spriteset(move(1),load(1)) // engine2

// <engine1> uses standard <tender1>
// engine is articulated, so show either engine (0) or tender (240)
// depending on position in consist.
// Because "multihead" is allowed, we need the modulo operation here.
def(2) veh_posrel(MOD2, 
	ref(0) if(0)  // engine1
	ref(240) else // tender1
)

def(3) articulated(
	addveh(45) if(1) // add tender (veh-ID 45)
	ENDLIST else     // end of articulated vehicle
)

// Switch between callback and graphics branch
def(4) callback(
	ref(3) if(CB_ARTI) // articulated callback
	ref(2) else        // graphics
)

makevehicle(0,
	default(ref(4)) // engine1 with tender1
)


// <engine2> uses shorter <tender2>
// engine is articulated, so show either engine (1) or tender (241)
def(2) veh_posrel(MOD2, 
	ref(1) if(0)  // engine1
	ref(241) else // tender1
)

// switch between callback and graphics branch
def(4) callback(
	ref(3) if(CB_ARTI) // articulated callback, same as for <engine1>
	ref(2) else        // graphics
)

makevehicle(1,
	default(ref(4))       // engine2
	override(45,ref(241)) // override with tender2

)


Example4: using callbacks for powered wagons, articulated vehicles, engine attach, and additional menu text

This example explains how to use CB_POWR (powered wagon), CB_ARTI (articulated vehicle), CB_ATAC (engine attach), and CB_TEXT (additional text in menu) to construct a four-part EMU.

The EMU is composed from the same building block, an electric rail car (veh-ID = 62). In addition, it should be allowed to link three of these four-part EMUs, i.e. a full consist may contain 12 rail cars in total.

For sake of realism, no "foreign" vehicles should be allowed to be attached to the EMU. This is achieved by checking the veh-ID by veh_id(). Appropriate error messages will be generated if there's something wrong, either with number or with type of added vehicles.

Please notice that CB_ATAC and CB_TEXT do not need to be introduced in property function callbacks().

Furthermore, it is shown how to make an explicit menu entry for such an EMU. For this to work, we need an extra sprite depicting not the single rail car but the complete four-part EMU. Because this sprite will be used only inside the buying menu, the corresponding sprite block needs only this single sprite for the horizontal direction. All other directions are not needed and hence we don't have to include sprites for them.

Also, the function chain for the menu entry needs references for callbacks CB_ARTI and CB_TEXT. The latter is used to place the text of additional information into the menu entry, and CB_ARTI is needed to get the capacity of the four-part EMU to show up correctly in the buying menu.

Example (using callbacks for powered wagons, articulated vehicles, engine attach, and additional menu text):
setfeature(_TRAIN)

grf_init(GRF_TUT4, ... )

// 'engine attach' error messages
define(ATT_CARBADNUM,0x00)
define(ATT_CARBADTYPE,0x01)

defgrftext(ATT_CARBADNUM,ALL,
	" (wrong number of cars)",
	" (wrong type of car)"
)

// additional text for menu
define(TLH_4PARTEMU,0x02)
defgrftext(TLH_4PARTEMU,ALL,
	CRLF T_LORANGE "4-part EMU for commuter service."
	" (Links max 12 parts)"
)

// set rail car (veh-ID 98) to use new sprites, 
// enable callbacks for powered wagons and articulated vehicles
define(_BR425,98)
definevehicle(_BR425,{},
	newgraphics()
	enginetype(ELECTRIC)
	callbacks(CB_POWR, CB_ARTI)
)

//----------------------------------------------------------
// the "train"
//----------------------------------------------------------

// all rail car sprites go here
spriteblock(
	set(
		sprite( ... ) // 8 sprites for 1st car
	)
	set(
		sprite( ... ) // 8 sprites for 2nd car
	)
	set(
		sprite( ... ) // 8 sprites for 3rd car
	)
	set(
		sprite( ... ) // 8 sprites for 4th car
	)
)

// use above sets of sprites
def(0) spriteset(move(0),load(0)) // 1st car
def(1) spriteset(move(1),load(1)) // 2nd car
def(2) spriteset(move(2),load(2)) // 3rd car
def(3) spriteset(move(3),load(3)) // 4th car

// determine which car we are at. We may link up to four
// EMUs in a consist, so use the modulo-4 function.
def(2) veh_posrel(MOD4, 
	ref(3) if(3) // 4th car
	ref(2) if(2) // 3rd car
	ref(1) if(1) // 2nd car
	ref(0) else  // 1st car
)

// set callback for articulated vehicle
def(5) articulated(
	addveh(_BR425) if(1 .. 3) // 3 additional parts of this veh-ID
	ENDLIST else		  // end articulated vehicle
)

// set callback for engine attach
// allow only 3 * EMU (= 12 cars) in total
def(6) veh_num(
	engine(
		attach(ATT_OK) if(0 .. 9)  // allow adding of additional 8 cars
		attach(ATT_CARBADNUM) else // error: "wrong number of cars"
	)
)

// allow only EMU to attach itself, nothing else
def(7) veh_id(
	ref(6) if(_BR425)	    // allow, check total number of cars
	attach(ATT_CARBADTYPE) else // error: "wrong type of car"
)

// only car 1 and, if exist, 5 and 9 should be sparking 
def(8) veh_posabs(FRONT, 
	effect(electric(4)) if(1,5,9) // spark
	ref(0) else		      // no sparks
)

// switch between callback and graphics branch
def(9) callback(
	ref(5) if(CB_ARTI) // articulated vehicle
	ref(7) if(CB_ATAC) // engine attach
	ref(8) if(CB_POWR) // power and effects
	ref(4) else	   // graphics
)

//----------------------------------------------------------
// menu entry
//----------------------------------------------------------

spriteblock(
	set(
		sprite(emu.pcx 226 120 01 15 91 -27 -11)
	)
)

def(0) spriteset(move(0),load(0)) // menu pic

// switch between callback and graphics branch
def(1) callback(
	ref(5) if(CB_ARTI)     // (for capacity calculation)
	grftext() if (CB_TEXT) // show additional text in menu
	ref(0) else	       // graphics
)

makevehicle(_BR425,
	link(ref(1),MENU) // menu entry
	default(ref(9))   // vehicle
)


Example5: using the generic callback to allow the AI to build suitable stations

In original TTD, the AI was hardcoded. For vehicles, it had a fixed list for each cargo type, and for stations it simply used one of the 4 possible TTD station types.

For the first time, TTDPatch implemented an interface between the AI and newGRFs, by introducing a generic callback ("AI construction/purchase selection"). This is called for various decisions when the AI is constructing a new route. By use of this callback it became possible to make the AI selection depend on source and destination industries as well as service distance, among other things.

In this example, it is shown how to teach the AI to build 1- or 2-platform stations depending on the cargo to be transported, the current year, and the population of the town the station is to be built.

In m4nfo, the generic callback is called CB_AISELECT, it returns the station-ID of the station to be built (by function cbr()), most probably worked out after evaluation of certain variables, see example code.

Please note the special form of the makestation() function which doesn´t specify any station-ID at all. This creates a generic feature-specific definition not associated with any particular feature, which is used for a generic callback.

Please note as well that this code example is part of the NewStations Set, and as such it references station types (ROOFS, BENCHES, FLATROOF, ...) defined in this very set.

Please also note that in OpenTTD, the AI is implemented by self-contained modules, so you'll need one which implements building stations as well, not only vehicles.

Example (let the AI build stations):
//------------------------------------------------------------------
// AI platforms                   
//------------------------------------------------------------------

// Randomize using engine tick anim_counter():
// need to shift right by 3 bits because one player is processed 
// each tick, so mod 8 would always be the same for each player

// Pick n*1 station, <1950
def(1) anim_counter(shiftmask(3,7),
	cbr(ROOFS) if(0 .. 2)	// roofs
	cbr(BENCHES) if(3 .. 4) // benches
	cbr(SHALL) else		// small hall
)

// Pick n*1 station, >1950
def(2) anim_counter(shiftmask(3,7),
	cbr(ROOFS) if(0 .. 1)	// roofs
	cbr(BENCHES) if(2 .. 3) // benches
	cbr(SHALL) if(4 .. 5)	// small hall
	cbr(PFRONT) if(6)	// carpark front
	cbr(PBACK) else		// carpark back
)

// Pick n*1 station depending on year
def(3) year(
	ref(1) if(<1950)
	ref(2) else
)

def(1) townpopulation(
	cbr(FLATROOF) if(<800) // built "flat roofs" if small town
	cbr(GLASS)	       // built modern "glass roofs" for larger towns
)

// Pick n*2 station depending on year
def(4) year(
	cbr(ROOFS) if(<1960)	       // build "roofs"
	cbr(FLATROOF) if(1960 .. 1979) // build "flat roofs"
	ref(1) else		       // check town population
)

// Check number of platforms
def(5) AI_stationwidth(
	ref(3) if(1)  // 1-track platform
	ref(4) if(2)  // 2-track platform
	cbfail() else // only 1 and 2 track supported
)

// Check passenger/mail service
def(6) AI_cargo(
	ref(5) if(PASS, MAIL)
	cbfail() else // freight not supported!
)

// Check callback type:
// return invalid result for no callback or callback other than 18
def(7) callback(
	ref(6) if(CB_AISELECT)  // AI construction
	cbfail(0) if(1 .. 0xFF) // invalid CB
	NOTAVAILABLE else	// no callback
)

// define generic station callback
makestation(default(ref(7)))