NMLTutorial/Train four part refit
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 Hirundu. 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 third part of the train example. The three part EMU will be made refittable to a four part EMU.
What's going to happen
A lot of things need to be added to make this train refittable between three and four parts.
The method used to do this is actually the other way round than the user will think from the behaviour ingame. The train will be changed to a four part EMU. For the (default) three part refit one of these four parts will be hidden. A lot of switches will be used to make the train look right, make it have the correct capacity, power, weight, running costs and tractive effort.
There are some drawbacks to this method. There's no way to charge the user for puchasing the extra vehicle part. Also autoreplacing will be difficult, as there's now way for the user to select which vehicle length they want. For that reason the author of this tutorial thinks making two different vehicles and having both available from the purchase menu is a better solution, but this method is a good illustration for some of the more advanced features of NML. It's up to you to decide which implementation you think is best for your vehicles.
What we'll be doing:
- Add the refit option;
- Make the graphics work;
- Add callbacks to supply the correct vehicle properties;
- Make the purchase menu display the correct values.
Refit option
The refit option will be added by means of the "cargo subtype". This allows to split each cargo into multiple entities which can be assigned different properties by means of other callbacks.
The first step towards this is defining names for these separate entries using the cargo_subtype_text
callback. This callback requires the use of a switch, so reference a switch from the graphics block:
cargo_subtype_text: sw_icm_cargo_subtype_text;
The switch itself will use the cargo_subtype
variable. This variable starts at 0 and will be increased by 1 until the callback returns 0xFF
. Return strings to use as cargo subtype. Each returned string will be a separate cargo subtype:
switch(FEAT_TRAINS, SELF, sw_icm_cargo_subtype_text, cargo_subtype) { 0: return string(STR_ICM_SUBTYPE_3_PART); 1: return string(STR_ICM_SUBTYPE_4_PART); return 0xFF; }
This adds two cargo subtypes. Also add these strings to the language file:
STR_ICM_SUBTYPE_3_PART : (3 parts) STR_ICM_SUBTYPE_4_PART : (4 parts)
Graphics
The graphics defined previously were for a three part vehicle. Now we have to make this into a four part vehicle and hide one wagon for the three part refit.
Four part articulated vehicle
In order to make the vehicle four parts, the articulated vehicle callback switch needs to be changed:
switch(FEAT_TRAINS, SELF, sw_icm_articulated_part, extra_callback_info1) { /* Add three articulated parts, for a total of four */ 1 .. 3: return icm; return 0xFF; }
This is done by changing the value range from 1 .. 2
into 1 .. 3
.
Start/stop callback
The start/stop callback checking the vehicle length needs to be changed as well. If we still want a maximum of four EMUs, the maximum length will now be 4 * 4 instead of 4 * 3. This means changing the switch for this callback:
switch(FEAT_TRAINS, SELF, sw_icm_start_stop, num_vehs_in_consist) { /* Vehicles may be coupled to a maximum of 4 units (12-16 cars) */ 1 .. 16: return 0xFF; return string(STR_ICM_CANNOT_START); }
This is done by changing the value range from 1 .. 12
into 1 .. 16
.
Graphics
Now that the vehicle is four parts, the default graphics switch needs to be changed as well to allow for two middle parts instead of one:
switch(FEAT_TRAINS, SELF, sw_icm_graphics, position_in_consist % 4) { 0: set_icm_front_lighted; 3: set_icm_rear_lighted; sw_icm_graphics_middle; }
Instead of modulo 3 we're now taking modulo 4. And the value for the rear vehicle part was changed from 2 to 3. For the middle parts, we also need to hide the one of the wagons for the three part EMU. Therefore we can't directly reference the spriteset but need an extra intermediate switch block.
The extra switch block:
switch(FEAT_TRAINS, SELF, sw_icm_graphics_middle, ((position_in_consist % 4) == 2) && (cargo_subtype == 0)) { 1: set_icm_invisible; set_icm_middle; }
Even an expression this advanced can be used for switch block decision. Here we again check the position of the vehicle part, but also which cargo subtype is used for this vehicle. If it is the three part EMU (cargo subtype 0) AND it is the third vehicle part (position 2 counting from 0), we hide this vehicle part by displaying no graphics for it. In all other cases we display the regular middle part.
Vehicle length
If you were to encode the result so far as a NewGRF, you end up with a three part train that has a big gap between the second and last part. This is because it's essentially still a four part vehicle, just with no graphics for the third part. This can be solved by changing the length of the two middle parts. If we make the second part (7/8) long and the third part (1/8), both together are a full wagon length, completely hiding the invisible third part.
This is done by means of the shorten_vehicle
callback, referencing a switch block as we first need to differentiate between the two cargo subtypes and then by the position in consist. Reference the switch block from the graphics block:
shorten_vehicle: sw_icm_shorten_vehicle;
Then we get the two switch blocks:
/* --- Shorten vehicle callback --- */ switch(FEAT_TRAINS, SELF, sw_icm_shorten_3_part_vehicle, position_in_consist % 4) { /* In the three part version, shorten the 2nd vehicle to 7/8 and the 3rd to 1/8 * The rear (1/8) part is then made invisisble */ 1: return SHORTEN_TO_7_8; 2: return SHORTEN_TO_1_8; return SHORTEN_TO_8_8; } switch(FEAT_TRAINS, SELF, sw_icm_shorten_vehicle, cargo_subtype) { 0: sw_icm_shorten_3_part_vehicle; return SHORTEN_TO_8_8; // 4-part vehicle needs no shortening }
The second of these switch blocks (called first from the graphics block) differentiates between the two cargo subtypes. For subtype 0 (three part vehicle) we need to do the shortenings and reference the first switch block. For the four part vehicle we need no shortening, so directly return to "shorten" to full length.
The first switch again makes a decision based on the position_in_consist
variable we've seen several times now. As said, the second part will be shortened to (7/8), the third part to (1/8) and the front and back part are kept full length (8/8).
Now the vehicle looks good in both refits.
Vehicle properties
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: sw_icm_graphics_front; 2: sw_icm_graphics_rear; sw_icm_graphics_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 icm; return 0xFF; } /* --- 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 0xFF; 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 */ icm: return CB_RESULT_ATTACH_ALLOW; return string(STR_ICM_CANNOT_ATTACH_OTHER); } /* Define the actual train */ item(FEAT_TRAINS, 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 cargoes need explicit enabling/disabling: refittable_cargo_types: bitmask(); // 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 train engine, but not an EMU. On the next page we'll expand the code we have so far to make this train into a proper EMU which can be purchased as prebuilt consist without the need to add wagons yourself.
NML Tutorial: Train four part refit