NMLTutorial/Train recolour

From TTWiki
Jump to: navigation, search

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 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 the COLOUR_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 the company_colour1 vehicle variable.
real life colours (param_colour_scheme == 2)
The real life colours of this Dutch NS train are blue and yellow. We can force the train to be recoloured in these colours regardless of the company colours selected by the user. This is similar to the 1cc recolour from above, but now we return two specific colours rather than depend on the company colour variable.
2cc (default)
This is what will be used as default if the GRF parameter setting is not equal to 0 or 2. Here a different way of returning the recolour sprite is used. The sprite number of the first 2cc recolour sprite can be found with the base_sprite_2cc variable. Add CB_RESULT_COLOUR_MAPPING_ADD_CC to this (see the callback description) to find the company colours used.

With that the GRF parameter setting to choose the vehicle colours is done.


Visual effect

The train currently has a visual effect (electric spark) on all parts of the vehicle, but only the first part has a pantograph. We can limit the effect to only this first part by using the visual_effect_and_powered callback. Making a decision only on the position_in_consist variable we can do this using a conditional assignment directly from the graphics block without the need for a switch block:

Add to the graphics block:

        visual_effect_and_powered:    return (position_in_consist % 4 == 0) ?
                                          visual_effect_and_powered(VISUAL_EFFECT_ELECTRIC, 2, DISABLE_WAGON_POWER) :
                                          visual_effect_and_powered(VISUAL_EFFECT_DISABLE, 0, DISABLE_WAGON_POWER);

For the first part of the vehicle we return the electric spark visual effect with an offset of 2 and not making this a powered wagon. For all other parts we disable the visual effect (offset doesn't matter now) and also not make this a powered wagon.


Lights out

There's one more thing that can be improved about this train. We still have not used the sprites that show no lights at the front and rear of the train. It would be nice if two EMUs are coupled that there are no lights on where the coupling is, only at the front and back.

For this we have to change the graphics switch block, not referencing the spritesets for back and front directly, but using an intermediate switch block to decide if we want lights or not.

Change the current switch block:

/* Choose between front, middle and back parts */
switch(FEAT_TRAINS, SELF, sw_icm_graphics, position_in_consist % 4) {
    0:      sw_icm_graphics_front;
    3:      sw_icm_graphics_rear;
    sw_icm_graphics_middle;
}

This now links to two additional switch blocks, one for the front and one for the rear:

/* Only the frontmost vehicle should have its lights on */
switch(FEAT_TRAINS, SELF, sw_icm_graphics_front, position_in_consist) {
    0: set_icm_front_lighted;
    set_icm_front;
}

/* Only the rearmost vehicle should have red lights */
switch(FEAT_TRAINS, SELF, sw_icm_graphics_rear, position_in_consist_from_end) {
    0: set_icm_rear_lighted;
    set_icm_rear;
}

In case of the front part, we make a decision based on the position_in_consist variable. If it's the very first of the consist, show front lights, otherwise show no front lights for further connected EMUs.

In case of the last part, we make a decision based on the position_in_consist_from_end variable. This counts exactly opposite than position_in_consist, now starting from the back. If it's the very last of the consist, show rear lights, otherwise show no rear lights for further connected EMUs.


Total code

When put in the correct order, this should now encode as a working NewGRF. The total code:

/* 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 user-configurable parameters */
    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);
            };
        }
    }
}

/* 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  --- */

/* Only the frontmost vehicle should have its lights on */
switch(FEAT_TRAINS, SELF, sw_icm_graphics_front, position_in_consist) {
    0: set_icm_front_lighted;
    set_icm_front;
}

/* Only the rearmost vehicle should have red lights */
switch(FEAT_TRAINS, SELF, sw_icm_graphics_rear, position_in_consist_from_end) {
    0: set_icm_rear_lighted;
    set_icm_rear;
}

/* 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 % 4) {
    0:      sw_icm_graphics_front;
    3:      sw_icm_graphics_rear;
    sw_icm_graphics_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 CB_RESULT_NO_TEXT;
}

/* --- Colour mapping callback  --- */
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;
}

/* --- 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 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-16 cars) */
    1 .. 16: 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);
}

/* --- 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 7;
    2: return 1;
    return 8;
}

switch(FEAT_TRAINS, SELF, sw_icm_shorten_vehicle, cargo_subtype) {
    0: sw_icm_shorten_3_part_vehicle;
    return 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, 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;
        cargo_subtype_text:           sw_icm_cargo_subtype_text;
        additional_text:              return string(STR_ICM_ADDITIONAL_TEXT);
        colour_mapping:               sw_icm_colour_mapping;
        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 */
        visual_effect_and_powered:    return (position_in_consist % 4 == 0) ?
                                          visual_effect_and_powered(VISUAL_EFFECT_ELECTRIC, 2, DISABLE_WAGON_POWER) :
                                          visual_effect_and_powered(VISUAL_EFFECT_DISABLE, 0, DISABLE_WAGON_POWER);
        length:                       sw_icm_shorten_vehicle;
        /* Only the front vehicle has power */
        power:                        sw_icm_power;
        //purchase_power:               return int(1260 / 0.7457); // kW -> hp
        /* 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_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

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.


Now our train is really completed and that makes this the end of the train example.


NML Tutorial: Train recolour