NMLTutorial/Train three part articulated
The example used here is from the NML source. The code for this was originally written in NFO by DJNekkid for the 2cc Trainset and rewritten in NML by Hirundo. The graphics used in the example are by Purno. Code and graphics are both licensed according to the GPL v2 or later. The code has been modified for the purpose of this tutorial
This continues the second part of the train example. The train will be made into an articulated EMU that can be purchased all at once.
Callbacks
Except the articulated vehicle callback, we'll add two more callbacks to only allow attaching more of these multiple units (and not other trains/wagons) and to give the train a maximum length.
The callbacks and switch blocks that are needed for this will be discussed below. What callbacks are available for vehicles can be found in the NML Documentation. From here you can decide what callbacks suit your needs and find their names to be used in the graphics block.
Articulated part callback
We have seen this one before in the tram example. Returning a single value is not enough for this callback, so we need to reference a switch block to handle the callback.
Add to the graphics block:
articulated_part: sw_icm_articulated_part;
The callback needs to return the identifier of a vehicle for as long as you want to keep adding parts and return CB_RESULT_NO_MORE_ARTICULATED_PARTS
to stop the process. During the callback the game will continuously call the callback, each time increasing the extra_callback_info1
variable by one.
switch(FEAT_TRAINS, SELF, sw_icm_articulated_part, extra_callback_info1) { /* Add two articulated parts, for a total of three */ 1 .. 2: return item_icm; return CB_RESULT_NO_MORE_ARTICULATED_PARTS; }
This causes the callback to be called three times, adding two extra parts to the item_icm
vehicle. For an extended explanation of this, see the tram example.
Start/stop callback
The start/stop callback is called whenever a vehicle is started in the depot and decides if the vehicle may leave the depot. The callback must return CB_RESULT_NO_TEXT
to allow the vehicle to leave the depot. If you want to prevent the vehicle from leaving the depot, the callback must return a string containing an error message.
We want to use this callback to check the vehicle length. As such it makes sense to base the decision on the length of the train, which can be found using the num_vehs_in_consist
variable. Because we need to base a decision on some variable, we'll use a switch block to do that.
Add to the graphics block:
start_stop: sw_icm_start_stop;
We want to limit our train length to a maximum of four coupled EMUs. As each EMU has a length of three, the total possible train length is twelve. So for train lengths 1 to 12 we want to return CB_RESULT_NO_TEXT
(and allow the train to leave the depot). For any other train length we return a string to disallow starting the train and informing the user why that is.
switch(FEAT_TRAINS, SELF, sw_icm_start_stop, num_vehs_in_consist) { /* Vehicles may be coupled to a maximum of 4 units (12 cars) */ 1 .. 12: return CB_RESULT_NO_TEXT; return string(STR_ICM_CANNOT_START); }
As we added a new string, define it in the language file:
STR_ICM_CANNOT_START :... train too long (max. 4 coupled EMUs).
Wagon attach callback
We only want to allow attaching more of the same EMUs to this train. Amongst others, the callback can return a custom string to disallow or return CB_RESULT_ATTACH_ALLOW
to allow attaching. The decision must be made on the basis of the identifier of the wagon that is being attached. We can use the vehicle_type_id
variable to make that decision, using a switch block.
Add to the graphics block:
can_attach_wagon: sw_icm_can_attach_wagon;
This will be the switch block:
switch(FEAT_TRAINS, SELF, sw_icm_can_attach_wagon, vehicle_type_id) { /* SELF refers to the wagon here, check that it's an ICM */ item_icm: return CB_RESULT_ATTACH_ALLOW; return string(STR_ICM_CANNOT_ATTACH_OTHER); }
If the attached wagon is of the type item_icm, it is allowed. Otherwise, a custom error message is displayed. This obviously means that we have to add this message to the language file:
STR_ICM_CANNOT_ATTACH_OTHER :... only other ICMs can be attached to ICM.
Graphics
The final step in making this work is adding the graphics for the second and third part of the train. This is the same as in the tram example: point to a switch from the graphics block, make a decision based on the position_in_consist
variable and point to the spritesets from there.
We already have the graphics and spritesets defined, so that's no problem.
/* Choose between front, middle and back parts */ switch(FEAT_TRAINS, SELF, sw_icm_graphics, position_in_consist % 3) { 0: set_icm_front_lighted; 2: set_icm_rear_lighted; set_icm_middle; }
This particular switch block actually first does a calculation on the position_in_consist
variable before making a decision on the result of that calculation. Here the modulo 3 of the value of position_in_consist
is calculated (%
is the modulo operator, see Wikipedia for more details on modulo but basically it's the remainder after devision (by three in this case)).
So for the first three vehicle parts the expression will yield 0, 1 and 2. For part four through six, it will again yield 0, 1 and 2, etc. The callback decision is then made on these three values alone: for part 0 use graphics for the train front, for part 2 use graphics for the train end and for all other parts use graphics for the middle.
Also don't forget to reference this switch instead of the spriteset directly from the graphics block:
default: sw_icm_graphics;
The spriteset for the purchase menu sprite is already there. We only need to reference it from the graphics block"
purchase: set_icm_purchase;
Total code so far
When put in the correct order, this should now encode as a working NewGRF. The total code so far:
/* Define grf */ grf { grfid: "NML\00"; /* GRF name and description strings are defined in the lang files */ name: string(STR_GRF_NAME); desc: string(STR_GRF_DESC); /* This is the first version, start numbering at 0. */ version: 0; min_compatible_version: 0; } /* Define a rail type table, * this allows referring to railtypes * irrespective of the grfs loaded. */ railtypetable { RAIL, ELRL, MONO, MGLV, } /* Basic template for 4 vehicle views */ template tmpl_vehicle_basic(x, y) { // arguments x, y: coordinates of top-left corner of first sprite [x, y, 8, 24, -3, -12] //xpos ypos xsize ysize xrel yrel [x + 9, y, 22, 20, -14, -12] [x + 32, y, 32, 16, -16, -12] [x + 65, y, 22, 20, -6, -12] } /* Template for a vehicle with only 4 views (symmetric) */ template tmpl_vehicle_4_views(num) { // argument num: Index in the graphics file, assuming vertical ordering of vehicles tmpl_vehicle_basic(1, 1 + 32 * num) } /* Template for a vehicle with 8 views (non-symmetric) */ template tmpl_vehicle_8_views(num, reversed) { // argument num: Index in the graphics file, assuming vertical ordering of vehicles // argument reversed: Reverse visible orientation of vehicle, if set to 1 tmpl_vehicle_basic(reversed ? 89 : 1, 1 + 32 * num) tmpl_vehicle_basic(reversed ? 1 : 89, 1 + 32 * num) } /* Template for a single vehicle sprite */ template tmpl_vehicle_single(num, xsize, ysize, xoff, yoff) { [1, 1 + 32 * num, xsize, ysize, xoff, yoff] } /* Define the spritesets, these allow referring to these sprites later on */ spriteset (set_icm_front_lighted, "gfx/icm.png") { tmpl_vehicle_8_views(0, 0) } spriteset (set_icm_rear_lighted, "gfx/icm.png") { tmpl_vehicle_8_views(1, 1) } spriteset (set_icm_front, "gfx/icm.png") { tmpl_vehicle_8_views(2, 0) } spriteset (set_icm_rear, "gfx/icm.png") { tmpl_vehicle_8_views(3, 1) } spriteset (set_icm_middle, "gfx/icm.png") { tmpl_vehicle_4_views(4) } spriteset (set_icm_purchase, "gfx/icm.png") { tmpl_vehicle_single(5, 53, 14, -25, -10) } spriteset (set_icm_invisible, "gfx/icm.png") { tmpl_vehicle_single(6, 1, 1, 0, 0) } /* Choose between front, middle and back parts */ switch(FEAT_TRAINS, SELF, sw_icm_graphics, position_in_consist % 3) { 0: set_icm_front_lighted; 2: set_icm_rear_lighted; set_icm_middle; } /* --- Articulated part callback --- */ switch(FEAT_TRAINS, SELF, sw_icm_articulated_part, extra_callback_info1) { /* Add three articulated parts, for a total of four */ 1 .. 2: return item_icm; return CB_RESULT_NO_MORE_ARTICULATED_PARTS; } /* --- Start/stop callback --- */ switch(FEAT_TRAINS, SELF, sw_icm_start_stop, num_vehs_in_consist) { /* Vehicles may be coupled to a maximum of 4 units (12 cars) */ 1 .. 12: return CB_RESULT_NO_TEXT; return string(STR_ICM_CANNOT_START); } /* --- Wagon attach callback --- */ switch(FEAT_TRAINS, SELF, sw_icm_can_attach_wagon, vehicle_type_id) { /* SELF refers to the wagon here, check that it's an ICM */ item_icm: return CB_RESULT_ATTACH_ALLOW; return string(STR_ICM_CANNOT_ATTACH_OTHER); } /* Define the actual train */ item(FEAT_TRAINS, item_icm) { /* Define properties first, make sure to set all of them */ property { name: string(STR_ICM_NAME); // not available in toyland: climates_available: bitmask(CLIMATE_TEMPERATE, CLIMATE_ARCTIC, CLIMATE_TROPICAL); introduction_date: date(1983, 1, 1); model_life: VEHICLE_NEVER_EXPIRES; vehicle_life: 30; reliability_decay: 20; refittable_cargo_classes: bitmask(CC_PASSENGERS); non_refittable_cargo_classes: bitmask(); // refitting is done via cargo classes only, no cargo types need explicit enabling/disabling // It's an intercity train, loading is relatively slow: loading_speed: 6; cost_factor: 45; running_cost_factor: 100; // Changed by callback sprite_id: SPRITE_ID_NEW_TRAIN; speed: 141 km/h; // actually 140, but there are rounding errors misc_flags: bitmask(TRAIN_FLAG_2CC, TRAIN_FLAG_MU); refit_cost: 0; //refit costs don't apply to subcargo display // callback flags are not set manually track_type: ELRL; // from rail type table ai_special_flag: AI_FLAG_PASSENGER; power: 1260 kW; // Changed by CB running_cost_base: RUNNING_COST_ELECTRIC; dual_headed: 0; cargo_capacity: 36; // per part, changed by callback weight: 144 ton; // Total, changed by callback ai_engine_rank: 0; // not intended to be used by the ai engine_class: ENGINE_CLASS_ELECTRIC; extra_power_per_wagon: 0 kW; // 4/12 of weight on driving wheels, with a default friction coefficient of 0.3: tractive_effort_coefficient: 0.3 / 3; // changed by callback air_drag_coefficient: 0.06; shorten_vehicle: SHORTEN_TO_8_8; // Overridden by callback to disable for non-powered wagons: visual_effect_and_powered: visual_effect_and_powered(VISUAL_EFFECT_ELECTRIC, 2, DISABLE_WAGON_POWER); extra_weight_per_wagon: 0 ton; bitmask_vehicle_info: 0; } /* Define graphics and callbacks * Setting all callbacks is not needed, only define what is used */ graphics { default: sw_icm_graphics; purchase: set_icm_purchase; start_stop: sw_icm_start_stop; articulated_part: sw_icm_articulated_part; can_attach_wagon: sw_icm_can_attach_wagon; } }
Language file so far
english.lng now contains this:
##grflangid 0x01 STR_GRF_NAME :NML Example NewGRF: Train STR_GRF_DESC :{ORANGE}NML Example NewGRF: Train{}{BLACK}This NewGRF is intended to provide a coding example for the high-level NewGRF-coding language NML.{}Original graphics by {SILVER}Purno, {BLACK}coding by {SILVER}DJNekkid.{}{BLACK}This NewGRF defines a Dutch EMU, the ICM 'Koploper'. STR_ICM_NAME :ICM 'Koploper' (Electric) STR_ICM_CANNOT_START :... train too long (max. 4 coupled EMUs). STR_ICM_CANNOT_ATTACH_OTHER :... only other ICMs can be attached to ICM.
This is now a working 3-part EMU. In real life, the EMU is also available as a 4-part variant. In the next part of the tutorial, we will add the option to choose between a 3- and 4 part EMU via a refit.
NML Tutorial: Train three part articulated