NMLTutorial/Train four part refit

From TTWiki
Jump to navigationJump to 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 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 is a way to charge the player for the extra vehicle part, but it has some issues, and we shall not bother with it here (if you want more details, feel free to check out the source code of the Fake Subways GRF). Also autoreplacing will be difficult, as there's no 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 CB_RESULT_NO_TEXT. 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 CB_RESULT_NO_TEXT;
}

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 item_icm;
    return CB_RESULT_NO_MORE_ARTICULATED_PARTS;
}

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 CB_RESULT_NO_TEXT;
    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 length 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:

        length:              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 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
}

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

Both refits still have identical properties. Surely the four part EMU has a higher capacity. We'll also change the running costs, power, weight and tractive effort depending on the refit chosen. This is all done by callbacks. You can find the available vehicle callbacks here, with the second table especially on callbacks that change certain properties.

Running cost factor

You can easily imagine that the longer vehicle will be more expensive to run. The three part has a running cost factor of 100, the four part will get a factor of 150. This requires adding the running_cost_factor callback to the graphics block. From there we can link to a switch block and base the decision on the cargo_subtype variable. A different method as shown below is not to use a switch block but to make the decision directly from the graphics block using a conditional assignment. We've used a conditional assignment before in one of the template blocks.

Add to the graphics block:

        running_cost_factor:          return (cargo_subtype == 1) ? 150 : 100;

In case the cargo_subtype is 1 (the four part vehicle), a running cost factor of 150 is used. Otherwise, a factor of 100 is used. If you'd rather used a switch block then that's up to you. Just reference one from the graphics block instead of the conditional assignment and use the switch block to make the decision based on the cargo_subtype variable.

Cargo capacity

The cargo capacity is 36 passengers per unit. Now the three part EMU will have a capacity that is too high, because it technically is a four part EMU with one part hidden. We need to give this hidden part 0 capacity.

This is done by the cargo_capacity callback. If the cargo_subtype is 0 and the position_in_consist is the third part, we give it 0 capacity. Else it will get 36 capacity. Also this can be done directly from the graphics block, using a slightly more advanced conditional assignment. Of course you could again have used a switch block here, but there's no need for that in this case.

Add to the graphics block:

cargo_capacity:               return (cargo_subtype == 0) && ((position_in_consist % 4) == 2) ? 0 : 36;

Power

The power of the four part vehicle will be higher. Let's use a switch block this time. Of course a conditional assignment could be used here, but compare this example with the running cost factor yourself. The callback used here is called power.

Reference the switch block from the graphics block:

        power:                        sw_icm_power;

The switch block itself will again make a decision based on the cargo_subtype variable. The callback must return the vehicle power in horsepower (imperial) and this must be an integer value. Because we know the power in kW, a little calculation is needed which NML can do for you. Returning the integer is done by the int() function that turns a (decimal) number into an integer.

switch(FEAT_TRAINS, SELF, sw_icm_power, cargo_subtype) {
    0: return int(1260 / 0.7457); // kW -> hp
    return int(1890 / 0.7457); // kW -> hp
}

Weight

Of course you could have done the calculation yourself and put the rounded values in the switch block. That we'll do for the weight of the vehicle. The callback used here is called weight.

Again, reference the switch block from the graphics block:

weight:                       sw_icm_weight;

The switch block itself will again make a decision based on the cargo_subtype variable. The weight here must be specified in tons:

switch(FEAT_TRAINS, SELF, sw_icm_weight, cargo_subtype) {
    0: return 144; //ton, 3 part train
    return 192; //ton, 4 part train
}

Tractive effort coefficient

The last property to sort out is the tractive effort. The callback used here is called tractive_effort_coefficient.

Again, reference the switch block from the graphics block:

        tractive_effort_coefficient:  sw_icm_te;

In the switch block we'll actually calculate the tractive effort coefficient:

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);
}

The technical background of the tractive effort coefficient is that it must be supplied as value between 0 and 255, with 255 equal to 100%. The tractive effort coefficient itself is calculated by multiplying the friction coefficient (see Wikipedia for it's meaning) with the percentage of weight that is on driven wheels. I this case we use a friction of 30%. In real life the three part vehicle has 12 axles of which 4 powered. The four part vehicle has 16 axles of which 6 powered. Assuming an equal distribution of weight along the length of the vehicle, the three part vehicle has 33.3% of it's weight on powered wheels. The four part vehicle has 37.5% of it's weight on powered wheels. Multiply both by the friction coefficient and the factor of 255 and you have the tractive effort coefficient.

If you don't provide a tractive effort coefficient, the game will assume a friction coefficient of 0.3 and all axles powered. A tractive effort coefficient of 100% you'll only get with 100% friction and and all axles powered. This is unrealistic for railroads but can be used for maglev when there actually is no contact between vehicle and guideway.


Purchase menu

With all these callbacks we've sort of broken the display of properties in the purchase menu. This is because the position_in_consist variable is not available in the purchase menu and cargo_subtype variable is always 0 in the purchase menu. The latter can be to our advantage if we want to display the properties of the shorter refit, but the other needs some fixing.

Running cost factor

This callback was only based on cargo_subtype, so no fixing needed here.

Cargo capacity

The capacity used both variables, so some fixing is in order here. We want to display 36*3 as capacity. Because capacity is defined per unit and our vehicle is technically four units, we need to divide this over four units: 36*3/4. Add the purchase_cargo_capacity callback to the graphics block and return this value:

        purchase_cargo_capacity:      return 36 * 3 / 4;

Power, weight and tractive effort coefficient

Luckily, these are also only based on the cargo subtype variable, so no fixing needed. If you wanted a different value to be displayed in the purchase menu, you'd use the purchase_power, purchase_weight and purchase_tractive_effort_coefficient callbacks to do this. And if you want to know, the running cost factor would logically use the purchase_running_cost_factor callback.

Additional text

We want to inform the user that this train has an option to refit it into a four part version and that the properties shown are for the three part version. For this we'll use the additional_text callback which we also used in the tram example. It can directly return a string from the graphics block:

        additional_text:              return string(STR_ICM_ADDITIONAL_TEXT);

Also don't forget to add this string to the language file:

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.


This means our vehicle now works like it should with the two refits. Encode it into a NewGRF if you like.


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 % 4) {
    0:      set_icm_front_lighted;
    3:      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 CB_RESULT_NO_TEXT;
}

/* --- 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);
        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;
        length:                       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. */
    }
}

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_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 four part refit