NMLTutorial/Object slopes

From TTWiki
Jump to: navigation, search

The example used here is from the Dutch Road Furniture. The original graphics for this are by FooBar. The code is by FooBar, based on code for the object example from the NML source by planetmaker and Hirundo. 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 second part of the object example. In this last part we'll make the object compatible with sloped terrain as well as snow and desert terrain. For this some recent features of OpenTTD will be used, which makes this last part of the example incompatible with OpenTTD earlier than 1.2.0 (r22723).


Version check

As indicated, the particular code on this page only works on OpenTTD 1.2.0 (r22723) or higher. Therefore, we want to disable this NewGRF on older versions. This is done by comparing said version number with the openttd_version variable in an if statement. To avoid having to write the version number in hex, we can use the version_openttd() function and have NML do that for us. If the current game version is older than said version number generate a fatal error message, otherwise skip the error message and continue with the rest of the NewGRF:

if (version_openttd(1,2,0,22723) > openttd_version) {
	error(FATAL, REQUIRES_OPENTTD, string(STR_OPENTTD_VERSION));
}

For the error message we set that it must be FATAL, which means that an error message is issued and loading the NewGRF is aborted. As message we use the builtin string REQUIRES_OPENTTD (automatically translated by the game). This default string must be supplied with the actual version number, provided via the custom string string(STR_OPENTTD_VERSION).

Due to this custom string reference, we of course also must define it in the language file (it's sufficient to only have this in the default language file):

STR_OPENTTD_VERSION :1.2.0 (r22723)


Sloped ground sprites

So far our object can only show flat ground tiles. Now we could make a spritelayout for each different slope (19) and each different view of the object (4) which would give us 76 spritelayouts. Luckily, recent versions of OpenTTD allow us to use temporary variable storage to be used inside spritelayouts, which means we can write a small piece of NML code that makes the game calculate what ground sprite to use for a given slope.

This calculation is done in a switch block, so we need to reference a different switch block from the graphics block. This new switch block in turn will reference the switch block we already have:

        default:            switch_fingerpost_3_object;

The new switch block will make the calculation based on the tile_slope object variable using the slope_to_sprite_offset() function. This calculates how many sprites (the offset) after the flat ground sprite the sprite for the slope is located at. This is then stored in temporary storage register 0 using the STORE_TEMP() function.

Because we don't actually need to make a decision in this switch (it's just used to store the slope sprite offset), we only have a default value inside this switch referencing the original switch block that selected the proper spritelayout block depending on the object view:

switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_object, STORE_TEMP(slope_to_sprite_offset(tile_slope), 0)) {
	switch_fingerpost_3_view;
}

Now that we have the calculation, we must actually use it in the four spritelayout blocks for the ground sprite. This means that instead of only referencing the flat ground sprite, we must add the calculated slope sprite offset to this to actually get the sprite number for the slope we want:

//south east
spritelayout spritelayout_fingerpost_3_SE {
    ground {
        sprite: GROUNDSPRITE_NORMAL + LOAD_TEMP(0);
    }
    building {
        sprite: spriteset_fingerpost_3(0);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 6; //from NE edge
        yoffset: 12; //from NW edge
        zoffset: 0;
    }
}

Here we take the sprite number of the flat ground sprite (GROUNDSPRITE_NORMAL) and add the calculated offset to this. We get this offset from the temporary storage using the LOAD_TEMP() function. The argument 0 defines what storage register we want to get a value from. Because we stored in 0, we need to load from 0 as well.

The ground sprite for the other spritelayouts is changed in the same way. We will not give you the other spritelayout blocks here, as they need to be changed once more.

Purchase menu

Now we have a small problem with the purchase menu. The tile_slope object variable isn't available there, so we cannot use the calculation for the purchase menu! This can be solved easily by not doing the calculation for the purchase menu and always have a sprite offset of 0 there (always giving the flat ground sprite). So for the purchase menu we just set the temporary storage value to 0.

First reference the purchase callback from the graphics block and link to a switch block where we will set the temporary storage:

        purchase:           switch_fingerpost_3_purchase;

The switch block itself will only set temporary storage register 0 to a value of 0 and then immediately reference the switch block that makes the decision based on the object views:

switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_purchase, STORE_TEMP(0, 0)) {
	switch_fingerpost_3_view;
}

Now the purchase menu works again.


Snow and desert tiles

Now our object works fine on slopes, but still shows a grass tile on snow and desert tiles. Not good! This is because we always use GROUNDSPRITE_NORMAL as a base for the ground sprite to draw, which is the grass tile in all climates.

