changeset 17738:fe79e4216f58 draft

(svn r22518) -Feature: [NewGRF] Advanced sprite layouts with register modifiers.
author frosch <frosch@openttd.org>
date Sun, 29 May 2011 16:56:22 +0000
parents 73cb6335dba1
children 432d92576ae3
files src/lang/english.txt src/newgrf.cpp src/newgrf_airporttiles.cpp src/newgrf_commons.cpp src/newgrf_commons.h src/newgrf_house.cpp src/newgrf_industrytiles.cpp src/newgrf_object.cpp src/newgrf_spritegroup.cpp src/newgrf_spritegroup.h src/newgrf_station.cpp src/sprite.cpp src/station_cmd.cpp
diffstat 13 files changed, 560 insertions(+), 68 deletions(-) [+]
line wrap: on
line diff
--- a/src/lang/english.txt
+++ b/src/lang/english.txt
@@ -2475,6 +2475,7 @@
 STR_NEWGRF_ERROR_MISSING_SPRITES                                :{WHITE}The currently used base graphics set is missing a number of sprites.{}Please update the base graphics set
 STR_NEWGRF_ERROR_GRM_FAILED                                     :Requested GRF resources not available
 STR_NEWGRF_ERROR_FORCEFULLY_DISABLED                            :{2:RAW_STRING} was disabled by {4:RAW_STRING}
+STR_NEWGRF_ERROR_INVALID_SPRITE_LAYOUT                          :Invalid/unknown sprite layout format
 
 # NewGRF related 'general' warnings
 STR_NEWGRF_POPUP_CAUTION_CAPTION                                :{WHITE}Caution!
--- a/src/newgrf.cpp
+++ b/src/newgrf.cpp
@@ -482,16 +482,19 @@
  * Read a sprite and a palette from the GRF and convert them into a format
  * suitable to OpenTTD.
  * @param buf                 Input stream.
+ * @param read_flags          Whether to read TileLayoutFlags.
  * @param invert_action1_flag Set to true, if palette bit 15 means 'not from action 1'.
  * @param action1_offset      Offset to add to action 1 sprites.
  * @param action1_pitch       Factor to multiply action 1 sprite indices with.
  * @param action1_max         Maximal valid action 1 index.
  * @param [out] grf_sprite    Read sprite and palette.
+ * @return read TileLayoutFlags.
  */
-static void ReadSpriteLayoutSprite(ByteReader *buf, bool invert_action1_flag, uint action1_offset, uint action1_pitch, uint action1_max, PalSpriteID *grf_sprite)
+static TileLayoutFlags ReadSpriteLayoutSprite(ByteReader *buf, bool read_flags, bool invert_action1_flag, uint action1_offset, uint action1_pitch, uint action1_max, PalSpriteID *grf_sprite)
 {
 	grf_sprite->sprite = buf->ReadWord();
 	grf_sprite->pal = buf->ReadWord();
+	TileLayoutFlags flags = read_flags ? (TileLayoutFlags)buf->ReadWord() : TLF_NOTHING;
 
 	MapSpriteMappingRecolour(grf_sprite);
 
@@ -509,7 +512,143 @@
 			SB(grf_sprite->sprite, 0, SPRITE_WIDTH, sprite);
 			SetBit(grf_sprite->sprite, SPRITE_MODIFIER_CUSTOM_SPRITE);
 		}
