The m4nfo Tutorial

Show full frameset for easy navigation

Using Advanced Sprite Layouts

Introduction

Newer versions of OpenTTD support a so-called 'advanced sprite layout' (ASL), which allows 'dynamic' layout modification by using registers. m4nfo's formal layout specification for this format is kept unchanged, except there are two additional parameters in m4nfo's sprite layout functions ground(), regular(), notransparency(), recolour(), and glass(), namely <flags> and <registers>.

Example: objects adjusting automatically to neighbour objects

To switch between usual sprite layout and advanced sprite layout, m4nfo's internal functions asl_on() and asl_off() are used.

But first of all, the object has to be defined:

defineobject(_TANKS,
	classid(MC14)
	classname(facilities)
	objectname(tanks)
	climate(TEMPERATE, ARCTIC, TROPIC)
	size(1,1)
	price(20)
	flags(NOFOUNDATION)
	timeframe(1.1.1920 .. 1.1.2345)
	numviews(4)
)

This 1-tile object has 4 views, hence four different storage tanks will be provided.

Next, some preparation for advanced sprite layout usage:

asl_on()
define(REGISTER,10)

And now for the sprite block. First 4 sprites are the tank graphics, next 4 sprites are graphics for retaining walls, two in x- and two in y-direction (we can't get away with only one sprite per direction, because we can't adjust them properly by modifying offsets alone).

spriteblock(
// 0 .. 3 tanks
  set(
    sprite(facilities.pcx 10 690 09 38 38 -18 -20)
    sprite(facilities.pcx 50 690 09 38 38 -18 -20)
    sprite(facilities.pcx 90 690 09 38 38 -18 -20)
    sprite(facilities.pcx 130 690 09 38 38 -18 -20)
// 4 .. 7 walls
    sprite(facilities.pcx 170 690 09 18 32 -31 -2)
    sprite(facilities.pcx 204 690 09 18 32 1 -2)
    sprite(facilities.pcx 170 690 09 18 32 -29 -2)
    sprite(facilities.pcx 204 690 09 18 32 -1 -2)
  )
)

Next, we have to set up sprite sets for 16 different wall layouts. E.g., a tank object with no neighbours should have walls on all 4 edges, and a tank with 4 neighbours (in +x, -x, +y, -y) should have no walls at all. So, all combinations of neighbours needed are as depicted:

I.e., the first combination (code "0") has no neighbours (red edges), and hence has walls on all edges (blue), the second combination (code "1") has one neighbour on its south-west edge (+x), etc. pp.

Let's start with the layout for the tank with all walls:

// tank and 4 walls
def(0) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
// top left (NW)
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(4))
// top right (NE)
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(5))
// bottom right (SE)
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(6))
// bottom left (SW)
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(7))
)

Ground tile is the original TTD "grass" tile, and the tank graphics is defined as "regular(0, .. aslflags(OFFSET_SPRITE), registers(REGISTER))", i.e. its sprite number will be evaluated by adding the content of register REGISTER to "0" (the first sprite in the sprite block). That said, register evaluation will result in 0 for the first tank sprite, 1 for the second, 2 for the third, and 3 for the fourth. We will set REGISTER to these values soon.

The next 4 sprites define the 4 retaining walls. To handle them in a simple but flexible way, each sprite gets its own register ("4", "5", "6", and "7") which will be set as required later as well.

Now, next 15 sprite sets can be easily made up from the remaining wall combinations (see pic above). We just have to delete unfitting wall sprite sets. Only to be complete, here is the code for the rest of them:

def(1) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(4))
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(5))
	regular(0, xyz(0,15,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(6))
)

def(2) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(4))
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(5))
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(7))
)

def(3) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(4))
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(5))
)

def(4) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(4))
	regular(0, xyz(0,15,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(6))
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(7))
)

def(5) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(4))
	regular(0, xyz(0,15,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(6))
)

def(6) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(4))
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(7))
)

def(7) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(4))
)

