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.
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:
There are several things you have to do to get callbacks to work:
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.
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().
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))
)
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.
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))
)
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.
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
)
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.
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
)
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.
//------------------------------------------------------------------ // 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)))