-	}
+	} else if ((flags & TLF_SPRITE_VAR10) && !(flags & TLF_SPRITE_REG_FLAGS)) {
+		grfmsg(1, "ReadSpriteLayoutSprite: Spritelayout specifies var10 value for non-action-1 sprite");
+		DisableGrf(STR_NEWGRF_ERROR_INVALID_SPRITE_LAYOUT);
+		return flags;
+	}
+
+	if (flags & TLF_CUSTOM_PALETTE) {
+		/* Use palette from Action 1 */
+		uint index = GB(grf_sprite->pal, 0, 14);
+		if (action1_pitch == 0 || index >= action1_max) {
+			grfmsg(1, "ReadSpriteLayoutSprite: Spritelayout uses undefined custom spriteset %d for 'palette'", index);
+			grf_sprite->pal = PAL_NONE;
+		} else {
+			SpriteID sprite = action1_offset + index * action1_pitch;
+			SB(grf_sprite->pal, 0, SPRITE_WIDTH, sprite);
+			SetBit(grf_sprite->pal, SPRITE_MODIFIER_CUSTOM_SPRITE);
+		}
+	} else if ((flags & TLF_PALETTE_VAR10) && !(flags & TLF_PALETTE_REG_FLAGS)) {
+		grfmsg(1, "ReadSpriteLayoutRegisters: Spritelayout specifies var10 value for non-action-1 palette");
+		DisableGrf(STR_NEWGRF_ERROR_INVALID_SPRITE_LAYOUT);
+		return flags;
+	}
+
+	return flags;
+}
+
+/**
+ * Preprocess the TileLayoutFlags and read register modifiers from the GRF.
+ * @param buf        Input stream.
+ * @param flags      TileLayoutFlags to process.
+ * @param is_parent  Whether the sprite is a parentsprite with a bounding box.
+ * @param dts        Sprite layout to insert data into.
+ * @param index      Sprite index to process; 0 for ground sprite.
+ */
+static void ReadSpriteLayoutRegisters(ByteReader *buf, TileLayoutFlags flags, bool is_parent, NewGRFSpriteLayout *dts, uint index)
+{
+	if (!(flags & TLF_DRAWING_FLAGS)) return;
+
+	if (dts->registers == NULL) dts->AllocateRegisters();
+	TileLayoutRegisters &regs = const_cast<TileLayoutRegisters&>(dts->registers[index]);
+	regs.flags = flags & TLF_DRAWING_FLAGS;
+
+	if (flags & TLF_DODRAW)  regs.dodraw  = buf->ReadByte();
+	if (flags & TLF_SPRITE)  regs.sprite  = buf->ReadByte();
+	if (flags & TLF_PALETTE) regs.palette = buf->ReadByte();
+
+	if (is_parent) {
+		if (flags & TLF_BB_XY_OFFSET) {
+			regs.delta.parent[0] = buf->ReadByte();
+			regs.delta.parent[1] = buf->ReadByte();
+		}
+		if (flags & TLF_BB_Z_OFFSET)    regs.delta.parent[2] = buf->ReadByte();
+	} else {
+		if (flags & TLF_CHILD_X_OFFSET) regs.delta.child[0]  = buf->ReadByte();
+		if (flags & TLF_CHILD_Y_OFFSET) regs.delta.child[1]  = buf->ReadByte();
+	}
+
+	if (flags & TLF_SPRITE_VAR10) {
+		regs.sprite_var10 = buf->ReadByte();
+		if (regs.sprite_var10 > TLR_MAX_VAR10) {
+			grfmsg(1, "ReadSpriteLayoutRegisters: Spritelayout specifies var10 (%d) exceeding the maximal allowed value %d", regs.sprite_var10, TLR_MAX_VAR10);
+			DisableGrf(STR_NEWGRF_ERROR_INVALID_SPRITE_LAYOUT);
+			return;
+		}
+	}
+
+	if (flags & TLF_PALETTE_VAR10) {
+		regs.palette_var10 = buf->ReadByte();
+		if (regs.palette_var10 > TLR_MAX_VAR10) {
+			grfmsg(1, "ReadSpriteLayoutRegisters: Spritelayout specifies var10 (%d) exceeding the maximal allowed value %d", regs.palette_var10, TLR_MAX_VAR10);
+			DisableGrf(STR_NEWGRF_ERROR_INVALID_SPRITE_LAYOUT);
+			return;
+		}
+	}
+}
+
+/**
+ * Read a spritelayout from the GRF.
+ * @param buf                  Input
+ * @param num_building_sprites Number of building sprites to read
+ * @param action1_offset       Offset to add to action 1 sprites
+ * @param action1_pitch        Factor to multiply action 1 sprite indices with
+ * @param action1_max          Maximal valid action 1 index
+ * @param allow_var10          Whether the spritelayout may specifiy var10 values for resolving multiple action-1-2-3 chains
+ * @param no_z_position        Whether bounding boxes have no Z offset
+ * @param dts                  Layout container to output into
+ * @return true on error (GRF was disabled)
+ */
+static bool ReadSpriteLayout(ByteReader *buf, uint num_building_sprites, uint action1_offset, uint action1_pitch, uint action1_max, bool allow_var10, bool no_z_position, NewGRFSpriteLayout *dts)
+{
+	bool has_flags = HasBit(num_building_sprites, 6);
+	ClrBit(num_building_sprites, 6);
+	TileLayoutFlags valid_flags = TLF_KNOWN_FLAGS;
+	if (!allow_var10) valid_flags &= ~TLF_VAR10_FLAGS;
+	dts->Allocate(num_building_sprites); // allocate before reading groundsprite flags
+
+	/* Groundsprite */
+	TileLayoutFlags flags = ReadSpriteLayoutSprite(buf, has_flags, false, action1_offset, action1_pitch, action1_max, &dts->ground);
+	if (_skip_sprites < 0) return true;
+
+	if (flags & ~(valid_flags & ~TLF_NON_GROUND_FLAGS)) {
+		grfmsg(1, "ReadSpriteLayout: Spritelayout uses invalid flag 0x%x for ground sprite", flags & ~(valid_flags & ~TLF_NON_GROUND_FLAGS));
+		DisableGrf(STR_NEWGRF_ERROR_INVALID_SPRITE_LAYOUT);
+		return true;
+	}
+
+	ReadSpriteLayoutRegisters(buf, flags, false, dts, 0);
+	if (_skip_sprites < 0) return true;
+
+	for (uint i = 0; i < num_building_sprites; i++) {
+		DrawTileSeqStruct *seq = const_cast<DrawTileSeqStruct*>(&dts->seq[i]);
+
+		flags = ReadSpriteLayoutSprite(buf, has_flags, false, action1_offset, action1_pitch, action1_max, &seq->image);
+		if (_skip_sprites < 0) return true;
+
+		if (flags & ~valid_flags) {
+			grfmsg(1, "ReadSpriteLayout: Spritelayout uses unknown flag 0x%x", flags & ~valid_flags);
+			DisableGrf(STR_NEWGRF_ERROR_INVALID_SPRITE_LAYOUT);
+			return true;
+		}
+
+		seq->delta_x = buf->ReadByte();
+		seq->delta_y = buf->ReadByte();
+
+		if (!no_z_position) seq->delta_z = buf->ReadByte();
+
+		if (seq->IsParentSprite()) {
+			seq->size_x = buf->ReadByte();
+			seq->size_y = buf->ReadByte();
+			seq->size_z = buf->ReadByte();
+		}
+
+		ReadSpriteLayoutRegisters(buf, flags, seq->IsParentSprite(), dts, i + 1);
+		if (_skip_sprites < 0) return true;
+	}
+
+	return false;
 }
 
 /**
@@ -540,6 +679,7 @@
 
 enum ChangeInfoResult {
 	CIR_SUCCESS,    ///< Variable was parsed and read
+	CIR_DISABLED,   ///< GRF was disabled due to error
 	CIR_UNHANDLED,  ///< Variable was parsed but unread
 	CIR_UNKNOWN,    ///< Variable is unknown
 	CIR_INVALID_ID, ///< Attempt to modify an invalid ID
@@ -1269,7 +1409,9 @@
 						continue;
 					}
 
-					ReadSpriteLayoutSprite(buf, false, 0, 1, UINT_MAX, &dts->ground);
+					ReadSpriteLayoutSprite(buf, false, false, 0, 1, UINT_MAX, &dts->ground);
+					/* On error, bail out immediately. Temporary GRF data was already freed */
+					if (_skip_sprites < 0) return CIR_DISABLED;
 
 					static SmallVector<DrawTileSeqStruct, 8> tmp_layout;
 					tmp_layout.Clear();
@@ -1286,7 +1428,9 @@
 						dtss->size_y = buf->ReadByte();
 						dtss->size_z = buf->ReadByte();
 
