NMLTutorial/Train recolour
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 and concludes the fourth part of the train example. A parameter setting will be added (with a GUI for OpenTTD) to allow choosing between 1cc, 2cc or real life colours for our train. And we'll do some further visual improvements.
GRF Parameter setting
The GRF parameter setting will allow choosing between 1cc, 2cc or real life colours. This is done by adding a param block to the grf block. In this param block one (named) parameter will be defined:
param { /* There is one parameter, which can be used to alter the colour scheme */ param_colour_scheme { type: int; name: string(STR_PARAM_COLOUR_SCHEME_NAME); desc: string(STR_PARAM_COLOUR_SCHEME_DESC); /* There are currently three possible values: * - 1cc * - 2cc (default) * - real-world */ min_value: 0; max_value: 2; def_value: 1; names: { 0: string(STR_PARAM_COLOUR_SCHEME_1CC); 1: string(STR_PARAM_COLOUR_SCHEME_2CC); 2: string(STR_PARAM_COLOUR_SCHEME_REAL); }; } }
The GRF parameter is named param_colour_scheme
. This identifier needs to be unique and can be used elsewhere in the code to make decisions upon. Because we want three possible values, the GRF parameter needs to be of the integer type. The boolean type wouldn't work, as that only allows two possible values (on and off). The minimum value will be 0, the maximum 2 (resulting in three possible values 0, 1 and 2) and the default (in OpenTTD, TTDPatch always defaults to 0) will be 1. Strings are defined for the GRF parameter name and description. Each GRF parameter value will also get custom strings to display the settings rather than just numbers.
This means we have to add these strings to the language file as well:
STR_PARAM_COLOUR_SCHEME_NAME :Colour scheme STR_PARAM_COLOUR_SCHEME_DESC :Select the type of colour scheme to use STR_PARAM_COLOUR_SCHEME_1CC :One company colour STR_PARAM_COLOUR_SCHEME_2CC :Two company colours STR_PARAM_COLOUR_SCHEME_REAL :Real-world colours
Colour mapping callback
This only added the parameter setting, but it doesn't do anything yet. We'll use the colour_mapping
callback. This callback allows recolouring a sprite using, well errr, recolour sprites. This is a slightly complicated callback, but the first step is easy. Reference a switch block from the graphics block:
colour_mapping: sw_icm_colour_mapping;
The switch block itself we'll just give to you, attempting to explain what it does afterwards. If you don't understand how this switch block works, just copy it or ignore the callback altogether. Try yourself at some more easy NewGRFs first to get the hang of NML and come back to this later.
switch(FEAT_TRAINS, SELF, sw_icm_colour_mapping, param_colour_scheme) { /* Emulate 1cc by making the first colour always yellow, this looks much better (and more realistic) */ 0: return palette_2cc(COLOUR_YELLOW, company_colour1); /* Use realistic colours, i.e. yellow + dark blue */ 2: return palette_2cc(COLOUR_YELLOW, COLOUR_DARK_BLUE); /* Use the default, i.e. 2 company colours */ return base_sprite_2cc + CB_RESULT_COLOUR_MAPPING_ADD_CC; }
The decision of this switch block is made on the param_colour_scheme
. The name of this variable is what we gave to it when defining the param block, so that's how you use a predefined GRF parameter. The values this GRF parameter can have are those defined in the same param block, so these values can be used as range in the switch block. It's useful to consider one value as default here, in case a user manages to make a setting outside the range of the minimum and maximum GRF parameter setting.
The colour_mapping
needs to return the sprite number of a recolour sprite. We'll be using the default recolour sprites for this and NML has some functions and variables to retrieve these sprite numbers for you. You can find more about that in the NML Documentation.
- 1cc (
param_colour_scheme
== 0) - Here we use the
palette_2cc
function even though we only want 1cc. To make the train look better we make one of the two company colours yellow rather than keeping the normal company colour range, using theCOLOUR_YELLOW
from the default colour constants. The other company colour will depend on what first company colour the user has selected, which we'll get from thecompany_colour1
vehicle variable. - real life colours (
param_colour_scheme
== 2) - 2cc (default)
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 { ELRL } /* 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) } /* --- Graphics callback --- */ /* In the 3-part version, the 3rd car is invisible */ switch(FEAT_TRAINS, SELF, sw_icm_graphics_middle, ((position_in_consist % 4) == 2) && (cargo_subtype == 0)) { 1: set_icm_invisible; set_icm_middle; } /* 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; } /* --- Cargo subtype text --- */ 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; } /* --- Articulated part callback --- */ 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; } /* --- 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-16 cars) */ 1 .. 16: 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); } /* --- 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 } /* Power, weight and TE are all applied to the front vehicle only */ switch(FEAT_TRAINS, SELF, sw_icm_power, cargo_subtype) { 0: return int(1260 / 0.7457); // kW -> hp return int(1890 / 0.7457); // kW -> hp } switch(FEAT_TRAINS, SELF, sw_icm_weight, cargo_subtype) { 0: return 144; //ton, 3 part train return 192; //ton, 4 part train } switch(FEAT_TRAINS, SELF, sw_icm_te, cargo_subtype) { /* Base TE coefficient = 0.3 * 3 parts: 4/12 of weight on driving wheels * 4 parts: 6/16 of weight on driving wheels */ 0: return int(0.3 * 255 / 3); return int(0.3 * 255 * 3 / 8); } /* 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; cargo_subtype_text: sw_icm_cargo_subtype_text; additional_text: return string(STR_ICM_ADDITIONAL_TEXT); start_stop: sw_icm_start_stop; articulated_part: sw_icm_articulated_part; can_attach_wagon: sw_icm_can_attach_wagon; running_cost_factor: return (cargo_subtype == 1) ? 150 : 100; /* Capacity is per part */ cargo_capacity: return (cargo_subtype == 0) && ((position_in_consist % 4) == 2) ? 0 : 36; /* In the purchase menu, we want to show the capacity for the three-part version, * i.e. divide the capacity of three cars across four */ purchase_cargo_capacity: return 36 * 3 / 4; /* Only the front vehicle has a visual effect */ shorten_vehicle: sw_icm_shorten_vehicle; /* Only the front vehicle has power */ power: sw_icm_power; /* Only the front vehicle has weight */ weight: sw_icm_weight; /* Only the front vehicle has TE */ tractive_effort_coefficient: sw_icm_te; /* Only 1/3 of the weight is on the driving weels. */ } }
The complete language file
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_ADDITIONAL_TEXT :Choose between 3- and 4-part EMU via refit{}Stated values are for the 3-part variant, the 4-part version has 33% more capacity and 50% more power and running cost. STR_ICM_SUBTYPE_3_PART : (3 parts) STR_ICM_SUBTYPE_4_PART : (4 parts) STR_ICM_CANNOT_START :... train too long (max. 4 coupled EMUs). STR_ICM_CANNOT_ATTACH_OTHER :... only other ICMs can be attached to ICM.
If you like, this train is done. If you like to continue, you can learn about GRF parameters and then we'll add a parameter setting to display this train in either 1cc, 2cc or real life colours.
NML Tutorial: Train recolour