From ff7eb996e6beef071c583c41f43550e55e40920a Mon Sep 17 00:00:00 2001
From: Peter Nelson <peter1138@openttd.org>
Date: Fri, 14 Feb 2025 18:30:17 +0000
Subject: [PATCH] Codechange: Use variable storage for GrfProps with cargo-type
 groups. (#13557)

Slots are only allocated when used instead of being reserved.

Array-based GrfProps are still used when the number of options is more limited.
---
 src/engine_base.h            |  2 +-
 src/industry_cmd.cpp         |  4 ++--
 src/newgrf.cpp               | 22 +++++++++---------
 src/newgrf_airport.cpp       |  2 +-
 src/newgrf_airporttiles.cpp  |  4 ++--
 src/newgrf_commons.cpp       | 27 ++++++++++++++++++++++
 src/newgrf_commons.h         | 43 +++++++++++++++++++++++++++++++-----
 src/newgrf_engine.cpp        |  9 ++++----
 src/newgrf_house.cpp         |  4 ++--
 src/newgrf_industries.cpp    |  4 ++--
 src/newgrf_industrytiles.cpp |  4 ++--
 src/newgrf_object.cpp        |  4 ++--
 src/newgrf_object.h          |  2 +-
 src/newgrf_roadstop.cpp      | 18 ++++++++++-----
 src/newgrf_roadstop.h        |  2 +-
 src/newgrf_station.cpp       | 19 +++++++++++-----
 src/newgrf_station.h         |  2 +-
 src/station_cmd.cpp          |  2 +-
 src/table/object_land.h      |  2 +-
 src/town_cmd.cpp             |  4 ++--
 src/town_gui.cpp             |  2 +-
 21 files changed, 126 insertions(+), 56 deletions(-)

diff --git a/src/engine_base.h b/src/engine_base.h
index c3e73a3225..a5bcf2317b 100644
--- a/src/engine_base.h
+++ b/src/engine_base.h
@@ -80,7 +80,7 @@ struct Engine : EnginePool::PoolItem<&_engine_pool> {
 	 * Used for obtaining the sprite offset of custom sprites, and for
 	 * evaluating callbacks.
 	 */
-	GRFFilePropsBase<NUM_CARGO + 2> grf_prop;
+	VariableGRFFileProps grf_prop;
 	std::vector<WagonOverride> overrides;
 
 	Engine() {}
diff --git a/src/industry_cmd.cpp b/src/industry_cmd.cpp
index c5063caf67..b29e40a824 100644
--- a/src/industry_cmd.cpp
+++ b/src/industry_cmd.cpp
@@ -335,7 +335,7 @@ static void DrawTile_Industry(TileInfo *ti)
 		 * DrawNewIndustry will return false if ever the resolver could not
 		 * find any sprite to display.  So in this case, we will jump on the
 		 * substitute gfx instead. */
-		if (indts->grf_prop.spritegroup[0] != nullptr && DrawNewIndustryTile(ti, ind, gfx, indts)) {
+		if (indts->grf_prop.GetSpriteGroup() != nullptr && DrawNewIndustryTile(ti, ind, gfx, indts)) {
 			return;
 		} else {
 			/* No sprite group (or no valid one) found, meaning no graphics associated.
@@ -404,7 +404,7 @@ static Foundation GetFoundation_Industry(TileIndex tile, Slope tileh)
 	 */
 	if (gfx >= NEW_INDUSTRYTILEOFFSET) {
 		const IndustryTileSpec *indts = GetIndustryTileSpec(gfx);
-		if (indts->grf_prop.spritegroup[0] != nullptr && indts->callback_mask.Test(IndustryTileCallbackMask::DrawFoundations)) {
+		if (indts->grf_prop.GetSpriteGroup() != nullptr && indts->callback_mask.Test(IndustryTileCallbackMask::DrawFoundations)) {
 			uint32_t callback_res = GetIndustryTileCallback(CBID_INDTILE_DRAW_FOUNDATIONS, 0, 0, gfx, Industry::GetByTile(tile), tile);
 			if (callback_res != CALLBACK_FAILED && !ConvertBooleanCallback(indts->grf_prop.grffile, CBID_INDTILE_DRAW_FOUNDATIONS, callback_res)) return FOUNDATION_NONE;
 		}
diff --git a/src/newgrf.cpp b/src/newgrf.cpp
index 6c0b71dc03..35c242f848 100644
--- a/src/newgrf.cpp
+++ b/src/newgrf.cpp
@@ -5810,7 +5810,7 @@ static void StationMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 				continue;
 			}
 
-			statspec->grf_prop.spritegroup[ctype] = _cur.spritegroups[groupid];
+			statspec->grf_prop.SetSpriteGroup(ctype, _cur.spritegroups[groupid]);
 		}
 	}
 
@@ -5830,7 +5830,7 @@ static void StationMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 			continue;
 		}
 
-		statspec->grf_prop.spritegroup[SpriteGroupCargo::SG_DEFAULT] = _cur.spritegroups[groupid];
+		statspec->grf_prop.SetSpriteGroup(SpriteGroupCargo::SG_DEFAULT, _cur.spritegroups[groupid]);
 		statspec->grf_prop.grfid = _cur.grffile->grfid;
 		statspec->grf_prop.grffile = _cur.grffile;
 		statspec->grf_prop.local_id = station;
@@ -5867,7 +5867,7 @@ static void TownHouseMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 			continue;
 		}
 
-		hs->grf_prop.spritegroup[0] = _cur.spritegroups[groupid];
+		hs->grf_prop.SetSpriteGroup(0, _cur.spritegroups[groupid]);
 	}
 }
 