-						ReadSpriteLayoutSprite(buf, true, 0, 1, UINT_MAX, &dtss->image);
+						ReadSpriteLayoutSprite(buf, false, true, 0, 1, UINT_MAX, &dtss->image);
+						/* On error, bail out immediately. Temporary GRF data was already freed */
+						if (_skip_sprites < 0) return CIR_DISABLED;
 					}
 					dts->Clone(tmp_layout.Begin());
 				}
@@ -1428,6 +1572,19 @@
 				statspec->animation.triggers = buf->ReadWord();
 				break;
 
+			case 0x20: // Advanced sprite layout
+				statspec->tiles = buf->ReadExtendedByte();
+				delete[] statspec->renderdata; // delete earlier loaded stuff
+				statspec->renderdata = new NewGRFSpriteLayout[statspec->tiles];
+
+				for (uint t = 0; t < statspec->tiles; t++) {
+					NewGRFSpriteLayout *dts = &statspec->renderdata[t];
+					uint num_building_sprites = buf->ReadByte();
+					/* On error, bail out immediately. Temporary GRF data was already freed */
+					if (ReadSpriteLayout(buf, num_building_sprites, 0, 1, UINT_MAX, true, false, dts)) return CIR_DISABLED;
+				}
+				break;
+
 			default:
 				ret = CIR_UNKNOWN;
 				break;
@@ -3524,6 +3681,10 @@
 	switch (cir) {
 		default: NOT_REACHED();
 
+		case CIR_DISABLED:
+			/* Error has already been printed; just stop parsing */
+			return true;
+
 		case CIR_SUCCESS:
 			return false;
 
@@ -3961,7 +4122,6 @@
 					byte num_spriteset_ents   = _cur_grffile->spriteset_numents;
 					byte num_spritesets       = _cur_grffile->spriteset_numsets;
 					byte num_building_sprites = max((uint8)1, type);
-					uint i;
 
 					assert(TileLayoutSpriteGroup::CanAllocateItem());
 					TileLayoutSpriteGroup *group = new TileLayoutSpriteGroup();
@@ -3969,26 +4129,8 @@
 					/* num_building_stages should be 1, if we are only using non-custom sprites */
 					group->num_building_stages = max((uint8)1, num_spriteset_ents);
 
-					/* Groundsprite */
-					ReadSpriteLayoutSprite(buf, false, _cur_grffile->spriteset_start, num_spriteset_ents, num_spritesets, &group->dts.ground);
-
-					group->dts.Allocate(num_building_sprites);
-					for (i = 0; i < num_building_sprites; i++) {
-						DrawTileSeqStruct *seq = const_cast<DrawTileSeqStruct*>(&group->dts.seq[i]);
-
-						ReadSpriteLayoutSprite(buf, false, _cur_grffile->spriteset_start, num_spriteset_ents, num_spritesets, &seq->image);
-						seq->delta_x = buf->ReadByte();
-						seq->delta_y = buf->ReadByte();
-
-						if (type > 0) {
-							seq->delta_z = buf->ReadByte();
-							if (!seq->IsParentSprite()) continue;
-						}
-
-						seq->size_x = buf->ReadByte();
-						seq->size_y = buf->ReadByte();
-						seq->size_z = buf->ReadByte();
-					}
+					/* On error, bail out immediately. Temporary GRF data was already freed */
+					if (ReadSpriteLayout(buf, num_building_sprites, _cur_grffile->spriteset_start, num_spriteset_ents, num_spritesets, false, type == 0, &group->dts)) return;
 					break;
 				}
 
--- a/src/newgrf_airporttiles.cpp
+++ b/src/newgrf_airporttiles.cpp
@@ -264,7 +264,7 @@
 
 static void AirportDrawTileLayout(const TileInfo *ti, const TileLayoutSpriteGroup *group, byte colour, StationGfx gfx)
 {
-	const DrawTileSprites *dts = &group->dts;
+	const DrawTileSprites *dts = group->ProcessRegisters(NULL);
 
 	SpriteID image = dts->ground.sprite;
 	SpriteID pal   = dts->ground.pal;
--- a/src/newgrf_commons.cpp
+++ b/src/newgrf_commons.cpp
@@ -23,6 +23,7 @@
 #include "tunnelbridge_map.h"
 #include "newgrf_object.h"
 #include "genworld.h"
+#include "newgrf_spritegroup.h"
 
 /**
  * Constructor of generic class
@@ -435,6 +436,8 @@
 	return tile_type << 24 | z << 16 | terrain_type << 8 | tileh;
 }
 
+/* static */ SmallVector<DrawTileSeqStruct, 8> NewGRFSpriteLayout::result_seq;
+
 /**
  * Clone the building sprites of a spritelayout.
  * @param source The building sprites to copy.
@@ -454,6 +457,26 @@
 }
 
 /**
+ * Clone a spritelayout.
+ * @param source The spritelayout to copy.
+ */
+void NewGRFSpriteLayout::Clone(const NewGRFSpriteLayout *source)
+{
+	this->Clone((const DrawTileSprites*)source);
+
+	if (source->registers != NULL) {
+		size_t count = 1; // 1 for the ground sprite
+		const DrawTileSeqStruct *element;
+		foreach_draw_tile_seq(element, source->seq) count++;
+
+		TileLayoutRegisters *regs = MallocT<TileLayoutRegisters>(count);
+		MemCpyT(regs, source->registers, count);
+		this->registers = regs;
+	}
+}
+
+
+/**
  * Allocate a spritelayout for \a num_sprites building sprites.
  * @param num_sprites Number of building sprites to allocate memory for. (not counting the terminator)
  */
@@ -465,3 +488,150 @@
 	sprites[num_sprites].MakeTerminator();
 	this->seq = sprites;
 }