def(8) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(5))
	regular(0, xyz(0,15,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(6))
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(7))
)

def(9) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(5))
	regular(0, xyz(0,15,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(6))
)

def(10) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(5))
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(7))
)

def(11) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(5))
)

def(12) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,15,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(6))
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(7))
)

def(13) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(0,15,0), dxdydz(15,1,3), aslflags(OFFSET_SPRITE),
		registers(6))
)

def(14) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags(OFFSET_SPRITE),
		registers(7))
)

def(15) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE),
		registers(REGISTER))
)

Now comes the main part of the code, finding out which of those 16 sprite sets to use in which combination of neighbour tiles, with the result of a perfect wall around all single tank tiles, forming a combined larger "tank farm" structure.

Fortunately enough, m4nfo has a special function for this, so the user doesn't have to figure it out himself. This function, objinfo_type4(<type>, <block>), examines the 4-neighbourhood of the current tile for the given <type>. It returns a 'nibble' (4 bits, numbers 0 .. 15) with bits set for each occurence of the given tile type. So, here goes:

// draw retaining walls according to tile placement
def(16) objinfo_type4(_TANKS,
	ref(0) if(0) // all walls
	ref(1) if(1)
	ref(2) if(2)
	ref(3) if(3)
	ref(4) if(4)
	ref(5) if(5)
	ref(6) if(6)
	ref(7) if(7)
	ref(8) if(8)
	ref(9) if(9)
	ref(10) if(10)
	ref(11) if(11)
	ref(12) if(12)
	ref(13) if(13)
	ref(14) if(14)
	ref(15) else // no walls
)

As can be seen, the resulting nibble is interpreted as a code number from the diagram above, which refers to the appropriate sprite set to use. Easy as pie! :D

Well. Now comes the time for some register magic. When calling above function we first have to set the correct values to get the proper tank view (0 .. 3):

// tanks 0 .. 3
def(2) setregisters(REGISTER, 0, ref(16))
def(3) setregisters(REGISTER, 1, ref(16))
def(4) setregisters(REGISTER, 2, ref(16))
def(5) setregisters(REGISTER, 3, ref(16))

I.e., when placing tank view 1, we set REGISTER with "1" and chain to 16, etc. Remains selection of "views":

def(6) getviews(
	ref(2) if(0)
	ref(3) if(1)
	ref(4) if(2)
	ref(5) else
)

As above: "view 1" chains to 3, sets REGISTER to "1" and chains to 16.

At last, we need to set the registers for correct placement of retaining walls. Unlike the tank sprites, which have to be set only once per view, we have to set 4 registers for each view now. This is best done by setting 4 registers at once:

// set registers for walls
def(10) setregisters(4, {4,5,6,7}, ref(6))

I.e., 4 registers, starting at register 4, are set to "4", "5", "6", and "7", consecutively and we're chaining to the getviews() function.

In this way, all 4 registers are being set per view. And that's it, we would only have to end with the usual makeobject().

But wait! We should include some nice extra graphics for the menu, showing all 4 views in all their glory.

As we won't need any register magic for this to do, we are switching back to normal sprite layout mode:

// icons
asl_off()

And add an extra sprite block for the icons:

spriteblock(
    set(
	sprite(facilities.pcx 10 735 09 46 64 -31 -21)
    )
    set(
	sprite(facilities.pcx 76 735 09 46 64 -31 -21)
    )
    set(
	sprite(facilities.pcx 10 785 09 46 64 -31 -21)
    )
    set(
	sprite(facilities.pcx 76 785 09 46 64 -31 -21)
    )
)

Adding another getviews() function gets it done for today!

def(1) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,16))
)

def(2) spriteset(
	ground(GRASS)
	regular(1, xyz(3,3,0), dxdydz(10,10,16))
)

def(3) spriteset(
	ground(GRASS)
	regular(2, xyz(3,3,0), dxdydz(10,10,16))
)