@@ -5899,7 +5899,7 @@ static void IndustryMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 			continue;
 		}
 
-		indsp->grf_prop.spritegroup[0] = _cur.spritegroups[groupid];
+		indsp->grf_prop.SetSpriteGroup(0, _cur.spritegroups[groupid]);
 	}
 }
 
@@ -5931,7 +5931,7 @@ static void IndustrytileMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 			continue;
 		}
 
-		indtsp->grf_prop.spritegroup[0] = _cur.spritegroups[groupid];
+		indtsp->grf_prop.SetSpriteGroup(0, _cur.spritegroups[groupid]);
 	}
 }
 
@@ -5995,7 +5995,7 @@ static void ObjectMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 				continue;
 			}
 
-			spec->grf_prop.spritegroup[OBJECT_SPRITE_GROUP_PURCHASE] = _cur.spritegroups[groupid];
+			spec->grf_prop.SetSpriteGroup(OBJECT_SPRITE_GROUP_PURCHASE, _cur.spritegroups[groupid]);
 		}
 	}
 
@@ -6015,7 +6015,7 @@ static void ObjectMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 			continue;
 		}
 
-		spec->grf_prop.spritegroup[OBJECT_SPRITE_GROUP_DEFAULT] = _cur.spritegroups[groupid];
+		spec->grf_prop.SetSpriteGroup(OBJECT_SPRITE_GROUP_DEFAULT, _cur.spritegroups[groupid]);
 		spec->grf_prop.grfid = _cur.grffile->grfid;
 		spec->grf_prop.grffile = _cur.grffile;
 		spec->grf_prop.local_id = object;
@@ -6116,7 +6116,7 @@ static void AirportMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 			continue;
 		}
 
-		as->grf_prop.spritegroup[0] = _cur.spritegroups[groupid];
+		as->grf_prop.SetSpriteGroup(0, _cur.spritegroups[groupid]);
 	}
 }
 
@@ -6148,7 +6148,7 @@ static void AirportTileMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 			continue;
 		}
 
-		airtsp->grf_prop.spritegroup[0] = _cur.spritegroups[groupid];
+		airtsp->grf_prop.SetSpriteGroup(0, _cur.spritegroups[groupid]);
 	}
 }
 
@@ -6182,7 +6182,7 @@ static void RoadStopMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 				continue;
 			}
 
-			roadstopspec->grf_prop.spritegroup[ctype] = _cur.spritegroups[groupid];
+			roadstopspec->grf_prop.SetSpriteGroup(ctype, _cur.spritegroups[groupid]);
 		}
 	}
 
@@ -6202,7 +6202,7 @@ static void RoadStopMapSpriteGroup(ByteReader &buf, uint8_t idcount)
 			continue;
 		}
 
-		roadstopspec->grf_prop.spritegroup[SpriteGroupCargo::SG_DEFAULT] = _cur.spritegroups[groupid];
+		roadstopspec->grf_prop.SetSpriteGroup(SpriteGroupCargo::SG_DEFAULT, _cur.spritegroups[groupid]);
 		roadstopspec->grf_prop.grfid = _cur.grffile->grfid;
 		roadstopspec->grf_prop.grffile = _cur.grffile;
 		roadstopspec->grf_prop.local_id = roadstop;
