This is the page of YETI NML code documentation. Here I will describe a step by step guide how to get a single industry coded. After that I am going to mention how do I add animation to save file size, and eventually some things which are special and not typically used.
This guide should be useful to anybody starting with coding an industry set, or just anyone who wants to orientate in YETI code.

Note: All of the things are properly described on so if you want a complete list of features and things, there you can find it. But if you are being overwhelmed and have no idea where to start, this page might help you.

Note2: This guide uses examples from the yeti code found at the repository.

Basic Project Structure

First off I will describe what is what.
We are trying to code a 4x4 tile industry called 1A, we want it to produce only if workers arrive but we will also try to create an industry which produces all the time.

Starting the newGRF

Before we get into actually making an industry, we need to create the grf header that every newGRF needs to have:
grf {
    grfid: "VB\03\03";
    name: string(STR_GRF_NAME);
    desc: string(STR_GRF_DESC);
    url  : string(STR_GRF_WEBSITE);
    version: 6;
    min_compatible_version: 1;
  • grfid is usually picked by your initials, then you can use basically any number - just try to check if it does not collide with other newGRFs
  • the strings are in english.lng - a file you need to create in \lang\english.lng
  • string definition looks like this STR_GRF_NAME :YETI Extended Towns & Industries
    What each of the strings does should be self-explanatory.

Removing default things

A good next step is to remove all default industries and cargoes so we have our workspace clean. Industries are removed as a whole, cargoes are with 0,29 parameter to remove all cargoes in the range of IDs 0 to 29. 30 is regearing so if you do not want to break NARS2, leave 30 as it is. I add 31 in case that was there.


disable_ item(FEAT_CARGOS, 0, 29);
disable_item(FEAT_CARGOS, 31, 31);

NoteForSylf: the cargoes should leave pax/mail I guess ... so needs more excluding but I do not know which one is pax and which one is mail atm

Defining Cargoes

In order to have the cargoes actually work with the newGRF, we need to setup the labels through cargotable - even if your newGRF does not define those cargoes. For example a train set needs to have a cargotable too.


cargotable {
GRVL,//1  c1A stone
CLAY,//17  c1B clay
WOOD,//3  c1C wood
BDMT,//4  c1X building materials

LVST,//18  c2A livestock
GRAI,//6  c2B grain
FRUT,//7  c2C fruit and vegetables
FOOD,//8  c2X food

IORE,//20  c3A iron ore
OIL_,//10 c3B oil
URAN,//19c3C uranium

STEL,//12 c3AA steel
RFPR,//13 cargo_3BB refined products
BATT,//14 c3CC batteries

VEHI,//15 c3X machinery
YETI //16 c4X workers

Full cargotable for YETI, with comments to make it clear what is what, and cargo IDs.

NoteForSylf: 2 is probably mail which is why Clay is 17 for now. 5 is probably goods I think - livestock 18. IORE 20 because of something was 9. All this was done before I had the disable_item hence this mess. Needs cleanup


It is time to tell the game to create our first cargo which we could use, so here we go:

// =============   c1A GRVL   ==============
item(FEAT_CARGOS, c1A, 1){
    number: 1;
    cargo_classes: bitmask(CC_BULK);
    cargo_label: "GRVL";    
    type_name: string(STR_c1A__type_name);
    unit_name: string(STR_c1A__unit_name);
    units_of_cargo: string(STR_c1A__units_of_cargo);
    items_of_cargo: string(STR_c1A__items_of_cargo);
    type_abbreviation: string(STR_c1A__abbreviation);
    //sprite: ;
    weight: 1;
    penalty_lowerbound: 200;
    single_penalty_length: 200;
    price_factor: 100.0;
    station_list_colour: 0x51;
    cargo_payment_list_colour: 0x51;
    is_freight: 1;
    capacity_multiplier: 1.0;

  • the strings are just various texts in various places
  • sprite is the icon of the cargo. Not defining it does not break anything, but can cause your stone/gravel look like for example livestock in the icons.
  • penalties and price factor are all related to the payment you get for transporting the cargo

Defining Spriteset

Every thing in the game has to look somehow, for that is used the 8bpp x1 sprite:

spriteset (sprite_industry_tile00_under){

This is a spriteset definition, defining spriteset sprite_industry_tile00_under - with a template template_industrytile_x1, and giving the template parameters 0,0,0,0, and filename.
Obviously, you need to create the template first, so here we go:

template template_industrytile_x1(u, z) { //start of template template_industrytile_x1 with parameters u and z (you can pick any letters you like)
//  u == MOVEMENT on X in grid
//  v == START on X (should be almost always 0)
//  x == MOVEMENT on Y in grid
//  y == START on Y
//            0     for TOP
//            384 for MAIN
//  z == file name, e.g. "gfx/industry_3BB/industry_3BB_f0000.png" 

[v+32*u, //position of top left corner in X
y+16*x, //position of top left corner in Y
 64, //size of sprite in X
32+96-y, //size of sprite in Y
-32, //sprite X offset
-96+y, //sprite Y offset
z] //and the filename

} //end of template

The Grid I refer to here is XY grid of tiles to be able to cut from one image. I do not do it this way anymore, but here goes a template just for an example.
In my case of YETI, I am defining mainly 4X zoom sprites, paying less attention to the 8bpp 1x because almost always only the 32bpp x4 sprite shows.
template template_industrytile_x4_redone(x, z) {
//  x == MOVEMENT on X
//  z == file name, e.g. "gfx/industry_3BB/industry_3BB_f0000.png" 
    [x*256, 0,  256, 704, -128, -578, z]

In reality I am using this template for x4 (all of the stuff that is visible). It uses the image above, where X parameter is simply the number of the sprite, and z filename again.

Note: defining only x4 in 32bpp can cause some 1px jumping of sprites when they are automatically downscaled to x1. Fixing this is easy - you just have to define also proper 32bpp x1 - so in total you would define 8bpp x1, 32bpp x1, and 32bpp x4.

And here goes a simple x4 definition:

alternative_sprites (sprite_industry_tile00_under, ZOOM_LEVEL_IN_4X, BIT_DEPTH_32BPP) {
template_industrytile_x4_redone(0,"gfx/industry_1A/industry_1A_UNDERLAY.png")//1A 0}

As you might note, it is not a spriteset sprite_industry_tile00_under, but an alternative_sprite. That basically means it is not Technically Necessary (tm) for the newGRF to work. What it causes is that you need to tell the alternative_sprite which of the sprite are you "alternating", zoom level, bit depth, and after that the template with its parameters.


The previous example is correct, but YETI uses one extra trick in order to be easily able to load an animation.

Normally, you would load the spriteset just by writing "sprite_industry_tile00_under" at the place it should be loaded. For animation it would need to have 128 frames though!
That is why the template is a bit different - because it is able to do this:

alternative_sprites (sprite_industry_3X_tile00_anim, ZOOM_LEVEL_IN_4X, BIT_DEPTH_32BPP) {
and up to f0127

This is extremely useful because then the animation loading is as simple as this:


Note: the number in () is simply the number of the sprite loaded in the long alternative_sprites list. I do not have to do this, but I also use this method for defining the _under sprites - and then I just pick one of them. For example


because I know 14th position is for 3X from here:
alternative_sprites (sprite_industry_tile00_under, ZOOM_LEVEL_IN_4X, BIT_DEPTH_32BPP) {
template_industrytile_x4_redone(0,"gfx/industry_1A/industry_1A_UNDERLAY.png")//1A 0
template_industrytile_x4_redone(0,"gfx/industry_1B/industry_1B_UNDERLAY.png")//1B 1
template_industrytile_x4_redone(0,"gfx/industry_1C/industry_1C_UNDERLAY.png")//1C 2
template_industrytile_x4_redone(0,"gfx/industry_1X/industry_1X_UNDERLAY.png")//1X 3
template_industrytile_x4_redone(0,"gfx/industry_2A/industry_2A_UNDERLAY.png")//2A 4
template_industrytile_x4_redone(0,"gfx/industry_2B/industry_2B_UNDERLAY.png")//2B 5
template_industrytile_x4_redone(0,"gfx/industry_2C/industry_2C_UNDERLAY.png")//2C 6
template_industrytile_x4_redone(0,"gfx/industry_2X/industry_2X_UNDERLAY.png")//2X 7
template_industrytile_x4_redone(0,"gfx/industry_3A/industry_3A_UNDERLAY.png")//3A 8
template_industrytile_x4_redone(0,"gfx/industry_3B/industry_3B_UNDERLAY.png")//3B 9
template_industrytile_x4_redone(0,"gfx/industry_3C/industry_3C_UNDERLAY.png")//3C 10
template_industrytile_x4_redone(0,"gfx/industry_3AA/industry_3AA_UNDERLAY.png")//3AA 11
template_industrytile_x4_redone(0,"gfx/industry_3BB/industry_3BB_UNDERLAY.png")//3BB 12
template_industrytile_x4_redone(0,"gfx/industry_3CC/industry_3CC_UNDERLAY.png")//3CC 13
template_industrytile_x4_redone(0,"gfx/industry_3X/industry_3X_UNDERLAY.png")//3X 14
template_industrytile_x4_redone(0,"gfx/industry_4X/industry_4X_UNDERLAY.png")//4X 15
... and additional [] to fill the 128 sprite count. This is because when things animate, they for some reason need to load as 128 frame things even if they are static (the underlay sprites)

Defining a spritelayout

spritelayout industry_3X_33_spritelayout {
    ground { sprite: GROUNDSPRITE_NORMAL; }
    building {
    sprite: sprite_industry_tile33_under(14);
    xoffset: 0;
    yoffset: 0;
    //xextent: 64;
    //yextent: 32;
    //zextent: 0;
    recolour_mode: RECOLOUR_NONE;
    childsprite {
         sprite: sprite_industry_3X_tile33_anim(animation_frame);

The spritelayout is a place which merges together all the sprites for the tile we need: # A ground sprite - you Could redefine the ground sprite but it could cause more issues than gain # Building sprite - that is our static underlay sprite # Childsprite - that is our animation atop of the "building" underlay sprite

xoffset and yoffset are simply offsets to position your building on the ground - in our case we want that to be 0 to simply have the tile sit on the ground without any movement.
the extents - xextent, yextent and zextent are some magical numbers which define the bounding boxes. For now I have very limited clue on how they work so I leave them out - so default values are used.
I do not use recolouring but lets keep that to none for simplicity now.
And the childsprite is just loading the animation.

Defining an industry tile

item(FEAT_INDUSTRYTILES, industry_3X_tile_33_){
    property {
    substitute: 0;
    land_shape_flags: bitmask(LSF_ONLY_ON_FLAT_LAND);
    animation_info: [ANIMATION_LOOPING, 128];
    animation_speed: 1;
    accepted_cargos: [[STEL, 8], [RFPR, 8], [BATT, 8]];
    graphics {
    default: industry_3X_33_spritelayout;
    anim_control: CB_RESULT_START_ANIMATION;
    anim_next_frame: CB_RESULT_NEXT_FRAME;

Industry tile is a wtf entity that controls how the tile behaves
  • on what kind of tile it can be built - sloped, water, ...
  • adds triggers for the animation - when it starts and stops - this is currently not working properly in my code
  • information about what the animation actually does - is looping and has 128 frames in my case
  • information how fast the animation plays
  • what cargoes the tile accepts - the 8 means that you just need one tile in the station catchment to accept that cargo. If there was for example 2, you would need 4 tiles in the catchment to make a total of 8.
  • how does it look through callback (the graphics{} block)

Defining tile layout

When we already have a tile, we can combine all the tiles we have into one big layout which says how the industry comes together.
This is what for example defines various shapes of a coal mine in the original game. For YETI there simply is one layout per industry since the image needs to come together correctly.

tilelayout industry_3X_tilelayout {
 0,0: industry_3X_tile_00_;
 0,1: industry_3X_tile_01_;
 0,2: industry_3X_tile_02_;
 0,3: industry_3X_tile_03_;
 1,0: industry_3X_tile_10_;
 1,1: industry_3X_tile_11_;
 1,2: industry_3X_tile_12_;
 1,3: industry_3X_tile_13_;
 2,0: industry_3X_tile_20_;
 2,1: industry_3X_tile_21_;
 2,2: industry_3X_tile_22_;
 2,3: industry_3X_tile_23_;
 3,0: industry_3X_tile_30_;
 3,1: industry_3X_tile_31_;
 3,2: industry_3X_tile_32_;
 3,3: industry_3X_tile_33_;

A tilelayout is as simple as this, the 0,0 0,1 etc are coordinates of the tiles. Those can be seen here:

Defining industry

After all the hard work, it is time to define this tiny little thing:
    property {
    substitute: 0;
    layouts: [industry_3X_tilelayout];
    name: string(STR_industry_3X_NAME);

    accept_cargo_types: [STEL,RFPR,BATT];
    prod_cargo_types: [VEHI];
        prod_multiplier: [0,0];
    min_cargo_distr: 1;
    prob_random: 4;
    prob_in_game: 8;
    map_colour: 0x34;
    spec_flags: 0;
        prospect_chance: 1;
    graphics {
        produce_256_ticks: produce_bonus_3;

Most of these things should be quite clear what it does, but there are some notable parts:
  • life_type is some utter wtf and it can influence the production somehow - or at least could without defining the produce_256_ticks, maybe even with it. For YETI all industries process something so there it is clear what to choose anyway.
  • prob_random is the likelihood of spawning in the beginning of the game
  • prob_in_game is the likelihood of spawning during the game
  • map_colour is cute but simple :)
  • produce_256_ticks is where HELL starts. This is the function which says how the industry should be producing, so lets get to it

Defining production mechanism

Because my understanding of this is VERY minor at the moment, I will just paste what currently YETI is using. Frosch123 helped me with this (thanks (: )

A simple one I had first was this:

produce(produce_date, 0, 0, 0, current_year /100, 0);

This just takes the current year, divides it by 100 and that is the amount of cargo it produces per 256 ticks. Simple but not very useful for a "good mechanism". If you just want to get the newGRF to somewhat work, using this could be convenient for you.
max(waiting_cargo_1 / 10, 16), max(waiting_cargo_2 / 10, 16), max(waiting_cargo_3 / 10, 16),
min(waiting_cargo_1, max(waiting_cargo_1 / 10, 16)) + min(waiting_cargo_2, max(waiting_cargo_2 / 10, 16)) + min(waiting_cargo_3, max(waiting_cargo_3 / 10, 16)),

And this is what currently all industries except worker yard use. Do not ask me what exactly it does though :D It takes 10% of the current waiting cargo and processes it. Somehow.
produce(produce_combined_2, max(16, waiting_cargo_1 / 10), max(16, waiting_cargo_2 / 10), 0, 10 + ( ( current_year - 1850 ) /15 ) + 2*(min(16, waiting_cargo_1)) + 2*(min(16, waiting_cargo_2)), 0);

Here goes the worker yard thing, producing based on year AND additionally processing the arrived food/building materials. But without any bonus if both food and BDMT arrives. Nor with any dependency on town size etc. Which I would love to eventually have there.