def(4) spriteset(
	ground(GRASS)
	regular(3, xyz(3,3,0), dxdydz(10,10,16))
)

def(6) getviews(
	ref(1) if(0)
	ref(2) if(1)
	ref(3) if(2)
	ref(4) else
)

makeobject(_TANKS,
	link(ref(6),MENU)
	default(ref(10))
)



Example 1 revisited

Above example could be further compactified by exploiting the features of Advanced Sprite Layouts. In fact, it is not necessary to explicitly define above 15 sprite sets for wall placement. Instead, one sprite set should suffice, by use of ASL's SKIP flag:

// 4 walls
def(20) spriteset(
	ground(GRASS)
	regular(0, xyz(3,3,0), dxdydz(10,10,24), aslflags(OFFSET_SPRITE), registers(REGISTER))
// top left (NW)
	regular(0, xyz(0,0,0), dxdydz(15,1,3), aslflags({SKIP, OFFSET_SPRITE}), registers({0, 4}))
// top right (NE)
	regular(0, xyz(0,0,0), dxdydz(1,15,3), aslflags({SKIP, OFFSET_SPRITE}), registers({1, 5}))
// bottom right (SE)
	regular(0, xyz(0,15,0), dxdydz(15,1,3), aslflags({SKIP, OFFSET_SPRITE}), registers({2, 6}))
// bottom left (SW)
	regular(0, xyz(15,0,0), dxdydz(1,15,3), aslflags({SKIP, OFFSET_SPRITE}), registers({3, 7}))
)

Since functions aslflags() and registers() now contain more than one parameter, their parameter lists must be quoted, by '{' and '}'. In addition, it is necessary to keep the same order for flags and associated registers, i.e. for the 'top left' sprite, register 0 is associated with flag SKIP, and register 4 is with flag OFFSET_SPRITE.

Now, the only thing to do is to set registers 0 .. 3 either to '0' or to '1', depending on the need to draw a wall for the specified direction. E.g., to draw walls for every four directions, registers 0 .. 3 should contain '1', and registers 4 .. 7 should contain sprite offsets of '4' .. '7' to draw the correct wall sprite (this remains unchanged from example 1):

def(0) setregisters(0, {1,1,1,1}, ref(20)) // 1111 = 15 -> draw all walls
def(1) setregisters(0, {1,1,1,0}, ref(20)) // 1110 = 14
def(2) setregisters(0, {1,1,0,1}, ref(20)) // 13
def(3) setregisters(0, {1,1,0,0}, ref(20)) // 12
def(4) setregisters(0, {1,0,1,1}, ref(20)) // 11
def(5) setregisters(0, {1,0,1,0}, ref(20)) // 10
def(6) setregisters(0, {1,0,0,1}, ref(20)) // 9
def(7) setregisters(0, {1,0,0,0}, ref(20)) // 8
def(8) setregisters(0, {0,1,1,1}, ref(20)) // 7
def(9) setregisters(0, {0,1,1,0}, ref(20)) // 6
def(10) setregisters(0, {0,1,0,1}, ref(20)) // 5
def(11) setregisters(0, {0,1,0,0}, ref(20)) // 4
def(12) setregisters(0, {0,0,1,1}, ref(20)) // 3
def(13) setregisters(0, {0,0,1,0}, ref(20)) // 2
def(14) setregisters(0, {0,0,0,1}, ref(20)) // 0001 = 1
def(15) setregisters(0, {0,0,0,0}, ref(20)) // 0000 = 0 -> draw no walls

As before, refs 0 .. 15 are called according to the result of objinfo_type4():

// draw retaining walls according to tile placement
def(16) objinfo_type4(_TANKS,
	ref(0) if(0) // all walls
	ref(1) if(1)
	ref(2) if(2)
	ref(3) if(3)
	ref(4) if(4)
	ref(5) if(5)
	ref(6) if(6)
	ref(7) if(7)
	ref(8) if(8)
	ref(9) if(9)
	ref(10) if(10)
	ref(11) if(11)
	ref(12) if(12)
	ref(13) if(13)
	ref(14) if(14)
	ref(15) else // no walls
)