diff --git a/src/newgrf_airport.cpp b/src/newgrf_airport.cpp
index 729791d6a8..fa1def20ae 100644
--- a/src/newgrf_airport.cpp
+++ b/src/newgrf_airport.cpp
@@ -246,7 +246,7 @@ AirportResolverObject::AirportResolverObject(TileIndex tile, Station *st, const
 		CallbackID callback, uint32_t param1, uint32_t param2)
 	: ResolverObject(spec->grf_prop.grffile, callback, param1, param2), airport_scope(*this, tile, st, spec, layout)
 {
-	this->root_spritegroup = spec->grf_prop.spritegroup[0];
+	this->root_spritegroup = spec->grf_prop.GetSpriteGroup();
 }
 
 SpriteID GetCustomAirportSprite(const AirportSpec *as, uint8_t layout)
diff --git a/src/newgrf_airporttiles.cpp b/src/newgrf_airporttiles.cpp
index f1624b7801..23a012bb34 100644
--- a/src/newgrf_airporttiles.cpp
+++ b/src/newgrf_airporttiles.cpp
@@ -148,7 +148,7 @@ static uint32_t GetAirportTileIDAtOffset(TileIndex tile, const Station *st, uint
 		}
 	}
 	/* Not an 'old type' tile */
-	if (ats->grf_prop.spritegroup[0] != nullptr) { // tile has a spritegroup ?
+	if (ats->grf_prop.GetSpriteGroup() != nullptr) { // tile has a spritegroup ?
 		if (ats->grf_prop.grfid == cur_grfid) { // same airport, same grf ?
 			return ats->grf_prop.local_id;
 		} else {
@@ -218,7 +218,7 @@ AirportTileResolverObject::AirportTileResolverObject(const AirportTileSpec *ats,
 		tiles_scope(*this, ats, tile, st),
 		airport_scope(*this, tile, st, st != nullptr ? AirportSpec::Get(st->airport.type) : nullptr, st != nullptr ? st->airport.layout : 0)
 {
-	this->root_spritegroup = ats->grf_prop.spritegroup[0];
+	this->root_spritegroup = ats->grf_prop.GetSpriteGroup();
 }
 
 GrfSpecFeature AirportTileResolverObject::GetFeature() const
diff --git a/src/newgrf_commons.cpp b/src/newgrf_commons.cpp
index c335183d8e..932bf290fd 100644
--- a/src/newgrf_commons.cpp
+++ b/src/newgrf_commons.cpp
@@ -756,3 +756,30 @@ void NewGRFSpriteLayout::ProcessRegisters(uint8_t resolved_var10, uint32_t resol
 		if (regs != nullptr) regs++;
 	}
 }
+
+/**
+ * Get the SpriteGroup at the specified index.
+ * @param index Index to get.
+ * @returns SpriteGroup at index, or nullptr if not present.
+ */
+const SpriteGroup *VariableGRFFileProps::GetSpriteGroup(size_t index) const
+{
+	auto it = std::ranges::lower_bound(this->spritegroups, index, std::less{}, &CargoSpriteGroup::first);
+	if (it == std::end(this->spritegroups) || it->first != index) return nullptr;
+	return it->second;
+}
+
+/**
+ * Set the SpriteGroup at the specified index.
+ * @param index Index to set.
+ * @param spritegroup SpriteGroup to set.
+ */
+void VariableGRFFileProps::SetSpriteGroup(size_t index, const SpriteGroup *spritegroup)
+{
+	auto it = std::ranges::lower_bound(this->spritegroups, index, std::less{}, &CargoSpriteGroup::first);
+	if (it == std::end(this->spritegroups) || it->first != index) {
+		this->spritegroups.emplace(it, index, spritegroup);
+	} else {
+		it->second = spritegroup;
+	}
+}
diff --git a/src/newgrf_commons.h b/src/newgrf_commons.h
index ad06336ec8..279a7b510b 100644
--- a/src/newgrf_commons.h
+++ b/src/newgrf_commons.h
@@ -307,15 +307,12 @@ bool ConvertBooleanCallback(const struct GRFFile *grffile, uint16_t cbid, uint16
 bool Convert8bitBooleanCallback(const struct GRFFile *grffile, uint16_t cbid, uint16_t cb_res);
 
 /**
- * Data related to the handling of grf files.
- * @tparam Tcnt Number of spritegroups
+ * Base data related to the handling of grf files.
  */
-template <size_t Tcnt>
 struct GRFFilePropsBase {
 	uint16_t local_id = 0; ///< id defined by the grf file for this entity
 	uint32_t grfid = 0; ///< grfid that introduced this entity.
 	const struct GRFFile *grffile = nullptr; ///< grf file that introduced this entity
-	std::array<const struct SpriteGroup *, Tcnt> spritegroup{}; ///< pointers to the different sprites of the entity
 
 	/**
 	 * Test if this entity was introduced by NewGRF.
@@ -324,13 +321,47 @@ struct GRFFilePropsBase {
 	inline bool HasGrfFile() const { return this->grffile != nullptr; }
 };
 
+/**
+ * Fixed-length list of sprite groups for an entity.
+ * @tparam Tcount Number of spritegroups
+ */
+template <size_t Tcount>
+struct FixedGRFFileProps : GRFFilePropsBase {
+	std::array<const struct SpriteGroup *, Tcount> spritegroups{}; ///< pointers to the different sprite groups of the entity
+
+	/**
+	 * Get the SpriteGroup at the specified index.
+	 * @param index Index to get.
+	 * @returns SpriteGroup at index, or nullptr if not present.
+	 */
+	const struct SpriteGroup *GetSpriteGroup(size_t index = 0) const { return this->spritegroups[index]; }
+
+	/**
+	 * Set the SpriteGroup at the specified index.
+	 * @param index Index to set.
+	 * @param spritegroup SpriteGroup to set.
+	 */
+	void SetSpriteGroup(size_t index, const struct SpriteGroup *spritegroup) { this->spritegroups[index] = spritegroup; }
+};
+
+/**
+ * Variable-length list of sprite groups for an entity.
+ */
+struct VariableGRFFileProps : GRFFilePropsBase {
+	using CargoSpriteGroup = std::pair<size_t, const struct SpriteGroup *>;
+	std::vector<CargoSpriteGroup> spritegroups; ///< pointers to the different sprite groups of the entity
+
+	const struct SpriteGroup *GetSpriteGroup(size_t index) const;
+	void SetSpriteGroup(size_t index, const struct SpriteGroup *spritegroup);
+};
+
 /** Data related to the handling of grf files. */
-struct GRFFileProps : GRFFilePropsBase<1> {
+struct GRFFileProps : FixedGRFFileProps<1> {
 	/** Set all default data constructor for the props. */
 	constexpr GRFFileProps(uint16_t subst_id = 0) : subst_id(subst_id), override(subst_id) {}
 
 	uint16_t subst_id;
-	uint16_t override;                      ///< id of the entity been replaced by
+	uint16_t override; ///< id of the entity been replaced by
 };
 
 /** Container for a label for rail or road type conversion. */
diff --git a/src/newgrf_engine.cpp b/src/newgrf_engine.cpp
index e9197782e5..808dda722e 100644
--- a/src/newgrf_engine.cpp
+++ b/src/newgrf_engine.cpp
@@ -53,12 +53,11 @@ const SpriteGroup *GetWagonOverrideSpriteSet(EngineID engine, CargoType cargo, E
 void SetCustomEngineSprites(EngineID engine, CargoType cargo, const SpriteGroup *group)
 {
 	Engine *e = Engine::Get(engine);
-	assert(cargo < std::size(e->grf_prop.spritegroup));
 
-	if (e->grf_prop.spritegroup[cargo] != nullptr) {
+	if (e->grf_prop.GetSpriteGroup(cargo) != nullptr) {
 		GrfMsg(6, "SetCustomEngineSprites: engine {} cargo {} already has group -- replacing", engine, cargo);
 	}
-	e->grf_prop.spritegroup[cargo] = group;
+	e->grf_prop.SetSpriteGroup(cargo, group);
 }
 
 
@@ -1063,8 +1062,8 @@ VehicleResolverObject::VehicleResolverObject(EngineID engine_type, const Vehicle
 		if (this->root_spritegroup == nullptr) {
 			const Engine *e = Engine::Get(engine_type);
 			CargoType cargo = v != nullptr ? v->cargo_type : SpriteGroupCargo::SG_PURCHASE;
-			assert(cargo < std::size(e->grf_prop.spritegroup));
-			this->root_spritegroup = e->grf_prop.spritegroup[cargo] != nullptr ? e->grf_prop.spritegroup[cargo] : e->grf_prop.spritegroup[SpriteGroupCargo::SG_DEFAULT];
+			this->root_spritegroup = e->grf_prop.GetSpriteGroup(cargo);
+			if (this->root_spritegroup == nullptr) this->root_spritegroup = e->grf_prop.GetSpriteGroup(SpriteGroupCargo::SG_DEFAULT);
 		}
 	}
 }
diff --git a/src/newgrf_house.cpp b/src/newgrf_house.cpp
index 0924d86314..0059012dd6 100644
--- a/src/newgrf_house.cpp
+++ b/src/newgrf_house.cpp
@@ -113,7 +113,7 @@ HouseResolverObject::HouseResolverObject(HouseID house_id, TileIndex tile, Town
 	/* Tile must be valid and a house tile, unless not yet constructed in which case it may also be INVALID_TILE. */
 	assert((IsValidTile(tile) && (not_yet_constructed || IsTileType(tile, MP_HOUSE))) || (not_yet_constructed && tile == INVALID_TILE));
 
-	this->root_spritegroup = HouseSpec::Get(house_id)->grf_prop.spritegroup[0];
+	this->root_spritegroup = HouseSpec::Get(house_id)->grf_prop.GetSpriteGroup();
 }
 
 GrfSpecFeature HouseResolverObject::GetFeature() const
@@ -687,7 +687,7 @@ static void DoTriggerHouse(TileIndex tile, HouseTrigger trigger, uint8_t base_ra
 	HouseID hid = GetHouseType(tile);
 	HouseSpec *hs = HouseSpec::Get(hid);
 
-	if (hs->grf_prop.spritegroup[0] == nullptr) return;
+	if (hs->grf_prop.GetSpriteGroup() == nullptr) return;
 
 	HouseResolverObject object(hid, tile, Town::GetByTile(tile), CBID_RANDOM_TRIGGER);
 	object.waiting_triggers = GetHouseTriggers(tile) | trigger;
diff --git a/src/newgrf_industries.cpp b/src/newgrf_industries.cpp
index 02f2531484..4eb3123b4c 100644
--- a/src/newgrf_industries.cpp
+++ b/src/newgrf_industries.cpp
@@ -78,7 +78,7 @@ uint32_t GetIndustryIDAtOffset(TileIndex tile, const Industry *i, uint32_t cur_g
 		}
 	}
 	/* Not an 'old type' tile */
-	if (indtsp->grf_prop.spritegroup[0] != nullptr) { // tile has a spritegroup ?
+	if (indtsp->grf_prop.GetSpriteGroup() != nullptr) { // tile has a spritegroup ?
 		if (indtsp->grf_prop.grfid == cur_grfid) { // same industry, same grf ?
 			return indtsp->grf_prop.local_id;
 		} else {
@@ -476,7 +476,7 @@ IndustriesResolverObject::IndustriesResolverObject(TileIndex tile, Industry *ind
 	: ResolverObject(GetGrffile(type), callback, callback_param1, callback_param2),
 	industries_scope(*this, tile, indus, type, random_bits)
 {
-	this->root_spritegroup = GetIndustrySpec(type)->grf_prop.spritegroup[0];
+	this->root_spritegroup = GetIndustrySpec(type)->grf_prop.GetSpriteGroup();
 }
 
 /**
diff --git a/src/newgrf_industrytiles.cpp b/src/newgrf_industrytiles.cpp
index 41b9c4b506..d57888ea05 100644
--- a/src/newgrf_industrytiles.cpp
+++ b/src/newgrf_industrytiles.cpp
@@ -142,7 +142,7 @@ IndustryTileResolverObject::IndustryTileResolverObject(IndustryGfx gfx, TileInde
 	ind_scope(*this, tile, indus, indus->type),
 	gfx(gfx)
 {
-	this->root_spritegroup = GetIndustryTileSpec(gfx)->grf_prop.spritegroup[0];
+	this->root_spritegroup = GetIndustryTileSpec(gfx)->grf_prop.GetSpriteGroup();
 }
 
 GrfSpecFeature IndustryTileResolverObject::GetFeature() const
@@ -314,7 +314,7 @@ static void DoTriggerIndustryTile(TileIndex tile, IndustryTileTrigger trigger, I
 	IndustryGfx gfx = GetIndustryGfx(tile);
 	const IndustryTileSpec *itspec = GetIndustryTileSpec(gfx);
 
-	if (itspec->grf_prop.spritegroup[0] == nullptr) return;
+	if (itspec->grf_prop.GetSpriteGroup() == nullptr) return;
 
 	IndustryTileResolverObject object(gfx, tile, ind, CBID_RANDOM_TRIGGER);
 	object.waiting_triggers = GetIndustryTriggers(tile) | trigger;
diff --git a/src/newgrf_object.cpp b/src/newgrf_object.cpp
index ff093f737d..5ba5f07d3d 100644
--- a/src/newgrf_object.cpp
+++ b/src/newgrf_object.cpp
@@ -378,8 +378,8 @@ ObjectResolverObject::ObjectResolverObject(const ObjectSpec *spec, Object *obj,
 		CallbackID callback, uint32_t param1, uint32_t param2)
 	: ResolverObject(spec->grf_prop.grffile, callback, param1, param2), object_scope(*this, obj, spec, tile, view)
 {
-	this->root_spritegroup = (obj == nullptr && spec->grf_prop.spritegroup[OBJECT_SPRITE_GROUP_PURCHASE] != nullptr) ?
-			spec->grf_prop.spritegroup[OBJECT_SPRITE_GROUP_PURCHASE] : spec->grf_prop.spritegroup[OBJECT_SPRITE_GROUP_DEFAULT];
+	this->root_spritegroup = (obj == nullptr) ? spec->grf_prop.GetSpriteGroup(OBJECT_SPRITE_GROUP_PURCHASE) : nullptr;
+	if (this->root_spritegroup == nullptr) this->root_spritegroup = spec->grf_prop.GetSpriteGroup(OBJECT_SPRITE_GROUP_DEFAULT);
 }
 
 /**
diff --git a/src/newgrf_object.h b/src/newgrf_object.h
index ca16df4c95..4b00bb6da9 100644
--- a/src/newgrf_object.h
+++ b/src/newgrf_object.h
@@ -58,7 +58,7 @@ DECLARE_INCREMENT_DECREMENT_OPERATORS(ObjectClassID)
  */
 struct ObjectSpec : NewGRFSpecBase<ObjectClassID> {
 	/* 2 because of the "normal" and "buy" sprite stacks. */
-	GRFFilePropsBase<2> grf_prop; ///< Properties related the the grf file
+	FixedGRFFileProps<2> grf_prop; ///< Properties related the the grf file
 	AnimationInfo animation;      ///< Information about the animation.
 	StringID name;                ///< The name for this object.
 
diff --git a/src/newgrf_roadstop.cpp b/src/newgrf_roadstop.cpp
index d858fdb7a5..853d2e8313 100644
--- a/src/newgrf_roadstop.cpp
+++ b/src/newgrf_roadstop.cpp
@@ -227,25 +227,31 @@ RoadStopResolverObject::RoadStopResolverObject(const RoadStopSpec *roadstopspec,
 	if (st == nullptr) {
 		/* No station, so we are in a purchase list */
 		ctype = SpriteGroupCargo::SG_PURCHASE;
+		this->root_spritegroup = roadstopspec->grf_prop.GetSpriteGroup(ctype);
 	} else if (Station::IsExpected(st)) {
 		const Station *station = Station::From(st);
 		/* Pick the first cargo that we have waiting */
-		for (const CargoSpec *cs : CargoSpec::Iterate()) {
-			if (roadstopspec->grf_prop.spritegroup[cs->Index()] != nullptr &&
-					station->goods[cs->Index()].HasData() && station->goods[cs->Index()].GetData().cargo.TotalCount() > 0) {
-				ctype = cs->Index();
+		for (const auto &[cargo, spritegroup] : roadstopspec->grf_prop.spritegroups) {
+			if (cargo < NUM_CARGO && station->goods[cargo].HasData() && station->goods[cargo].GetData().cargo.TotalCount() > 0) {
+				ctype = cargo;
+				this->root_spritegroup = spritegroup;
 				break;
 			}
 		}
+
+		if (this->root_spritegroup == nullptr) {
+			ctype = SpriteGroupCargo::SG_DEFAULT_NA;
+			this->root_spritegroup = roadstopspec->grf_prop.GetSpriteGroup(ctype);
+		}
 	}
 
-	if (roadstopspec->grf_prop.spritegroup[ctype] == nullptr) {
+	if (this->root_spritegroup == nullptr) {
 		ctype = SpriteGroupCargo::SG_DEFAULT;
+		this->root_spritegroup = roadstopspec->grf_prop.GetSpriteGroup(ctype);
 	}
 
 	/* Remember the cargo type we've picked */
 	this->roadstop_scope.cargo_type = ctype;
-	this->root_spritegroup = roadstopspec->grf_prop.spritegroup[ctype];
 }
 
 TownScopeResolver *RoadStopResolverObject::GetTown()
diff --git a/src/newgrf_roadstop.h b/src/newgrf_roadstop.h
index f4cc3c4eee..dd2e32bebf 100644
--- a/src/newgrf_roadstop.h
+++ b/src/newgrf_roadstop.h
@@ -140,7 +140,7 @@ struct RoadStopSpec : NewGRFSpecBase<RoadStopClassID> {
 	 * Used for obtaining the sprite offset of custom sprites, and for
 	 * evaluating callbacks.
 	 */
-	GRFFilePropsBase<NUM_CARGO + 3> grf_prop;
+	VariableGRFFileProps grf_prop;
 	StringID name;              ///< Name of this stop
 
 	RoadStopAvailabilityType stop_type = ROADSTOPTYPE_ALL;
diff --git a/src/newgrf_station.cpp b/src/newgrf_station.cpp
index b4575f7e33..97ceab6c02 100644
--- a/src/newgrf_station.cpp
+++ b/src/newgrf_station.cpp
@@ -582,25 +582,32 @@ StationResolverObject::StationResolverObject(const StationSpec *statspec, BaseSt
 	if (this->station_scope.st == nullptr) {
 		/* No station, so we are in a purchase list */
 		ctype = SpriteGroupCargo::SG_PURCHASE;
+		this->root_spritegroup = statspec->grf_prop.GetSpriteGroup(ctype);
 	} else if (Station::IsExpected(this->station_scope.st)) {
 		const Station *st = Station::From(this->station_scope.st);
 		/* Pick the first cargo that we have waiting */
-		for (const CargoSpec *cs : CargoSpec::Iterate()) {
-			if (this->station_scope.statspec->grf_prop.spritegroup[cs->Index()] != nullptr &&
-					st->goods[cs->Index()].HasData() && st->goods[cs->Index()].GetData().cargo.TotalCount() > 0) {
-				ctype = cs->Index();
+		for (const auto &[cargo, spritegroup] : statspec->grf_prop.spritegroups) {
+			if (cargo < NUM_CARGO && st->goods[cargo].HasData() && st->goods[cargo].GetData().cargo.TotalCount() > 0) {
+				ctype = cargo;
+				this->root_spritegroup = spritegroup;
 				break;
 			}
 		}
+
+		if (this->root_spritegroup == nullptr) {
+			ctype = SpriteGroupCargo::SG_DEFAULT_NA;
+			this->root_spritegroup = statspec->grf_prop.GetSpriteGroup(ctype);
+		}
 	}
 
-	if (this->station_scope.statspec->grf_prop.spritegroup[ctype] == nullptr) {
+
+	if (this->root_spritegroup == nullptr) {
 		ctype = SpriteGroupCargo::SG_DEFAULT;
+		this->root_spritegroup = statspec->grf_prop.GetSpriteGroup(ctype);
 	}
 
 	/* Remember the cargo type we've picked */
 	this->station_scope.cargo_type = ctype;
-	this->root_spritegroup = this->station_scope.statspec->grf_prop.spritegroup[this->station_scope.cargo_type];
 }
 
 /**
diff --git a/src/newgrf_station.h b/src/newgrf_station.h
index f8b442720f..40c6cee9e2 100644
--- a/src/newgrf_station.h
+++ b/src/newgrf_station.h
@@ -125,7 +125,7 @@ struct StationSpec : NewGRFSpecBase<StationClassID> {
 	 * Used for obtaining the sprite offset of custom sprites, and for
 	 * evaluating callbacks.
 	 */
-	GRFFilePropsBase<NUM_CARGO + 3> grf_prop;
+	VariableGRFFileProps grf_prop;
 	StringID name;             ///< Name of this station.
 
 	/**
diff --git a/src/station_cmd.cpp b/src/station_cmd.cpp
index 4881c7c9fa..d0f1635cbb 100644
--- a/src/station_cmd.cpp
+++ b/src/station_cmd.cpp
@@ -3130,7 +3130,7 @@ static void DrawTile_Station(TileInfo *ti)
 		gfx = GetAirportGfx(ti->tile);
 		if (gfx >= NEW_AIRPORTTILE_OFFSET) {
 			const AirportTileSpec *ats = AirportTileSpec::Get(gfx);
-			if (ats->grf_prop.spritegroup[0] != nullptr && DrawNewAirportTile(ti, Station::GetByTile(ti->tile), ats)) {
+			if (ats->grf_prop.GetSpriteGroup() != nullptr && DrawNewAirportTile(ti, Station::GetByTile(ti->tile), ats)) {
 				return;
 			}
 			/* No sprite group (or no valid one) found, meaning no graphics associated.
diff --git a/src/table/object_land.h b/src/table/object_land.h
index 46ce9c23e4..8bb1070566 100644
--- a/src/table/object_land.h
+++ b/src/table/object_land.h
@@ -121,7 +121,7 @@ static const DrawTileSpriteSpan _object_hq[] = {
 
 #undef TILE_SPRITE_LINE
 
-#define M(name, size, build_cost_multiplier, clear_cost_multiplier, height, climate, gen_amount, flags) {{INVALID_OBJECT_CLASS, 0}, GRFFilePropsBase<2>(), {0, 0, 0, 0}, name, climate, size, build_cost_multiplier, clear_cost_multiplier, TimerGameCalendar::Date{}, CalendarTime::MAX_DATE + 1, flags, ObjectCallbackMasks{}, height, 1, gen_amount}
+#define M(name, size, build_cost_multiplier, clear_cost_multiplier, height, climate, gen_amount, flags) {{INVALID_OBJECT_CLASS, 0}, FixedGRFFileProps<2>{}, {0, 0, 0, 0}, name, climate, size, build_cost_multiplier, clear_cost_multiplier, TimerGameCalendar::Date{}, CalendarTime::MAX_DATE + 1, flags, ObjectCallbackMasks{}, height, 1, gen_amount}
 
 /* Climates
  * T = Temperate
diff --git a/src/town_cmd.cpp b/src/town_cmd.cpp
index 8c1d4c0327..c3278a3b93 100644
--- a/src/town_cmd.cpp
+++ b/src/town_cmd.cpp
@@ -273,7 +273,7 @@ static void DrawTile_Town(TileInfo *ti)
 		/* Houses don't necessarily need new graphics. If they don't have a
 		 * spritegroup associated with them, then the sprite for the substitute
 		 * house id is drawn instead. */
-		if (HouseSpec::Get(house_id)->grf_prop.spritegroup[0] != nullptr) {
+		if (HouseSpec::Get(house_id)->grf_prop.GetSpriteGroup() != nullptr) {
 			DrawNewHouseTile(ti, house_id);
 			return;
 		} else {
@@ -334,7 +334,7 @@ static Foundation GetFoundation_Town(TileIndex tile, Slope tileh)
 	 */
 	if (hid >= NEW_HOUSE_OFFSET) {
 		const HouseSpec *hs = HouseSpec::Get(hid);
-		if (hs->grf_prop.spritegroup[0] != nullptr && hs->callback_mask.Test(HouseCallbackMask::DrawFoundations)) {
+		if (hs->grf_prop.GetSpriteGroup() != nullptr && hs->callback_mask.Test(HouseCallbackMask::DrawFoundations)) {
 			uint32_t callback_res = GetHouseCallback(CBID_HOUSE_DRAW_FOUNDATIONS, 0, 0, hid, Town::GetByTile(tile), tile);
 			if (callback_res != CALLBACK_FAILED && !ConvertBooleanCallback(hs->grf_prop.grffile, CBID_HOUSE_DRAW_FOUNDATIONS, callback_res)) return FOUNDATION_NONE;
 		}
diff --git a/src/town_gui.cpp b/src/town_gui.cpp
index a508e5d15e..b0701b8d96 100644
--- a/src/town_gui.cpp
+++ b/src/town_gui.cpp
@@ -1410,7 +1410,7 @@ void DrawHouseInGUI(int x, int y, HouseID house_id, int view)
 			 * spritegroup associated with them, then the sprite for the substitute
 			 * house id is drawn instead. */
 			const HouseSpec *spec = HouseSpec::Get(house_id);
-			if (spec->grf_prop.spritegroup[0] != nullptr) {
+			if (spec->grf_prop.GetSpriteGroup() != nullptr) {
 				DrawNewHouseTileInGUI(x, y, spec, house_id, view);
 				return;
 			} else {