From 1d3a003af209a932f83bec38033226c1a294e740 Mon Sep 17 00:00:00 2001
From: Victor Aguilera Diaz <victorramonaguileradiaz@gmail.com>
Date: Sun, 19 Jul 2026 21:45:20 +0200
Subject: [PATCH] Do not renovate a city building into one that will not fit
 under an elevated way

way_builder_t::is_allowed_step refuses to build an elevated way over a building
that draws above the ground storey. stadt_t::renovate_city_building applied no
such test, so a short building under an existing elevated way could be renovated
into exactly the building the way could never have been built over, leaving the
two overlapping on screen. Reported by makie, forum 23991.

The test was spelled out inline in the way builder as

    gb->get_tile()->get_background(0,1,0) != IMG_EMPTY

Two independent notions of "too tall" drifting apart is what allowed this, so it
becomes one named predicate, building_tile_desc_t::has_upper_storey(), with a
building_desc_t counterpart that asks it of every tile. The way builder calls it
instead of open-coding it; behaviour there is unchanged.

Renovation passes a flag into the candidate search, beside the existing climate,
availability, size and exclusion filters. Choosing rather than forbidding
matters: the block keeps renovating and simply stops growing upwards once the
pakset has nothing suitable left, instead of being frozen at whatever it was
when the way went up. That needs no new code - desc_at_least is only assigned
inside the filtered branch, so when nothing qualifies the search returns NULL and
renovate_city_building takes its existing early return.

Scope is monorailboden only, which is the elevated-way ground. Any waytype, so
elevated rail, monorail, maglev and tram are covered - verified by running the
reproduction with an elevated rail way as well as a road, same result. NOT
brueckenboden: the bridge builder never tested building height, so tall buildings
under a bridge have always been legal and the asymmetry argument does not apply
there. Extending this to bridges would be a separate decision.

Measured on pak128 2.10.1, city grown under an elevated way, run to 1990:

    before   219 renovations under it,  93 too tall
    after    167 renovations under it,   0 too tall

The block still develops - it reached level 52 of the pakset's 59 - so nothing is
frozen. Upstream test suite 201/201.
---
 src/simutrans/builder/hausbauer.cc       | 20 +++++++++++-------
 src/simutrans/builder/hausbauer.h        |  6 +++---
 src/simutrans/builder/wegbauer.cc        |  2 +-
 src/simutrans/descriptor/building_desc.h | 24 +++++++++++++++++++++
 src/simutrans/world/simcity.cc           | 27 +++++++++++++++++++++---
 5 files changed, 64 insertions(+), 15 deletions(-)

diff --git a/src/simutrans/builder/hausbauer.cc b/src/simutrans/builder/hausbauer.cc
index 1bc93c894..5797f7a3c 100644
--- a/src/simutrans/builder/hausbauer.cc
+++ b/src/simutrans/builder/hausbauer.cc
@@ -886,7 +886,7 @@ const building_desc_t* hausbauer_t::get_special(uint32 bev, building_desc_t::bty
  * @param start_level the minimum level of the house/station
  * @param cl allowed climates
  */
-static const building_desc_t* get_city_building_from_list(const vector_tpl<const building_desc_t*>& list, int start_level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude )
+static const building_desc_t* get_city_building_from_list(const vector_tpl<const building_desc_t*>& list, int start_level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude, bool no_upper_storey )
 {
 	weighted_vector_tpl<const building_desc_t *> selections(16);
 	int level = start_level;
@@ -910,7 +910,11 @@ static const building_desc_t* get_city_building_from_list(const vector_tpl<const
 		     desc->is_available(time)  &&
 		     // size check
 			(desc->get_area()>=minsize  &&  desc->get_area() <= maxsize)  &&
-			(!exclude  ||  !exclude->is_contained(desc))
+			(!exclude  ||  !exclude->is_contained(desc))  &&
+			// an elevated way overhead: the replacement must fit under it, by
+			// the same test that stopped the way being built over a tall
+			// building in the first place (way_builder_t::is_allowed_step)
+			(!no_upper_storey  ||  !desc->has_upper_storey())
 		) {
 			desc_at_least = desc;
 			if( thislevel == level ) {
@@ -946,21 +950,21 @@ static const building_desc_t* get_city_building_from_list(const vector_tpl<const
 }
 
 
-const building_desc_t* hausbauer_t::get_commercial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude)
+const building_desc_t* hausbauer_t::get_commercial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude, bool no_upper_storey)
 {
-	return get_city_building_from_list(city_commercial, level, time, cl, clusters, minsize, maxsize, exclude );
+	return get_city_building_from_list(city_commercial, level, time, cl, clusters, minsize, maxsize, exclude, no_upper_storey );
 }
 
 
-const building_desc_t* hausbauer_t::get_industrial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude)
+const building_desc_t* hausbauer_t::get_industrial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude, bool no_upper_storey)
 {
-	return get_city_building_from_list(city_industry, level, time, cl, clusters, minsize, maxsize, exclude );
+	return get_city_building_from_list(city_industry, level, time, cl, clusters, minsize, maxsize, exclude, no_upper_storey );
 }
 
 