Written in this way, the code gets smaller, but might be a bit more harder to understand at first sight.

Example: dynamic adjusting of station tiles

This example demonstrates how station tiles can be easily adjusted due to their position in the overall station layout. Point here is when and on which of its sides a station tile should be fitted with a fence. This can easily be done by use of OTTD's 'advanced sprite layout'.

First of all, the station type has to be defined:

definestation(VOID3,"sidings",
	class(THIRD_TRACK)
	callbacks(CB_LAYOUT)
	include_widths({1,2,3,4})
	exclude_lengths(8)
	threshold(80)
	pylons(TTD_ALLTILES)
	flags({GROUNDSPRITES, DIVAMOUNT, FOUNDATIONS})
)

This defines a station for a switchyard, to show fences around the stations outer boundary. The tile layout is as follows:

asl_on()

layout(VOID3,
// [0,1] straight track
  tile(
    ground(1012)
    regular(0, xyz(0,0,0), dxdydz(16,1,5), aslflags({SKIP,RESOLVE_SPRITE}), registers({5,4}))  // fence left
    regular(2, xyz(0,16,0), dxdydz(16,1,5), aslflags({SKIP,RESOLVE_SPRITE}), registers({6,4})) // fence right
  )
  tile(
    ground(1011)
    regular(1, xyz(0,0,0), dxdydz(1,16,5), aslflags({SKIP,RESOLVE_SPRITE}), registers({5,4}))  // fence left
    regular(3, xyz(16,0,0), dxdydz(1,16,5), aslflags({SKIP,RESOLVE_SPRITE}), registers({6,4})) // fence right
  )
// [2/3] Icon
  tile(
    ground(1012)
    regular(6, xyz(0,0,0), dxdydz(16,16,11))
  )
  tile(
    ground(1011)
    regular(7, xyz(0,0,0), dxdydz(16,16,11))
  )
)

asl_off()

As can be seen, sprites 0 .. 3 all got flags SKIP and RESOLVE_SPRITE attached, i.e. each of them can be skipped when drawing by use of register 5, while the sprite itself is located in a special spriteblock, to be resolved independently by function spritetype() (using value 4).

Now, here's the code for this station. It uses m4nfo's special function plt_edges() to locate individual station tiles within the overall station, and sets registers 5 and 6 to either draw a fence on the tile's left or right side, or both, or no fence at all:

// RESOLVE sprites from different sprite blocks
def(1) spritetype(
	ref(SWITCHYARD) if(4) // switchyard sprites
	ref(GENERAL) else     // general sprites
)

// set registers for SKIP (fence)
// registers 5 = left, 6 = right

// fence left
def(2) setregisters(5,{1,0},ref(1))

// fence right
def(3) setregisters(5,{0,1},ref(1))

// fence both
def(4) setregisters(5,{1,1},ref(1))

// no fence
def(5) setregisters(5,{0,0},ref(1))

def(6) plt_edges(
	ref(2) if(8 .. 9, 12 .. 13)   // fence left
	ref(3) if(2 .. 3, 6 .. 7)     // fence right
	ref(4) if(10 .. 11, 14 .. 15) // fence both
	ref(5) else   // no fence
)

def(7) callback(
	cbr(0) if(CB_LAYOUT)
	ref(6) else
)

def(8) callback(
	cbr(2) if(CB_LAYOUT)
	ref(ICONS) else
)

makestation(VOID3,
	link(ref(8), MENU)
	default(ref(7))
)

Note that we're only checking for fences on the station's left and right edge, but not on its front and back edge, nor on its corners. Although, in the latter case, we're drawing an appropriate part fence. See the meaning of return values of plt_edges().

In addition, we're using three spriteblocks here: one for general sprites (GENERAL, including foundations), one for the special switchyard sprites (SWITCHYARD), and one for the icon sprites (ICONS).