The solution here is to use the terrain_type object variable to check what terrain we're actually building on. Now you can again have a bunch of spritelayouts for each different terrain type, but also here we can use the temporary storage to save what terrain sprite to use and retrieve this again in the spritelayouts. Let's give that a go, shall we?

  • For this we will use the second storage register (with index 1).
  • The default case is the normal grounds sprite, so store that in the register: STORE_TEMP(GROUNDSPRITE_NORMAL, 1).
  • In case of the tropic climate, we need to choose between grass and desert depending on the terrain_type variable. If grass, use the storage we already had, otherwise change the storage: STORE_TEMP(terrain_type == TILETYPE_DESERT ? GROUNDSPRITE_DESERT : LOAD_TEMP(1), 1). What this does is store in register 1: if the terrain is desert the sprite number of the desert flat ground sprite from GROUNDSPRITE_DESERT and if the terrain is not desert what we already had in register 1.
  • In case of the arctic climate, we need to choose between grass and snow depending on the terrain_type variable. If grass, use the storage we already had, otherwise change the storage: STORE_TEMP(terrain_type == TILETYPE_SNOW  ? GROUNDSPRITE_SNOW  : LOAD_TEMP(1), 1). What this does is store in register 1: if the terrain is snow the sprite number of the snow flat ground sprite from GROUNDSPRITE_SNOW and if the terrain is not snow what we already had in register 1.

These three expressions need to be put somewhere in our NML file. Each expression can go in a separate switch block all linked together, but luckily we may provide an array of expressions in a single switch block. This is done by separating each command by a comma and grouping them together between straight brackets. The decision of the switch block is based on the last expression in the chain (but that is in this case not important as we only have a default return for the switch block).

Change the switch block we made earlier on this page:

switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_object, [
        //tile slope offset in storage register 0
        STORE_TEMP(slope_to_sprite_offset(tile_slope), 0),
        //terrain type in storage register 1
        STORE_TEMP(GROUNDSPRITE_NORMAL, 1),
        STORE_TEMP(terrain_type == TILETYPE_DESERT ? GROUNDSPRITE_DESERT : LOAD_TEMP(1), 1),
        STORE_TEMP(terrain_type == TILETYPE_SNOW   ? GROUNDSPRITE_SNOW   : LOAD_TEMP(1), 1)
        ]) {
    switch_fingerpost_3_view;
}

For this new calculation to work, we must also change the spritelayout blocks. The choice of the flat ground sprite number is now stored in temporary storage register 1, so we must use this instead of the hardcoded GROUNDSPRITE_NORMAL. Just replace GROUNDSPRITE_NORMAL for the ground sprite with LOAD_TEMP(1) in all spritelayout blocks. If you haven't made the first change to all four spritelayout blocks, do that now all in one go:

//south east
spritelayout spritelayout_fingerpost_3_SE {
    ground {
        sprite: LOAD_TEMP(0) + LOAD_TEMP(1);
    }
    building {
        sprite: spriteset_fingerpost_3(0);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 6; //from NE edge
        yoffset: 12; //from NW edge
        zoffset: 0;
    }
}

//south west
spritelayout spritelayout_fingerpost_3_SW {
    ground {
        sprite: LOAD_TEMP(0) + LOAD_TEMP(1);
    }
    building {
        sprite: spriteset_fingerpost_3(1);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 12; //from NE edge
        yoffset: 6; //from NW edge
        zoffset: 0;
    }
}

//north west
spritelayout spritelayout_fingerpost_3_NW {
    ground {
        sprite: LOAD_TEMP(0) + LOAD_TEMP(1);
    }
    building {
        sprite: spriteset_fingerpost_3(2);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 6; //from NE edge
        yoffset: 0; //from NW edge
        zoffset: 0;
    }
}

//north east
spritelayout spritelayout_fingerpost_3_NE {
    ground {
        sprite: LOAD_TEMP(0) + LOAD_TEMP(1);
    }
    building {
        sprite: spriteset_fingerpost_3(3);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 0; //from NE edge
        yoffset: 6; //from NW edge
        zoffset: 0;
    }
}

Purchase menu

Now we have again broken the purchase menu, because the terrain_type object variable is also not available there. This can be solved easily by again not doing the calculation for the purchase menu and always have the grass terrain sprite in the purchase menu. So for the purchase menu we just set the temporary storage register 1 value to GROUNDSPRITE_NORMAL.

This change is similar to the change of the other switch block we just did:

switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_purchase, [
        //use flat gound sprite for purchase menu
        STORE_TEMP(0, 0),
        //use normal terrain for purchase menu
        STORE_TEMP(GROUNDSPRITE_NORMAL, 1),
        ]) {
	switch_fingerpost_3_view;
}

So for the purchase menu we just use the flat sprite of the grass terrain. Now the purchase menu works again.


With this the end of the object example is reached. You can now encode this as a NewGRF.


The complete code

If you put everything in the correct order, this will be the complete NML file:

// define the newgrf
grf {
    grfid:                  "\FB\FB\05\01";
    name:                   string(STR_GRF_NAME);
    desc:                   string(STR_GRF_DESCRIPTION);
    version:                0;
    min_compatible_version: 0;
}

//check OpenTTD version
//parameterized spritelayout is only supported since OpenTTD 1.2.0 r22723
if (version_openttd(1,2,0,22723) > openttd_version) {
	error(FATAL, REQUIRES_OPENTTD, string(STR_OPENTTD_VERSION));
}