-const building_desc_t* hausbauer_t::get_residential(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*> *exclude)
+const building_desc_t* hausbauer_t::get_residential(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*> *exclude, bool no_upper_storey)
 {
-	return get_city_building_from_list(city_residential, level, time, cl, clusters, minsize, maxsize, exclude );
+	return get_city_building_from_list(city_residential, level, time, cl, clusters, minsize, maxsize, exclude, no_upper_storey );
 }
 
 
diff --git a/src/simutrans/builder/hausbauer.h b/src/simutrans/builder/hausbauer.h
index e2bba9177..43a887544 100644
--- a/src/simutrans/builder/hausbauer.h
+++ b/src/simutrans/builder/hausbauer.h
@@ -85,13 +85,13 @@ public:
 	static void fill_menu(tool_selector_t* tool_selector, building_desc_t::btype, waytype_t wt, sint16 sound_ok);
 
 	/// @returns a random commercial building matching the requirements.
-	static const building_desc_t* get_commercial(int level, uint16 time, climate c, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL);
+	static const building_desc_t* get_commercial(int level, uint16 time, climate c, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL, bool no_upper_storey = false);
 
 	/// @returns a random industrial building matching the requirements.
-	static const building_desc_t* get_industrial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL);
+	static const building_desc_t* get_industrial(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL, bool no_upper_storey = false);
 
 	/// @returns a random residential building matching the requirements.
-	static const building_desc_t* get_residential(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL);
+	static const building_desc_t* get_residential(int level, uint16 time, climate cl, uint32 clusters, sint16 minsize, sint16 maxsize, vector_tpl<const building_desc_t*>* exclude = NULL, bool no_upper_storey = false);
 
 	/// @returns headquarters with level @p level (takes the first matching one)
 	static const building_desc_t* get_headquarters(int level, uint16 time);