+
+/**
+ * Allocate memory for register modifiers.
+ */
+void NewGRFSpriteLayout::AllocateRegisters()
+{
+	assert(this->seq != NULL);
+	assert(this->registers == NULL);
+
+	size_t count = 1; // 1 for the ground sprite
+	const DrawTileSeqStruct *element;
+	foreach_draw_tile_seq(element, this->seq) count++;
+
+	this->registers = CallocT<TileLayoutRegisters>(count);
+}
+
+/**
+ * Prepares a sprite layout before resolving action-1-2-3 chains.
+ * Integrates offsets into the layout and determines which chains to resolve.
+ * @note The function uses statically allocated temporary storage, which is reused everytime when calling the function.
+ *       That means, you have to use the sprite layout before calling #PrepareLayout() the next time.
+ * @param orig_offset          Offset to apply to non-action-1 sprites.
+ * @param newgrf_ground_offset Offset to apply to action-1 ground sprites.
+ * @param newgrf_offset        Offset to apply to action-1 non-ground sprites.
+ * @param separate_ground      Whether the ground sprite shall be resolved by a separate action-1-2-3 chain by default.
+ * @return Bitmask of values for variable 10 to resolve action-1-2-3 chains for.
+ */
+uint32 NewGRFSpriteLayout::PrepareLayout(uint32 orig_offset, uint32 newgrf_ground_offset, uint32 newgrf_offset, bool separate_ground) const
+{
+	result_seq.Clear();
+	uint32 var10_values = 0;
+
+	/* Create a copy of the spritelayout, so we can modify some values.
+	 * Also include the groundsprite into the sequence for easier processing. */
+	DrawTileSeqStruct *result = result_seq.Append();
+	result->image = ground;
+	result->delta_x = 0;
+	result->delta_y = 0;
+	result->delta_z = 0x80;
+
+	const DrawTileSeqStruct *dtss;
+	foreach_draw_tile_seq(dtss, this->seq) {
+		*result_seq.Append() = *dtss;
+	}
+	result_seq.Append()->MakeTerminator();
+
+	/* Determine the var10 values the action-1-2-3 chains needs to be resolved for,
+	 * and apply the default sprite offsets (unless disabled). */
+	const TileLayoutRegisters *regs = this->registers;
+	bool ground = true;
+	foreach_draw_tile_seq(result, result_seq.Begin()) {
+		TileLayoutFlags flags = TLF_NOTHING;
+		if (regs != NULL) flags = regs->flags;
+
+		/* Record var10 value for the sprite */
+		if (HasBit(result->image.sprite, SPRITE_MODIFIER_CUSTOM_SPRITE) || (flags & TLF_SPRITE_REG_FLAGS)) {
+			uint8 var10 = (flags & TLF_SPRITE_VAR10) ? regs->sprite_var10 : (ground && separate_ground ? 1 : 0);
+			SetBit(var10_values, var10);
+		}
+
+		/* Add default sprite offset, unless there is a custom one */
+		if (!(flags & TLF_SPRITE)) {
+			if (HasBit(result->image.sprite, SPRITE_MODIFIER_CUSTOM_SPRITE)) {
+				result->image.sprite += ground ? newgrf_ground_offset : newgrf_offset;
+			} else {
+				result->image.sprite += orig_offset;
+			}
+		}
+
+		/* Record var10 value for the palette */
+		if (HasBit(result->image.pal, SPRITE_MODIFIER_CUSTOM_SPRITE) || (flags & TLF_PALETTE_REG_FLAGS)) {
+			uint8 var10 = (flags & TLF_PALETTE_VAR10) ? regs->palette_var10 : (ground && separate_ground ? 1 : 0);
+			SetBit(var10_values, var10);
+		}
+
+		/* Add default palette offset, unless there is a custom one */
+		if (!(flags & TLF_PALETTE)) {
+			if (HasBit(result->image.pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) {
+				result->image.sprite += ground ? newgrf_ground_offset : newgrf_offset;
+			}
+		}
+
+		ground = false;
+		if (regs != NULL) regs++;
+	}
+
+	return var10_values;
+}
+
+/**
+ * Evaluates the register modifiers and integrates them into the preprocessed sprite layout.
+ * @pre #PrepareLayout() needs calling first.
+ * @param resolved_var10  The value of var10 the action-1-2-3 chain was evaluated for.
+ * @param resolved_sprite Result sprite of the action-1-2-3 chain.
+ * @param separate_ground Whether the ground sprite is resolved by a separate action-1-2-3 chain.
+ * @return Resulting spritelayout after processing the registers.
+ */
+void NewGRFSpriteLayout::ProcessRegisters(uint8 resolved_var10, uint32 resolved_sprite, bool separate_ground) const
+{
+	DrawTileSeqStruct *result;
+	const TileLayoutRegisters *regs = this->registers;
+	bool ground = true;
+	foreach_draw_tile_seq(result, result_seq.Begin()) {
+		TileLayoutFlags flags = TLF_NOTHING;
+		if (regs != NULL) flags = regs->flags;
+
+		/* Is the sprite or bounding box affected by an action-1-2-3 chain? */
+		if (HasBit(result->image.sprite, SPRITE_MODIFIER_CUSTOM_SPRITE) || (flags & TLF_SPRITE_REG_FLAGS)) {
+			/* Does the var10 value apply to this sprite? */
+			uint8 var10 = (flags & TLF_SPRITE_VAR10) ? regs->sprite_var10 : (ground && separate_ground ? 1 : 0);
+			if (var10 == resolved_var10) {
+				/* Apply registers */
+				if ((flags & TLF_DODRAW) && GetRegister(regs->dodraw) == 0) {
+					result->image.sprite = 0;
+					continue;
+				}
+				if (HasBit(result->image.sprite, SPRITE_MODIFIER_CUSTOM_SPRITE)) result->image.sprite += resolved_sprite;
+				if (flags & TLF_SPRITE) result->image.sprite += (int16)GetRegister(regs->sprite); // mask to 16 bits to avoid trouble
+
+				if (result->IsParentSprite()) {
+					if (flags & TLF_BB_XY_OFFSET) {
+						result->delta_x += (int32)GetRegister(regs->delta.parent[0]);
+						result->delta_y += (int32)GetRegister(regs->delta.parent[1]);
+					}
+					if (flags & TLF_BB_Z_OFFSET)    result->delta_z += (int32)GetRegister(regs->delta.parent[2]);
+				} else {
+					if (flags & TLF_CHILD_X_OFFSET) result->delta_x += (int32)GetRegister(regs->delta.child[0]);
+					if (flags & TLF_CHILD_Y_OFFSET) result->delta_y += (int32)GetRegister(regs->delta.child[1]);
+				}
+			}
+		}
+
+		/* Is the palette affected by an action-1-2-3 chain? */
+		if (HasBit(result->image.pal, SPRITE_MODIFIER_CUSTOM_SPRITE) || (flags & TLF_PALETTE_REG_FLAGS)) {
+			/* Does the var10 value apply to this sprite? */
+			uint8 var10 = (flags & TLF_PALETTE_VAR10) ? regs->palette_var10 : (ground && separate_ground ? 1 : 0);
+			if (var10 == resolved_var10) {
+				/* Apply registers */
+				if (HasBit(result->image.pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) result->image.pal += resolved_sprite;
+				if (flags & TLF_PALETTE) result->image.pal += (int16)GetRegister(regs->palette); // mask to 16 bits to avoid trouble
+			}
+		}
+
+		ground = false;
+		if (regs != NULL) regs++;
+	}
+}
--- a/src/newgrf_commons.h
+++ b/src/newgrf_commons.h
@@ -18,6 +18,7 @@
 #include "tile_type.h"
 #include "sprite.h"
 #include "core/alloc_type.hpp"
+#include "core/smallvec_type.hpp"
 
 /** Context for tile accesses */
 enum TileContext {
@@ -27,13 +28,74 @@
 };
 
 /**
+ * Flags to enable register usage in sprite layouts.
+ */
+enum TileLayoutFlags {
+	TLF_NOTHING           = 0x00,
+
+	TLF_DODRAW            = 0x01,   ///< Only draw sprite if value of register TileLayoutRegisters::dodraw is non-zero.
+	TLF_SPRITE            = 0x02,   ///< Add signed offset to sprite from register TileLayoutRegisters::sprite.
+	TLF_PALETTE           = 0x04,   ///< Add signed offset to palette from register TileLayoutRegisters::palette.
+	TLF_CUSTOM_PALETTE    = 0x08,   ///< Palette is from Action 1 (moved to SPRITE_MODIFIER_CUSTOM_SPRITE in palette during loading).
+
+	TLF_BB_XY_OFFSET      = 0x10,   ///< Add signed offset to bounding box X and Y positions from register TileLayoutRegisters::delta.parent[0..1].
+	TLF_BB_Z_OFFSET       = 0x20,   ///< Add signed offset to bounding box Z positions from register TileLayoutRegisters::delta.parent[2].
+
+	TLF_CHILD_X_OFFSET    = 0x10,   ///< Add signed offset to child sprite X positions from register TileLayoutRegisters::delta.child[0].
+	TLF_CHILD_Y_OFFSET    = 0x20,   ///< Add signed offset to child sprite Y positions from register TileLayoutRegisters::delta.child[1].
+
+	TLF_SPRITE_VAR10      = 0x40,   ///< Resolve sprite with a specific value in variable 10.
+	TLF_PALETTE_VAR10     = 0x80,   ///< Resolve palette with a specific value in variable 10.
+
+	TLF_KNOWN_FLAGS       = 0x7F,   ///< Known flags. Any unknown set flag will disable the GRF.
+
+	/** Flags which are still required after loading the GRF. */
+	TLF_DRAWING_FLAGS     = ~TLF_CUSTOM_PALETTE,
+
+	/** Flags which do not work for the (first) ground sprite. */
+	TLF_NON_GROUND_FLAGS  = TLF_BB_XY_OFFSET | TLF_BB_Z_OFFSET | TLF_CHILD_X_OFFSET | TLF_CHILD_Y_OFFSET,
+
+	/** Flags which refer to using multiple action-1-2-3 chains. */
+	TLF_VAR10_FLAGS       = TLF_SPRITE_VAR10 | TLF_PALETTE_VAR10,
+
+	/** Flags which require resolving the action-1-2-3 chain for the sprite, even if it is no action-1 sprite. */
+	TLF_SPRITE_REG_FLAGS  = TLF_DODRAW | TLF_SPRITE | TLF_BB_XY_OFFSET | TLF_BB_Z_OFFSET | TLF_CHILD_X_OFFSET | TLF_CHILD_Y_OFFSET,
+
+	/** Flags which require resolving the action-1-2-3 chain for the palette, even if it is no action-1 palette. */
+	TLF_PALETTE_REG_FLAGS = TLF_PALETTE,
+};
+DECLARE_ENUM_AS_BIT_SET(TileLayoutFlags)
+
+/**
+ * Additional modifiers for items in sprite layouts.
+ */
+struct TileLayoutRegisters {
+	TileLayoutFlags flags; ///< Flags defining which members are valid and to be used.
+	uint8 dodraw;          ///< Register deciding whether the sprite shall be drawn at all. Non-zero means drawing.
+	uint8 sprite;          ///< Register specifying a signed offset for the sprite.
+	uint8 palette;         ///< Register specifying a signed offset for the palette.
+	union {
+		uint8 parent[3];   ///< Registers for signed offsets for the bounding box position of parent sprites.
+		uint8 child[2];    ///< Registers for signed offsets for the position of child sprites.
+	} delta;
+	uint8 sprite_var10;    ///< Value for variable 10 when resolving the sprite.
+	uint8 palette_var10;   ///< Value for variable 10 when resolving the palette.
+};
+
+static const uint TLR_MAX_VAR10 = 7; ///< Maximum value for var 10.
+
+/**
  * NewGRF supplied spritelayout.
  * In contrast to #DrawTileSprites this struct is for allocated
  * layouts on the heap. It allocates data and frees them on destruction.
  */
 struct NewGRFSpriteLayout : ZeroedMemoryAllocator, DrawTileSprites {
+	const TileLayoutRegisters *registers;
+
 	void Allocate(uint num_sprites);
+	void AllocateRegisters();
 	void Clone(const DrawTileSeqStruct *source);
+	void Clone(const NewGRFSpriteLayout *source);
 
 	/**
 	 * Clone a spritelayout.
@@ -49,7 +111,37 @@
 	virtual ~NewGRFSpriteLayout()
 	{
 		free(const_cast<DrawTileSeqStruct*>(this->seq));
+		free(const_cast<TileLayoutRegisters*>(this->registers));
 	}
+
+	/**
+	 * Tests whether this spritelayout needs preprocessing by
+	 * #PrepareLayout() and #ProcessRegisters(), or whether it can be
+	 * used directly.
+	 * @return true if preprocessing is needed
+	 */
+	bool NeedsPreprocessing() const
+	{
+		return this->registers != NULL;
+	}
+
+	uint32 PrepareLayout(uint32 orig_offset, uint32 newgrf_ground_offset, uint32 newgrf_offset, bool separate_ground) const;
+	void ProcessRegisters(uint8 resolved_var10, uint32 resolved_sprite, bool separate_ground) const;
+
+	/**
+	 * Returns the result spritelayout after preprocessing.
+	 * @pre #PrepareLayout() and #ProcessRegisters() need calling first.
+	 * @return result spritelayout
+	 */
+	const DrawTileSeqStruct *GetLayout(PalSpriteID *ground) const
+	{
+		DrawTileSeqStruct *front = result_seq.Begin();
+		*ground = front->image;
+		return front + 1;
+	}
+
+private:
+	static SmallVector<DrawTileSeqStruct, 8> result_seq; ///< Temporary storage when preprocessing spritelayouts.
 };
 
 /**
--- a/src/newgrf_house.cpp
+++ b/src/newgrf_house.cpp
@@ -412,7 +412,7 @@
 
 static void DrawTileLayout(const TileInfo *ti, const TileLayoutSpriteGroup *group, byte stage, HouseID house_id)
 {
-	const DrawTileSprites *dts = &group->dts;
+	const DrawTileSprites *dts = group->ProcessRegisters(&stage);
 
 	const HouseSpec *hs = HouseSpec::Get(house_id);
 	PaletteID palette = hs->random_colour[TileHash2Bit(ti->x, ti->y)] + PALETTE_RECOLOUR_START;
@@ -428,6 +428,7 @@
 	PaletteID pal  = dts->ground.pal;
 
 	if (HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) image += stage;
+	if (HasBit(pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) pal += stage;
 
 	if (GB(image, 0, SPRITE_WIDTH) != 0) {
 		DrawGroundSprite(image, GroundSpritePaletteTransform(image, pal, palette));
--- a/src/newgrf_industrytiles.cpp
+++ b/src/newgrf_industrytiles.cpp
@@ -176,12 +176,13 @@
 
 static void IndustryDrawTileLayout(const TileInfo *ti, const TileLayoutSpriteGroup *group, byte rnd_colour, byte stage, IndustryGfx gfx)
 {
-	const DrawTileSprites *dts = &group->dts;
+	const DrawTileSprites *dts = group->ProcessRegisters(&stage);
 
 	SpriteID image = dts->ground.sprite;
 	PaletteID pal  = dts->ground.pal;
 
 	if (HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) image += stage;
+	if (HasBit(pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) pal += stage;
 
 	if (GB(image, 0, SPRITE_WIDTH) != 0) {
 		/* If the ground sprite is the default flat water sprite, draw also canal/river borders
--- a/src/newgrf_object.cpp
+++ b/src/newgrf_object.cpp
@@ -417,7 +417,7 @@
  */
 static void DrawTileLayout(const TileInfo *ti, const TileLayoutSpriteGroup *group, const ObjectSpec *spec)
 {
-	const DrawTileSprites *dts = &group->dts;
+	const DrawTileSprites *dts = group->ProcessRegisters(NULL);
 	PaletteID palette = ((spec->flags & OBJECT_FLAG_2CC_COLOUR) ? SPR_2CCMAP_BASE : PALETTE_RECOLOUR_START) + Object::GetByTile(ti->tile)->colour;
 
 	SpriteID image = dts->ground.sprite;
@@ -468,7 +468,7 @@
 	const SpriteGroup *group = SpriteGroup::Resolve(GetObjectSpriteGroup(spec, NULL), &object);
 	if (group == NULL || group->type != SGT_TILELAYOUT) return;
 
-	const DrawTileSprites *dts = &((const TileLayoutSpriteGroup *)group)->dts;
+	const DrawTileSprites *dts = ((const TileLayoutSpriteGroup *)group)->ProcessRegisters(NULL);
 
 	PaletteID palette;
 	if (Company::IsValidID(_local_company)) {
--- a/src/newgrf_spritegroup.cpp
+++ b/src/newgrf_spritegroup.cpp
@@ -226,3 +226,26 @@
 {
 	return object->ResolveReal(object, this);
 }
+
+/**
+ * Process registers and the construction stage into the sprite layout.
+ * The passed construction stage might get reset to zero, if it gets incorporated into the layout
+ * during the preprocessing.
+ * @param [in, out] stage Construction stage (0-3), or NULL if not applicable.
+ * @return sprite layout to draw.
+ */
+const DrawTileSprites *TileLayoutSpriteGroup::ProcessRegisters(uint8 *stage) const
+{
+	if (!this->dts.NeedsPreprocessing()) return &this->dts;
+
+	static DrawTileSprites result;
+	uint8 actual_stage = stage != NULL ? *stage : 0;
+	this->dts.PrepareLayout(0, actual_stage, actual_stage, false);
+	this->dts.ProcessRegisters(0, 0, false);
+	result.seq = this->dts.GetLayout(&result.ground);
+
+	/* Stage has been processed by PrepareLayout(), set it to zero. */
+	if (stage != NULL) *stage = 0;
+
+	return &result;
+}
--- a/src/newgrf_spritegroup.h
+++ b/src/newgrf_spritegroup.h
@@ -278,12 +278,17 @@
 	byte GetNumResults() const { return this->num_sprites; }
 };
 
+/**
+ * Action 2 sprite layout for houses, industry tiles, objects and airport tiles.
+ */
 struct TileLayoutSpriteGroup : SpriteGroup {
 	TileLayoutSpriteGroup() : SpriteGroup(SGT_TILELAYOUT) {}
 	~TileLayoutSpriteGroup() {}
 
 	byte num_building_stages;    ///< Number of building stages to show for this house/industry tile
 	NewGRFSpriteLayout dts;
+
+	const DrawTileSprites *ProcessRegisters(uint8 *stage) const;
 };
 
 struct IndustryProductionSpriteGroup : SpriteGroup {
--- a/src/newgrf_station.cpp
+++ b/src/newgrf_station.cpp
@@ -746,7 +746,7 @@
 bool DrawStationTile(int x, int y, RailType railtype, Axis axis, StationClassID sclass, uint station)
 {
 	const StationSpec *statspec;
-	const DrawTileSprites *sprites;
+	const DrawTileSprites *sprites = NULL;
 	const RailtypeInfo *rti = GetRailTypeInfo(railtype);
 	PaletteID palette = COMPANY_SPRITE_COLOUR(_local_company);
 	uint tile = 2;
@@ -754,36 +754,57 @@
 	statspec = StationClass::Get(sclass, station);
 	if (statspec == NULL) return false;
 
-	uint relocation = GetCustomStationRelocation(statspec, NULL, INVALID_TILE);
-
 	if (HasBit(statspec->callback_mask, CBM_STATION_SPRITE_LAYOUT)) {
 		uint16 callback = GetStationCallback(CBID_STATION_SPRITE_LAYOUT, 0x2110000, 0, statspec, NULL, INVALID_TILE);
 		if (callback != CALLBACK_FAILED) tile = callback;
 	}
 
+	uint32 total_offset = rti->GetRailtypeSpriteOffset();
+	uint32 relocation = 0;
+	uint32 ground_relocation = 0;
+	const NewGRFSpriteLayout *layout = NULL;
+	DrawTileSprites tmp_rail_layout;
+
 	if (statspec->renderdata == NULL) {
 		sprites = GetStationTileLayout(STATION_RAIL, tile + axis);
 	} else {
-		sprites = &statspec->renderdata[(tile < statspec->tiles) ? tile + axis : (uint)axis];
+		layout = &statspec->renderdata[(tile < statspec->tiles) ? tile + axis : (uint)axis];
+		if (!layout->NeedsPreprocessing()) {
+			sprites = layout;
+			layout = NULL;
+		}
+	}
+
+	if (layout != NULL) {
+		/* Sprite layout which needs preprocessing */
+		bool separate_ground = HasBit(statspec->flags, SSF_SEPARATE_GROUND);
+		uint32 var10_values = layout->PrepareLayout(total_offset, rti->fallback_railtype, 0, separate_ground);
+		uint8 var10;
+		FOR_EACH_SET_BIT(var10, var10_values) {
+			uint32 var10_relocation = GetCustomStationRelocation(statspec, NULL, INVALID_TILE, var10);
+			layout->ProcessRegisters(var10, var10_relocation, separate_ground);
+		}
+
+		tmp_rail_layout.seq = layout->GetLayout(&tmp_rail_layout.ground);
+		sprites = &tmp_rail_layout;
+		total_offset = 0;
+	} else {
+		/* Simple sprite layout */
+		ground_relocation = relocation = GetCustomStationRelocation(statspec, NULL, INVALID_TILE, 0);
+		if (HasBit(sprites->ground.sprite, SPRITE_MODIFIER_CUSTOM_SPRITE)) {
+			ground_relocation = GetCustomStationRelocation(statspec, NULL, INVALID_TILE, 1);
+		}
+		ground_relocation += rti->fallback_railtype;
 	}
 
 	SpriteID image = sprites->ground.sprite;
 	PaletteID pal = sprites->ground.pal;
-	if (HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) {
-		if (HasBit(statspec->flags, SSF_SEPARATE_GROUND)) {
-			/* Use separate action 1-2-3 chain for ground sprite */
-			image += GetCustomStationRelocation(statspec, NULL, INVALID_TILE, 1);
-		} else {
-			image += relocation;
-		}
-		image += rti->fallback_railtype;
-	} else {
-		image += rti->GetRailtypeSpriteOffset();
-	}
+	image += HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE) ? ground_relocation : total_offset;
+	if (HasBit(pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) pal += ground_relocation;
 
 	DrawSprite(image, GroundSpritePaletteTransform(image, pal, palette), x, y);
 
-	DrawRailTileSeqInGUI(x, y, sprites, rti->GetRailtypeSpriteOffset(), relocation, palette);
+	DrawRailTileSeqInGUI(x, y, sprites, total_offset, relocation, palette);
 
 	return true;
 }
--- a/src/sprite.cpp
+++ b/src/sprite.cpp
@@ -32,18 +32,29 @@
 {
 	bool parent_sprite_encountered = false;
 	const DrawTileSeqStruct *dtss;
+	bool skip_childs = false;
 	foreach_draw_tile_seq(dtss, dts->seq) {
 		SpriteID image = dtss->image.sprite;
+		PaletteID pal = dtss->image.pal;
+
+		if (skip_childs) {
+			if (!dtss->IsParentSprite()) continue;
+			skip_childs = false;
+		}
 
 		/* TTD sprite 0 means no sprite */
-		if (GB(image, 0, SPRITE_WIDTH) == 0 && !HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) continue;
+		if (GB(image, 0, SPRITE_WIDTH) == 0 && !HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) {
+			skip_childs = dtss->IsParentSprite();
+			continue;
+		}
 
 		/* Stop drawing sprite sequence once we meet a sprite that doesn't have to be opaque */
 		if (IsInvisibilitySet(to) && !HasBit(image, SPRITE_MODIFIER_OPAQUE)) return;
 
 		image += (HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE) ? newgrf_offset : orig_offset);
+		if (HasBit(pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) pal += newgrf_offset;
 
-		PaletteID pal = SpriteLayoutPaletteTransform(image, dtss->image.pal, default_palette);
+		pal = SpriteLayoutPaletteTransform(image, pal, default_palette);
 
 		if (dtss->IsParentSprite()) {
 			parent_sprite_encountered = true;
@@ -86,15 +97,26 @@
 	const DrawTileSeqStruct *dtss;
 	Point child_offset = {0, 0};
 
+	bool skip_childs = false;
 	foreach_draw_tile_seq(dtss, dts->seq) {
 		SpriteID image = dtss->image.sprite;
+		PaletteID pal = dtss->image.pal;
+
+		if (skip_childs) {
+			if (!dtss->IsParentSprite()) continue;
+			skip_childs = false;
+		}
 
 		/* TTD sprite 0 means no sprite */
-		if (GB(image, 0, SPRITE_WIDTH) == 0 && !HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) continue;
+		if (GB(image, 0, SPRITE_WIDTH) == 0 && !HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) {
+			skip_childs = dtss->IsParentSprite();
+			continue;
+		}
 
 		image += (HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE) ? newgrf_offset : orig_offset);
+		if (HasBit(pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) pal += newgrf_offset;
 
-		PaletteID pal = SpriteLayoutPaletteTransform(image, dtss->image.pal, default_palette);
+		pal = SpriteLayoutPaletteTransform(image, pal, default_palette);
 
 		if (dtss->IsParentSprite()) {
 			Point pt = RemapCoords(dtss->delta_x, dtss->delta_y, dtss->delta_z);
--- a/src/station_cmd.cpp
+++ b/src/station_cmd.cpp
@@ -2495,12 +2495,14 @@
 
 static void DrawTile_Station(TileInfo *ti)
 {
+	const NewGRFSpriteLayout *layout = NULL;
+	DrawTileSprites tmp_rail_layout;
 	const DrawTileSprites *t = NULL;
 	RoadTypes roadtypes;
 	int32 total_offset;
-	int32 custom_ground_offset;
 	const RailtypeInfo *rti = NULL;
 	uint32 relocation = 0;
+	uint32 ground_relocation = 0;
 	const BaseStation *st = NULL;
 	const StationSpec *statspec = NULL;
 	uint tile_layout = 0;
@@ -2509,7 +2511,6 @@
 		rti = GetRailTypeInfo(GetRailType(ti->tile));
 		roadtypes = ROADTYPES_NONE;
 		total_offset = rti->GetRailtypeSpriteOffset();
-		custom_ground_offset = rti->fallback_railtype;
 
 		if (IsCustomStationSpecIndex(ti->tile)) {
 			/* look for customization */
@@ -2519,8 +2520,6 @@
 			if (statspec != NULL) {
 				tile_layout = GetStationGfx(ti->tile);
 
-				relocation = GetCustomStationRelocation(statspec, st, ti->tile);
-
 				if (HasBit(statspec->callback_mask, CBM_STATION_SPRITE_LAYOUT)) {
 					uint16 callback = GetStationCallback(CBID_STATION_SPRITE_LAYOUT, 0, 0, statspec, st, ti->tile);
 					if (callback != CALLBACK_FAILED) tile_layout = (callback & ~1) + GetRailStationAxis(ti->tile);
@@ -2528,14 +2527,17 @@
 
 				/* Ensure the chosen tile layout is valid for this custom station */
 				if (statspec->renderdata != NULL) {
-					t = &statspec->renderdata[tile_layout < statspec->tiles ? tile_layout : (uint)GetRailStationAxis(ti->tile)];
+					layout = &statspec->renderdata[tile_layout < statspec->tiles ? tile_layout : (uint)GetRailStationAxis(ti->tile)];
+					if (!layout->NeedsPreprocessing()) {
+						t = layout;
+						layout = NULL;
+					}
 				}
 			}
 		}
 	} else {
 		roadtypes = IsRoadStop(ti->tile) ? GetRoadTypes(ti->tile) : ROADTYPES_NONE;
 		total_offset = 0;
-		custom_ground_offset = 0;
 	}
 
 	if (IsAirport(ti->tile)) {
@@ -2579,7 +2581,7 @@
 		palette = PALETTE_TO_GREY;
 	}
 
-	if (t == NULL || t->seq == NULL) t = GetStationTileLayout(GetStationType(ti->tile), GetStationGfx(ti->tile));
+	if (layout == NULL && (t == NULL || t->seq == NULL)) t = GetStationTileLayout(GetStationType(ti->tile), GetStationGfx(ti->tile));
 
 	/* don't show foundation for docks */
 	if (ti->tileh != SLOPE_FLAT && !IsDock(ti->tile)) {
@@ -2665,6 +2667,27 @@
 			}
 		}
 	} else {
+		if (layout != NULL) {
+			/* Sprite layout which needs preprocessing */
+			bool separate_ground = HasBit(statspec->flags, SSF_SEPARATE_GROUND);
+			uint32 var10_values = layout->PrepareLayout(total_offset, rti->fallback_railtype, 0, separate_ground);
+			uint8 var10;
+			FOR_EACH_SET_BIT(var10, var10_values) {
+				uint32 var10_relocation = GetCustomStationRelocation(statspec, st, ti->tile, var10);
+				layout->ProcessRegisters(var10, var10_relocation, separate_ground);
+			}
+			tmp_rail_layout.seq = layout->GetLayout(&tmp_rail_layout.ground);
+			t = &tmp_rail_layout;
+			total_offset = 0;
+		} else if (statspec != NULL) {
+			/* Simple sprite layout */
+			ground_relocation = relocation = GetCustomStationRelocation(statspec, st, ti->tile, 0);
+			if (HasBit(statspec->flags, SSF_SEPARATE_GROUND)) {
+				ground_relocation = GetCustomStationRelocation(statspec, st, ti->tile, 1);
+			}
+			ground_relocation += rti->fallback_railtype;
+		}
+
 		SpriteID image = t->ground.sprite;
 		PaletteID pal  = t->ground.pal;
 		if (rti != NULL && rti->UsesOverlay() && (image == SPR_RAIL_TRACK_X || image == SPR_RAIL_TRACK_Y)) {
@@ -2677,17 +2700,8 @@
 				DrawGroundSprite(overlay + (image == SPR_RAIL_TRACK_X ? RTO_X : RTO_Y), PALETTE_CRASH);
 			}
 		} else {
-			if (HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) {
-				if (HasBit(statspec->flags, SSF_SEPARATE_GROUND)) {
-					/* Use separate action 1-2-3 chain for ground sprite */
-					image += GetCustomStationRelocation(statspec, st, ti->tile, 1);
-				} else {
-					image += relocation;
-				}
-				image += custom_ground_offset;
-			} else {
-				image += total_offset;
-			}
+			image += HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE) ? ground_relocation : total_offset;
+			if (HasBit(pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) pal += ground_relocation;
 			DrawGroundSprite(image, GroundSpritePaletteTransform(image, pal, palette));
 
 			/* PBS debugging, draw reserved tracks darker */