//templates
template template_fingerpost(x,y,filename) {
    [x,     y,      20,     32,     -10,    -28,    filename]
    [x+30,  y,      20,     32,     -10,    -28,    filename]
    [x+60,  y,      20,     32,     -10,    -28,    filename]
    [x+90,  y,      20,     32,     -10,    -28,    filename]
}

//spriteset with four directions
spriteset (spriteset_fingerpost_3) {
    template_fingerpost(0,0,"gfx/dutch_fingerpost.png")
}

/* spritelayouts */

//south east
spritelayout spritelayout_fingerpost_3_SE {
    ground {
        sprite: LOAD_TEMP(0) + LOAD_TEMP(1);
    }
    building {
        sprite: spriteset_fingerpost_3(0);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 6; //from NE edge
        yoffset: 12; //from NW edge
        zoffset: 0;
    }
}

//south west
spritelayout spritelayout_fingerpost_3_SW {
    ground {
        sprite: LOAD_TEMP(0) + LOAD_TEMP(1);
    }
    building {
        sprite: spriteset_fingerpost_3(1);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 12; //from NE edge
        yoffset: 6; //from NW edge
        zoffset: 0;
    }
}

//north west
spritelayout spritelayout_fingerpost_3_NW {
    ground {
        sprite: LOAD_TEMP(0) + LOAD_TEMP(1);
    }
    building {
        sprite: spriteset_fingerpost_3(2);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 6; //from NE edge
        yoffset: 0; //from NW edge
        zoffset: 0;
    }
}

//north east
spritelayout spritelayout_fingerpost_3_NE {
    ground {
        sprite: LOAD_TEMP(0) + LOAD_TEMP(1);
    }
    building {
        sprite: spriteset_fingerpost_3(3);
        xextent: 4;
        yextent: 4;
        zextent: 24;
        xoffset: 0; //from NE edge
        yoffset: 6; //from NW edge
        zoffset: 0;
    }
}

//decide spritelayout for each of the 4 views
switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_view, view) {
    1:  spritelayout_fingerpost_3_SW;
    2:  spritelayout_fingerpost_3_NW;
    3:  spritelayout_fingerpost_3_NE;
    spritelayout_fingerpost_3_SE;
}

//calculate ground sprite for object
switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_object, [
        //tile slope offset in storage register 0
        STORE_TEMP(slope_to_sprite_offset(tile_slope), 0),
        //terrain type in storage register 1
        STORE_TEMP(GROUNDSPRITE_NORMAL, 1),
        STORE_TEMP(terrain_type == TILETYPE_DESERT ? GROUNDSPRITE_DESERT : LOAD_TEMP(1), 1),
        STORE_TEMP(terrain_type == TILETYPE_SNOW   ? GROUNDSPRITE_SNOW   : LOAD_TEMP(1), 1)
        ]) {
    switch_fingerpost_3_view;
}

//calculate ground sprite for purchase menu
switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_purchase, [
        //use flat gound sprite for purchase menu
        STORE_TEMP(0, 0),
        //use normal terrain for purchase menu
        STORE_TEMP(GROUNDSPRITE_NORMAL, 1),
        ]) {
    switch_fingerpost_3_view;
}

item (FEAT_OBJECTS, item_fingerpost_3) {
    property {
        class:                  "NLRF";
        classname:              string(STR_NLRF);
        name:                   string(STR_FINGERPOST_3);
        climates_available:     ALL_CLIMATES;
        size:                   [1,1];
        build_cost_multiplier:  2;
        remove_cost_multiplier: 8;
        introduction_date:      date(1961,1,1);
        end_of_life_date:       0xFFFFFFFF;
        object_flags:           bitmask(OBJ_FLAG_REMOVE_IS_INCOME, OBJ_FLAG_NO_FOUNDATIONS, OBJ_FLAG_ALLOW_BRIDGE);
        height:                 2;
        num_views:              4;
    }
    graphics {
        default:            switch_fingerpost_3_object;
        purchase:           switch_fingerpost_3_purchase;
        autoslope:          return(CB_RESULT_AUTOSLOPE);
        additional_text:    string(STR_FINGERPOST_3_PURCHASE);
    }
}

The language file will now contain:

##grflangid 0x01

#Main grf title and description
STR_GRF_NAME        :{TITLE}
STR_GRF_DESCRIPTION :Description: {SILVER}Dutch Road Furniture is an eyecandy object NewGRF that features road furniture that can be found alongside Dutch roads. {}(c)2011 FooBar. {}{BLACK}License: {SILVER}GPLv2 or higher.

#error messages
STR_OPENTTD_VERSION :1.2.0 (r22723)

#object classes
STR_NLRF            :Dutch Road Furniture

#object name and description
STR_FINGERPOST_3            :Dutch Fingerpost three-way
STR_FINGERPOST_3_PURCHASE   :The three-way fingerpost is centered at one side of the tile and facing outward. Intended to be placed directly opposite of the secondary road at a three-way junction.

The next part of the tutorial will teach you some things about adding 32 bit sprites to your NewGRFs.


NML Tutorial: Object slopes