diff --git a/src/simutrans/builder/wegbauer.cc b/src/simutrans/builder/wegbauer.cc
index ad08374b8..ae45d7ae1 100644
--- a/src/simutrans/builder/wegbauer.cc
+++ b/src/simutrans/builder/wegbauer.cc
@@ -606,7 +606,7 @@ bool way_builder_t::is_allowed_step(const grund_t *from, const grund_t *to, sint
 			if(gb) {
 				// no halt => citybuilding => do not touch
 				// also check for too high buildings ...
-				if(!check_owner(gb->get_owner(),player_builder)  ||  gb->get_tile()->get_background(0,1,0) != IMG_EMPTY) {
+				if(!check_owner(gb->get_owner(),player_builder)  ||  gb->get_tile()->has_upper_storey()) {
 					return false;
 				}
 				// building above houses is expensive ... avoid it!
diff --git a/src/simutrans/descriptor/building_desc.h b/src/simutrans/descriptor/building_desc.h
index 9130a875d..7a656d51e 100644
--- a/src/simutrans/descriptor/building_desc.h
+++ b/src/simutrans/descriptor/building_desc.h
@@ -50,6 +50,15 @@ public:
 		return get_background(0,0,0)!=IMG_EMPTY  ||  get_foreground(0,0)!=IMG_EMPTY;
 	}
 
+	/**
+	 * Does this tile draw anything above the ground storey?
+	 * This is the test that decides whether an elevated way may pass over the
+	 * building, so it lives here rather than being spelled out at each use.
+	 */
+	bool has_upper_storey() const {
+		return get_background(0, 1, 0) != IMG_EMPTY;
+	}
+
 	image_id get_background(uint16 phase, uint16 height, uint8 season) const
 	{
 		image_array_t const* const imglist = get_child<image_array_t>(0 + 2 * season);
@@ -287,6 +296,21 @@ public:
 
 	const building_tile_desc_t *get_tile(uint8 layout, sint16 x, sint16 y) const;
 
+	/**
+	 * Does any tile of this building draw above the ground storey?
+	 * An elevated way cannot pass over such a building (see way_builder_t),
+	 * so a replacement chosen for a spot that already has one overhead must
+	 * not have an upper storey either.
+	 */
+	bool has_upper_storey() const {
+		for(  uint16 i = 0;  i < layouts * size.x * size.y;  i++  ) {
+			if(  get_tile(i)->has_upper_storey()  ) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	// returns true,if building can be rotated
 	bool can_rotate() const {
 		if(size.x!=size.y  &&  layouts==1) {
diff --git a/src/simutrans/world/simcity.cc b/src/simutrans/world/simcity.cc
index 14eb83e9f..5c881234e 100644
--- a/src/simutrans/world/simcity.cc
+++ b/src/simutrans/world/simcity.cc
@@ -3505,11 +3505,32 @@ bool stadt_t::renovate_city_building(gebaeude_t *gb)
 	building_desc_t::btype want_to_have = building_desc_t::unknown;
 	int sum = 0;
 
+	// An elevated way over this tile is only there because the building below
+	// was short enough to allow it (way_builder_t::is_allowed_step). Renovating
+	// into something taller would produce a building the player could never
+	// have built the way over, so the replacement has to pass the same test.
+	// If nothing does, no renovation happens and the block simply stops growing
+	// upwards, which is the intended ceiling rather than a failure.
+	//
+	// monorailboden only, which is the elevated-way ground - any waytype, so
+	// elevated rail, monorail, maglev and tram are all covered. NOT
+	// brueckenboden: the bridge builder never tested building height, so tall
+	// buildings under a bridge have always been legal and the argument above
+	// does not apply to them. Changing that would be a separate decision.
+	bool no_upper_storey = false;
+	if(  const grund_t *gr_here = welt->lookup_kartenboden(k)  ) {
+		const grund_t *above = welt->lookup(gr_here->get_pos()
+			+ koord3d(0, 0, welt->get_settings().get_way_height_clearance()));
+		no_upper_storey = above
+			&&  above->get_typ() == grund_t::monorailboden
+			&&  above->get_weg_nr(0) != NULL;
+	}
+
 	// try to build
 	const building_desc_t* h = NULL;
 	if (sum_commercial > sum_industrial && sum_commercial > sum_residential) {
 		// we must check, if we can really update to higher level ...
-		h = hausbauer_t::get_commercial(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_commercial(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, no_upper_storey);
 		if(  h != NULL  &&  h->get_level() >= level+1  ) {
 			want_to_have = building_desc_t::city_com;
 			sum = sum_commercial;
@@ -3519,7 +3540,7 @@ bool stadt_t::renovate_city_building(gebaeude_t *gb)
 	if(    (sum_industrial > sum_commercial  &&  sum_industrial > sum_residential) ||
 	       (sum_commercial > sum_residential  &&  want_to_have == building_desc_t::unknown)  ) {
 		// we must check, if we can really update to higher level ...
-		h = hausbauer_t::get_industrial(level+1 , current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_industrial(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, no_upper_storey);
 		if(  h != NULL  &&  h->get_level() >= level+1  ) {
 			want_to_have = building_desc_t::city_ind;
 			sum = sum_industrial;
@@ -3529,7 +3550,7 @@ bool stadt_t::renovate_city_building(gebaeude_t *gb)
 	// (sum_wohnung>sum_industrie  &&  sum_wohnung>sum_gewerbe
 	if(  want_to_have == building_desc_t::unknown  ) {
 		// we must check, if we can really update to higher level ...
-		h = hausbauer_t::get_residential(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc);
+		h = hausbauer_t::get_residential(level+1, current_month, cl, neighbor_building_clusters, 1, max_area, &exclude_desc, no_upper_storey);
 		if(  h != NULL  &&  h->get_level() >= level+1  ) {
 			want_to_have = building_desc_t::city_res;
 			sum = sum_residential;
-- 
2.55.0.windows.2

