From e7c227aea9be790865289a5c5200d1c09d5a8ce3 Mon Sep 17 00:00:00 2001
From: Marko Lindqvist <cazfi74@gmail.com>
Date: Tue, 10 Jun 2025 09:28:00 +0300
Subject: [PATCH 75/96] Add MaxTopUnitsOnTile requirement type

The difference to old MaxUnitsOnTile is that transported units
are not counted, only topmost transporters.

Some internal namings related to MaxUnitsOnTile changed to
contain word "total"

See RM #1505

Signed-off-by: Marko Lindqvist <cazfi74@gmail.com>
---
 ai/default/daieffects.c              |   3 +-
 ai/default/daimilitary.c             |   3 +-
 common/fc_types.h                    |   3 +-
 common/metaknowledge.c               |   3 +-
 common/oblig_reqs.c                  |   2 +-
 common/reqtext.c                     | 124 ++++++++++++++----
 common/requirements.c                | 185 +++++++++++++++++++++++----
 doc/README.effects                   | 133 +++++++++----------
 gen_headers/enums/fc_types_enums.def |   3 +-
 server/cityturn.c                    |  47 ++++++-
 server/ruleset/rssanity.c            |   3 +-
 tools/ruledit/univ_value.c           |  10 +-
 12 files changed, 384 insertions(+), 135 deletions(-)

diff --git a/ai/default/daieffects.c b/ai/default/daieffects.c
index 550fcba18a..5d09c3ac25 100644
--- a/ai/default/daieffects.c
+++ b/ai/default/daieffects.c
@@ -964,7 +964,8 @@ bool dai_can_requirement_be_met_in_city(const struct requirement *preq,
   case VUT_DIPLREL_TILE_O:
   case VUT_DIPLREL_UNITANY:
   case VUT_DIPLREL_UNITANY_O:
-  case VUT_MAXTILEUNITS:
+  case VUT_MAXTILETOTALUNITS:
+  case VUT_MAXTILETOPUNITS:
   case VUT_STYLE:
   case VUT_UNITSTATE:
   case VUT_ACTIVITY:
diff --git a/ai/default/daimilitary.c b/ai/default/daimilitary.c
index fb7e497a4e..d92d219fb7 100644
--- a/ai/default/daimilitary.c
+++ b/ai/default/daimilitary.c
@@ -385,7 +385,8 @@ tactical_req_cb(const struct req_context *context,
   case VUT_UNITSTATE:
   case VUT_ACTIVITY:
   case VUT_MINSIZE:
-  case VUT_MAXTILEUNITS:
+  case VUT_MAXTILETOTALUNITS:
+  case VUT_MAXTILETOPUNITS:
   case VUT_MINHP:
   case VUT_MINMOVES:
   case VUT_COUNTER:
diff --git a/common/fc_types.h b/common/fc_types.h
index bfa2c07d28..268dccb787 100644
--- a/common/fc_types.h
+++ b/common/fc_types.h
@@ -569,7 +569,8 @@ typedef union {
   enum impr_flag_id impr_flag;
   enum plr_flag_id plr_flag;
   int minmoves;
-  int max_tile_units;
+  int max_tile_total_units;
+  int max_tile_top_units;
   int minveteran;
   int min_hit_points;
   int age;
diff --git a/common/metaknowledge.c b/common/metaknowledge.c
index 41f5e9319a..a61192d7e8 100644
--- a/common/metaknowledge.c
+++ b/common/metaknowledge.c
@@ -592,7 +592,8 @@ static bool is_req_knowable(const struct player *pov_player,
     }
   }
 
-  if (req->source.kind == VUT_MAXTILEUNITS) {
+  if (req->source.kind == VUT_MAXTILETOTALUNITS
+      || req->source.kind == VUT_MAXTILETOPUNITS) {
     if (context->tile == nullptr) {
       /* The tile may exist but not be passed when the problem type is
        * RPT_POSSIBLE. */
diff --git a/common/oblig_reqs.c b/common/oblig_reqs.c
index 7b633d60bd..16f5802eeb 100644
--- a/common/oblig_reqs.c
+++ b/common/oblig_reqs.c
@@ -623,7 +623,7 @@ void hard_code_oblig_hard_reqs(void)
    *    conquer a city belonging to someone they were at war with.
    * Conclusion: the conquered city had to be empty.
    */
-  oblig_hard_req_register(req_from_values(VUT_MAXTILEUNITS, REQ_RANGE_TILE,
+  oblig_hard_req_register(req_from_values(VUT_MAXTILETOTALUNITS, REQ_RANGE_TILE,
                                           FALSE, FALSE, TRUE, 0),
                           TRUE,
                           N_("All action enablers for %s must require"
diff --git a/common/reqtext.c b/common/reqtext.c
index 1965ce6fba..312013de48 100644
--- a/common/reqtext.c
+++ b/common/reqtext.c
@@ -2279,24 +2279,24 @@ bool req_text_insert(char *buf, size_t bufsz, struct player *pplayer,
     }
     break;
 
-  case VUT_MAXTILEUNITS:
+  case VUT_MAXTILETOTALUNITS:
     switch (preq->range) {
     case REQ_RANGE_TILE:
       fc_strlcat(buf, prefix, bufsz);
       if (preq->present) {
         cat_snprintf(buf, bufsz,
-                     PL_("At most %d unit may be present on the tile.",
-                         "At most %d units may be present on the tile.",
-                         preq->source.value.max_tile_units),
-                     preq->source.value.max_tile_units);
+                     PL_("At most %d total unit may be present on the tile.",
+                         "At most %d total units may be present on the tile.",
+                         preq->source.value.max_tile_total_units),
+                     preq->source.value.max_tile_total_units);
       } else {
         cat_snprintf(buf, bufsz,
-                     PL_("There must be more than %d unit present on "
+                     PL_("There must be more than %d total unit present on "
                          "the tile.",
-                         "There must be more than %d units present on "
+                         "There must be more than %d total units present on "
                          "the tile.",
-                         preq->source.value.max_tile_units),
-                     preq->source.value.max_tile_units);
+                         preq->source.value.max_tile_total_units),
+                     preq->source.value.max_tile_total_units);
       }
       return TRUE;
     case REQ_RANGE_CADJACENT:
@@ -2304,19 +2304,19 @@ bool req_text_insert(char *buf, size_t bufsz, struct player *pplayer,
       if (preq->present) {
         cat_snprintf(buf, bufsz,
                      PL_("The tile or at least one cardinally adjacent tile "
-                         "must have %d unit or fewer.",
+                         "must have %d total unit or fewer.",
                          "The tile or at least one cardinally adjacent tile "
-                         "must have %d units or fewer.",
-                         preq->source.value.max_tile_units),
-                     preq->source.value.max_tile_units);
+                         "must have %d total units or fewer.",
+                         preq->source.value.max_tile_total_units),
+                     preq->source.value.max_tile_total_units);
       } else {
         cat_snprintf(buf, bufsz,
                      PL_("The tile and all cardinally adjacent tiles must "
-                         "have more than %d unit each.",
+                         "have more than %d total unit each.",
                          "The tile and all cardinally adjacent tiles must "
-                         "have more than %d units each.",
-                         preq->source.value.max_tile_units),
-                     preq->source.value.max_tile_units);
+                         "have more than %d total units each.",
+                         preq->source.value.max_tile_total_units),
+                     preq->source.value.max_tile_total_units);
       }
       return TRUE;
     case REQ_RANGE_ADJACENT:
@@ -2324,19 +2324,93 @@ bool req_text_insert(char *buf, size_t bufsz, struct player *pplayer,
       if (preq->present) {
         cat_snprintf(buf, bufsz,
                      PL_("The tile or at least one adjacent tile must have "
-                         "%d unit or fewer.",
+                         "%d total unit or fewer.",
                          "The tile or at least one adjacent tile must have "
-                         "%d units or fewer.",
-                         preq->source.value.max_tile_units),
-                     preq->source.value.max_tile_units);
+                         "%d total units or fewer.",
+                         preq->source.value.max_tile_total_units),
+                     preq->source.value.max_tile_total_units);
       } else {
         cat_snprintf(buf, bufsz,
                      PL_("The tile and all adjacent tiles must have more "
-                         "than %d unit each.",
+                         "than %d total unit each.",
                          "The tile and all adjacent tiles must have more "
-                         "than %d units each.",
-                         preq->source.value.max_tile_units),
-                     preq->source.value.max_tile_units);
+                         "than %d total units each.",
+                         preq->source.value.max_tile_total_units),
+                     preq->source.value.max_tile_total_units);
+      }
+      return TRUE;
+    case REQ_RANGE_CITY:
+    case REQ_RANGE_TRADE_ROUTE:
+    case REQ_RANGE_CONTINENT:
+    case REQ_RANGE_PLAYER:
+    case REQ_RANGE_TEAM:
+    case REQ_RANGE_ALLIANCE:
+    case REQ_RANGE_WORLD:
+    case REQ_RANGE_LOCAL:
+    case REQ_RANGE_COUNT:
+      /* Not supported. */
+      break;
+    }
+    break;
+
+  case VUT_MAXTILETOPUNITS:
+    switch (preq->range) {
+    case REQ_RANGE_TILE:
+      fc_strlcat(buf, prefix, bufsz);
+      if (preq->present) {
+        cat_snprintf(buf, bufsz,
+                     PL_("At most %d untransported unit may be present on the tile.",
+                         "At most %d untransported units may be present on the tile.",
+                         preq->source.value.max_tile_top_units),
+                     preq->source.value.max_tile_top_units);
+      } else {
+        cat_snprintf(buf, bufsz,
+                     PL_("There must be more than %d untransported unit present on "
+                         "the tile.",
+                         "There must be more than %d untransported units present on "
+                         "the tile.",
+                         preq->source.value.max_tile_top_units),
+                     preq->source.value.max_tile_top_units);
+      }
+      return TRUE;
+    case REQ_RANGE_CADJACENT:
+      fc_strlcat(buf, prefix, bufsz);
+      if (preq->present) {
+        cat_snprintf(buf, bufsz,
+                     PL_("The tile or at least one cardinally adjacent tile "
+                         "must have %d untransporte unit or fewer.",
+                         "The tile or at least one cardinally adjacent tile "
+                         "must have %d untransporte units or fewer.",
+                         preq->source.value.max_tile_top_units),
+                     preq->source.value.max_tile_top_units);
+      } else {
+        cat_snprintf(buf, bufsz,
+                     PL_("The tile and all cardinally adjacent tiles must "
+                         "have more than %d untransported unit each.",
+                         "The tile and all cardinally adjacent tiles must "
+                         "have more than %d untransported units each.",
+                         preq->source.value.max_tile_top_units),
+                     preq->source.value.max_tile_top_units);
+      }
+      return TRUE;
+    case REQ_RANGE_ADJACENT:
+      fc_strlcat(buf, prefix, bufsz);
+      if (preq->present) {
+        cat_snprintf(buf, bufsz,
+                     PL_("The tile or at least one adjacent tile must have "
+                         "%d untransported unit or fewer.",
+                         "The tile or at least one adjacent tile must have "
+                         "%d untransported units or fewer.",
+                         preq->source.value.max_tile_top_units),
+                     preq->source.value.max_tile_top_units);
+      } else {
+        cat_snprintf(buf, bufsz,
+                     PL_("The tile and all adjacent tiles must have "
+                         "more than %d untransported unit each.",
+                         "The tile and all adjacent tiles must have "
+                         "more than %d untransported units each.",
+                         preq->source.value.max_tile_top_units),
+                     preq->source.value.max_tile_top_units);
       }
       return TRUE;
     case REQ_RANGE_CITY:
diff --git a/common/requirements.c b/common/requirements.c
index db6b863ab6..5ca4dd30ac 100644
--- a/common/requirements.c
+++ b/common/requirements.c
@@ -527,9 +527,15 @@ void universal_value_from_str(struct universal *source, const char *value)
       return;
     }
     break;
-  case VUT_MAXTILEUNITS:
-    source->value.max_tile_units = atoi(value);
-    if (0 <= source->value.max_tile_units) {
+  case VUT_MAXTILETOTALUNITS:
+    source->value.max_tile_total_units = atoi(value);
+    if (0 <= source->value.max_tile_total_units) {
+      return;
+    }
+    break;
+  case VUT_MAXTILETOPUNITS:
+    source->value.max_tile_top_units = atoi(value);
+    if (0 <= source->value.max_tile_top_units) {
       return;
     }
     break;
@@ -828,8 +834,11 @@ struct universal universal_by_number(const enum universals_n kind,
   case VUT_AI_LEVEL:
     source.value.ai_level = value;
     return source;
-  case VUT_MAXTILEUNITS:
-    source.value.max_tile_units = value;
+  case VUT_MAXTILETOTALUNITS:
+    source.value.max_tile_total_units = value;
+    return source;
+  case VUT_MAXTILETOPUNITS:
+    source.value.max_tile_top_units = value;
     return source;
   case VUT_TERRAINCLASS:
     source.value.terrainclass = value;
@@ -1006,8 +1015,10 @@ int universal_number(const struct universal *source)
     return source->value.minforeignpct;
   case VUT_AI_LEVEL:
     return source->value.ai_level;
-  case VUT_MAXTILEUNITS:
-    return source->value.max_tile_units;
+  case VUT_MAXTILETOTALUNITS:
+    return source->value.max_tile_total_units;
+  case VUT_MAXTILETOPUNITS:
+    return source->value.max_tile_top_units;
   case VUT_TERRAINCLASS:
     return source->value.terrainclass;
    case VUT_ROADFLAG:
@@ -1153,7 +1164,8 @@ struct requirement req_from_str(const char *type, const char *range,
       case VUT_TERRAINCLASS:
       case VUT_TERRAINALTER:
       case VUT_CITYTILE:
-      case VUT_MAXTILEUNITS:
+      case VUT_MAXTILETOTALUNITS:
+      case VUT_MAXTILETOPUNITS:
       case VUT_MINLATITUDE:
       case VUT_MAXLATITUDE:
       case VUT_MAX_DISTANCE_SQ:
@@ -1330,7 +1342,8 @@ struct requirement req_from_str(const char *type, const char *range,
       invalid = (req.range != REQ_RANGE_TILE);
       break;
     case VUT_CITYTILE:
-    case VUT_MAXTILEUNITS:
+    case VUT_MAXTILETOTALUNITS:
+    case VUT_MAXTILETOPUNITS:
       invalid = (req.range != REQ_RANGE_TILE
                  && req.range != REQ_RANGE_CADJACENT
                  && req.range != REQ_RANGE_ADJACENT);
@@ -1470,7 +1483,8 @@ struct requirement req_from_str(const char *type, const char *range,
     case VUT_DIPLREL_TILE_O:
     case VUT_DIPLREL_UNITANY:
     case VUT_DIPLREL_UNITANY_O:
-    case VUT_MAXTILEUNITS:
+    case VUT_MAXTILETOTALUNITS:
+    case VUT_MAXTILETOPUNITS:
     case VUT_MINTECHS:
     case VUT_FUTURETECHS:
     case VUT_MINCITIES:
@@ -2881,24 +2895,24 @@ is_minforeignpct_req_active(const struct civ_map *nmap,
 }
 
 /**********************************************************************//**
-  Determine whether a maximum units on tile requirement is satisfied in a
-  given context, ignoring parts of the requirement that can be handled
+  Determine whether a maximum total units on tile requirement is satisfied in
+  a given context, ignoring parts of the requirement that can be handled
   uniformly for all requirement types.
 
   context, other_context and req must not be null,
   and req must be a maxunitsontile requirement
 **************************************************************************/
 static enum fc_tristate
-is_maxunitsontile_req_active(const struct civ_map *nmap,
-                             const struct req_context *context,
-                             const struct req_context *other_context,
-                             const struct requirement *req)
+is_maxtotalunitsontile_req_active(const struct civ_map *nmap,
+                                  const struct req_context *context,
+                                  const struct req_context *other_context,
+                                  const struct requirement *req)
 {
   int max_units;
 
-  IS_REQ_ACTIVE_VARIANT_ASSERT(VUT_MAXTILEUNITS);
+  IS_REQ_ACTIVE_VARIANT_ASSERT(VUT_MAXTILETOTALUNITS);
 
-  max_units = req->source.value.max_tile_units;
+  max_units = req->source.value.max_tile_total_units;
 
   /* TODO: If can't see V_INVIS -> TRI_MAYBE */
   switch (req->range) {
@@ -2950,6 +2964,109 @@ is_maxunitsontile_req_active(const struct civ_map *nmap,
   return TRI_MAYBE;
 }
 
+
+/**********************************************************************//**
+  Determine whether a maximum top units on tile requirement is satisfied in
+  a given context, ignoring parts of the requirement that can be handled
+  uniformly for all requirement types.
+
+  context, other_context and req must not be null,
+  and req must be a maxunitsontile requirement
+**************************************************************************/
+static enum fc_tristate
+is_maxtopunitsontile_req_active(const struct civ_map *nmap,
+                                const struct req_context *context,
+                                const struct req_context *other_context,
+                                const struct requirement *req)
+{
+  int max_units;
+  int count;
+
+  IS_REQ_ACTIVE_VARIANT_ASSERT(VUT_MAXTILETOPUNITS);
+
+  max_units = req->source.value.max_tile_top_units;
+
+  /* TODO: If can't see V_INVIS -> TRI_MAYBE */
+  switch (req->range) {
+  case REQ_RANGE_TILE:
+    if (!context->tile) {
+      return TRI_MAYBE;
+    }
+    count = 0;
+    unit_list_iterate(context->tile->units, punit) {
+      if (!unit_transported(punit)) {
+        count++;
+      }
+    } unit_list_iterate_end;
+    return BOOL_TO_TRISTATE(count <= max_units);
+  case REQ_RANGE_CADJACENT:
+    if (!context->tile) {
+      return TRI_MAYBE;
+    }
+    count = 0;
+    unit_list_iterate(context->tile->units, punit) {
+      if (!unit_transported(punit)) {
+        count++;
+      }
+    } unit_list_iterate_end;
+    if (count <= max_units) {
+      return TRI_YES;
+    }
+    cardinal_adjc_iterate(nmap, context->tile, adjc_tile) {
+      count = 0;
+      unit_list_iterate(adjc_tile->units, punit) {
+        if (!unit_transported(punit)) {
+          count++;
+        }
+      } unit_list_iterate_end;
+      if (count <= max_units) {
+        return TRI_YES;
+      }
+    } cardinal_adjc_iterate_end;
+
+    return TRI_NO;
+  case REQ_RANGE_ADJACENT:
+    if (!context->tile) {
+      return TRI_MAYBE;
+    }
+    count = 0;
+    unit_list_iterate(context->tile->units, punit) {
+      if (!unit_transported(punit)) {
+        count++;
+      }
+    } unit_list_iterate_end;
+    if (count <= max_units) {
+      return TRI_YES;
+    }
+    adjc_iterate(nmap, context->tile, adjc_tile) {
+      count = 0;
+      unit_list_iterate(adjc_tile->units, punit) {
+        if (!unit_transported(punit)) {
+          count++;
+        }
+      } unit_list_iterate_end;
+      if (count <= max_units) {
+        return TRI_YES;
+      }
+    } adjc_iterate_end;
+    return TRI_NO;
+  case REQ_RANGE_CITY:
+  case REQ_RANGE_TRADE_ROUTE:
+  case REQ_RANGE_CONTINENT:
+  case REQ_RANGE_PLAYER:
+  case REQ_RANGE_TEAM:
+  case REQ_RANGE_ALLIANCE:
+  case REQ_RANGE_WORLD:
+  case REQ_RANGE_LOCAL:
+  case REQ_RANGE_COUNT:
+    break;
+  }
+
+  fc_assert_msg(FALSE, "Invalid range %d.", req->range);
+
+  return TRI_MAYBE;
+}
+
 /**********************************************************************//**
   Determine whether an extra requirement is satisfied in a given context,
   ignoring parts of the requirement that can be handled uniformly for all
@@ -6310,7 +6427,8 @@ static struct req_def req_definitions[VUT_COUNT] = {
   [VUT_MAX_DISTANCE_SQ] = {is_max_distance_sq_req_active, REQUCH_YES},
   [VUT_MAX_REGION_TILES] = {is_max_region_tiles_req_active, REQUCH_NO},
   [VUT_MAXLATITUDE] = {is_latitude_req_active, REQUCH_YES},
-  [VUT_MAXTILEUNITS] = {is_maxunitsontile_req_active, REQUCH_NO},
+  [VUT_MAXTILETOTALUNITS] = {is_maxtotalunitsontile_req_active, REQUCH_NO},
+  [VUT_MAXTILETOPUNITS] = {is_maxtopunitsontile_req_active, REQUCH_NO},
   [VUT_MINCALFRAG] = {is_mincalfrag_req_active, REQUCH_NO},
   [VUT_MINCULTURE] = {is_minculture_req_active, REQUCH_NO},
   [VUT_MINFOREIGNPCT] = {is_minforeignpct_req_active, REQUCH_NO},
@@ -6864,7 +6982,8 @@ bool universal_never_there(const struct universal *source)
   case VUT_DIPLREL_TILE_O:
   case VUT_DIPLREL_UNITANY:
   case VUT_DIPLREL_UNITANY_O:
-  case VUT_MAXTILEUNITS:
+  case VUT_MAXTILETOTALUNITS:
+  case VUT_MAXTILETOPUNITS:
   case VUT_UTYPE:
   case VUT_UCLASS:
   case VUT_MINVETERAN:
@@ -7536,8 +7655,10 @@ bool are_universals_equal(const struct universal *psource1,
     return psource1->value.minforeignpct == psource2->value.minforeignpct;
   case VUT_AI_LEVEL:
     return psource1->value.ai_level == psource2->value.ai_level;
-  case VUT_MAXTILEUNITS:
-    return psource1->value.max_tile_units == psource2->value.max_tile_units;
+  case VUT_MAXTILETOTALUNITS:
+    return psource1->value.max_tile_total_units == psource2->value.max_tile_total_units;
+  case VUT_MAXTILETOPUNITS:
+    return psource1->value.max_tile_top_units == psource2->value.max_tile_top_units;
   case VUT_TERRAINCLASS:
     return psource1->value.terrainclass == psource2->value.terrainclass;
   case VUT_ROADFLAG:
@@ -7718,8 +7839,11 @@ const char *universal_rule_name(const struct universal *psource)
     return buffer;
   case VUT_AI_LEVEL:
     return ai_level_name(psource->value.ai_level);
-  case VUT_MAXTILEUNITS:
-    fc_snprintf(buffer, sizeof(buffer), "%d", psource->value.max_tile_units);
+  case VUT_MAXTILETOTALUNITS:
+    fc_snprintf(buffer, sizeof(buffer), "%d", psource->value.max_tile_total_units);
+    return buffer;
+  case VUT_MAXTILETOPUNITS:
+    fc_snprintf(buffer, sizeof(buffer), "%d", psource->value.max_tile_top_units);
     return buffer;
   case VUT_TERRAINCLASS:
     return terrain_class_name(psource->value.terrainclass);
@@ -7993,11 +8117,18 @@ const char *universal_name_translation(const struct universal *psource,
     cat_snprintf(buf, bufsz, _("%s AI"),
                  ai_level_translated_name(psource->value.ai_level)); /* FIXME */
     return buf;
-  case VUT_MAXTILEUNITS:
+  case VUT_MAXTILETOTALUNITS:
+    /* TRANS: here <= means 'less than or equal' */
+    cat_snprintf(buf, bufsz, PL_("<=%d total unit",
+                                 "<=%d total units",
+                                 psource->value.max_tile_total_units),
+                 psource->value.max_tile_total_units);
+    return buf;
+  case VUT_MAXTILETOPUNITS:
     /* TRANS: here <= means 'less than or equal' */
     cat_snprintf(buf, bufsz, PL_("<=%d unit",
-                                 "<=%d units", psource->value.max_tile_units),
-                 psource->value.max_tile_units);
+                                 "<=%d units", psource->value.max_tile_top_units),
+                 psource->value.max_tile_top_units);
     return buf;
   case VUT_TERRAINCLASS:
     /* TRANS: Terrain class: "Land terrain" */
diff --git a/doc/README.effects b/doc/README.effects
index 920c01e8e4..2f7d9a9e6a 100644
--- a/doc/README.effects
+++ b/doc/README.effects
@@ -57,73 +57,74 @@ beyond a few hundred requirements).
 Requirement types and supported ranges
 ======================================
 
-Tech:            World, Alliance, Team, Player, Local
-TechFlag:        World, Alliance, Team, Player, Local
-MinTechs:        World, Player
-FutureTechs:     World, Player
-MinCities:       Player
-Achievement:     World, Alliance, Team, Player
-Counter:         City
-Gov:             Player
-Building:        World, Alliance, Team, Player, Continent, Traderoute, City,
-                 Tile, Local
-BuildingFlag:    Local, Tile, City
-BuildingGenus:   Local
-Site:            World, Alliance, Team, Player, Continent, Traderoute, City,
-                 Tile, Local
-Extra:           Local, Tile, Adjacent, CAdjacent, Traderoute, City
-RoadFlag:        Local, Tile, Adjacent, CAdjacent, Traderoute, City
-ExtraFlag:       Local, Tile, Adjacent, CAdjacent, Traderoute, City
-Terrain:         Tile, Adjacent, CAdjacent, Traderoute, City
-Good:            City
-UnitType:        Local, Tile, CAdjacent, Adjacent
-UnitTypeFlag:    Local, Tile, CAdjacent, Adjacent
-UnitClass:       Local, Tile, CAdjacent, Adjacent
-UnitClassFlag:   Local, Tile, CAdjacent, Adjacent
-Nation:          World, Alliance, Team, Player
-NationGroup:     World, Alliance, Team, Player
-Nationality:     Traderoute, City
-PlayerFlag:      Player
-PlayerState:     Player
-OriginalOwner:   City
-DiplRel:         World, Alliance, Team, Player, Local
-DiplRelTile:     Alliance, Team, Player, Local
-DiplRelTileOther:Local
-DiplRelUnitAny:  Alliance, Team, Player, Local
+Tech:              World, Alliance, Team, Player, Local
+TechFlag:          World, Alliance, Team, Player, Local
+MinTechs:          World, Player
+FutureTechs:       World, Player
+MinCities:         Player
+Achievement:       World, Alliance, Team, Player
+Counter:           City
+Gov:               Player
+Building:          World, Alliance, Team, Player, Continent, Traderoute,
+                   City, Tile, Local
+BuildingFlag:      Local, Tile, City
+BuildingGenus:     Local
+Site:              World, Alliance, Team, Player, Continent, Traderoute,
+                   City, Tile, Local
+Extra:             Local, Tile, Adjacent, CAdjacent, Traderoute, City
+RoadFlag:          Local, Tile, Adjacent, CAdjacent, Traderoute, City
+ExtraFlag:         Local, Tile, Adjacent, CAdjacent, Traderoute, City
+Terrain:           Tile, Adjacent, CAdjacent, Traderoute, City
+Good:              City
+UnitType:          Local, Tile, CAdjacent, Adjacent
+UnitTypeFlag:      Local, Tile, CAdjacent, Adjacent
+UnitClass:         Local, Tile, CAdjacent, Adjacent
+UnitClassFlag:     Local, Tile, CAdjacent, Adjacent
+Nation:            World, Alliance, Team, Player
+NationGroup:       World, Alliance, Team, Player
+Nationality:       Traderoute, City
+PlayerFlag:        Player
+PlayerState:       Player
+OriginalOwner:     City
+DiplRel:           World, Alliance, Team, Player, Local
+DiplRelTile:       Alliance, Team, Player, Local
+DiplRelTileOther:  Local
+DiplRelUnitAny:    Alliance, Team, Player, Local
 DiplRelUnitAnyOther: Local
-Action:          Local
-OutputType:      Local
-Specialist:      Local
-MinYear:         World
-MinCalFrag:      World
-Topology:        World
-Wrap:            World
-ServerSetting:   World
-Age (of unit):   Local
-Age (of city):   City
-Age (of player): Player
-FormAge:         Local
-MinSize:         Traderoute, City
-MinCulture:      World, Alliance, Team, Player, Traderoute, City
-MinForeignPct:   Traderoute, City
-AI:              Player
-MaxUnitsOnTile:  Tile, Adjacent, CAdjacent
-TerrainClass:    Tile, Adjacent, CAdjacent, Traderoute, City
-TerrainFlag:     Tile, Adjacent, CAdjacent, Traderoute, City
-TerrainAlter:    Tile
-MinLatitude:     Tile, Adjacent, CAdjacent, World
-MaxLatitude:     Tile, Adjacent, CAdjacent, World
-CityTile:        Tile, Adjacent, CAdjacent
-CityStatus:      Traderoute, City
-Style:           Player
-UnitState:       Local
-Activity:        Local
-MinMoveFrags:    Local
-MinVeteran:      Local
-MinHitPoints:    Local
-TileRel:         Tile, Adjacent, CAdjacent
-MaxDistanceSq:   Tile
-MaxRegionTiles:  Continent, Adjacent, CAdjacent
+Action:            Local
+OutputType:        Local
+Specialist:        Local
+MinYear:           World
+MinCalFrag:        World
+Topology:          World
+Wrap:              World
+ServerSetting:     World
+Age (of unit):     Local
+Age (of city):     City
+Age (of player):   Player
+FormAge:           Local
+MinSize:           Traderoute, City
+MinCulture:        World, Alliance, Team, Player, Traderoute, City
+MinForeignPct:     Traderoute, City
+AI:                Player
+MaxUnitsOnTile:    Tile, Adjacent, CAdjacent
+MaxTopUnitsOnTile: Tile, Adjacent, CAdjacent
+TerrainClass:      Tile, Adjacent, CAdjacent, Traderoute, City
+TerrainFlag:       Tile, Adjacent, CAdjacent, Traderoute, City
+TerrainAlter:      Tile
+MinLatitude:       Tile, Adjacent, CAdjacent, World
+MaxLatitude:       Tile, Adjacent, CAdjacent, World
+CityTile:          Tile, Adjacent, CAdjacent
+CityStatus:        Traderoute, City
+Style:             Player
+UnitState:         Local
+Activity:          Local
+MinMoveFrags:      Local
+MinVeteran:        Local
+MinHitPoints:      Local
+TileRel:           Tile, Adjacent, CAdjacent
+MaxDistanceSq:     Tile
+MaxRegionTiles:    Continent, Adjacent, CAdjacent
 
 
 Site is like Building, except that the requirement is fulfilled even after
diff --git a/gen_headers/enums/fc_types_enums.def b/gen_headers/enums/fc_types_enums.def
index 483f7aba40..82a94dba4b 100644
--- a/gen_headers/enums/fc_types_enums.def
+++ b/gen_headers/enums/fc_types_enums.def
@@ -65,7 +65,8 @@ values
   IMPR_FLAG          "BuildingFlag"
   IMPR_GENUS         "BuildingGenus"
   MAXLATITUDE        "MaxLatitude"
-  MAXTILEUNITS       "MaxUnitsOnTile"
+  MAXTILETOPUNITS    "MaxTopUnitsOnTile"
+  MAXTILETOTALUNITS  "MaxUnitsOnTile"
   MAX_DISTANCE_SQ    "MaxDistanceSq"
   MAX_REGION_TILES   "MaxRegionTiles"
   MINCALFRAG         "MinCalFrag"
diff --git a/server/cityturn.c b/server/cityturn.c
index abe91588da..1427851960 100644
--- a/server/cityturn.c
+++ b/server/cityturn.c
@@ -1838,7 +1838,40 @@ static bool worklist_item_postpone_req_vec(struct universal *target,
           purge = TRUE;
         }
         break;
-      case VUT_MAXTILEUNITS:
+      case VUT_MAXTILETOTALUNITS:
+        if (preq->present) {
+          notify_player(pplayer, city_tile(pcity),
+                        E_CITY_CANTBUILD, ftc_server,
+                        PL_("%s can't build %s from the worklist; "
+                            "more than %d total unit on tile."
+                            "  Postponing...",
+                            "%s can't build %s from the worklist; "
+                            "more than %d total units on tile."
+                            "  Postponing...",
+                            preq->source.value.max_tile_total_units),
+                        city_link(pcity),
+                        tgt_name,
+                        preq->source.value.max_tile_total_units);
+          script_server_signal_emit(signal_name, ptarget,
+                                    pcity, "need_tileunits");
+        } else {
+          notify_player(pplayer, city_tile(pcity),
+                        E_CITY_CANTBUILD, ftc_server,
+                        PL_("%s can't build %s from the worklist; "
+                            "fewer than %d total unit on tile."
+                            "  Postponing...",
+                            "%s can't build %s from the worklist; "
+                            "fewer than %d total units on tile."
+                            "  Postponing...",
+                            preq->source.value.max_tile_total_units + 1),
+                        city_link(pcity),
+                        tgt_name,
+                        preq->source.value.max_tile_total_units + 1);
+          script_server_signal_emit(signal_name, ptarget,
+                                    pcity, "need_tileunits");
+        }
+        break;
+      case VUT_MAXTILETOPUNITS:
         if (preq->present) {
           notify_player(pplayer, city_tile(pcity),
                         E_CITY_CANTBUILD, ftc_server,
@@ -1848,12 +1881,12 @@ static bool worklist_item_postpone_req_vec(struct universal *target,
                             "%s can't build %s from the worklist; "
                             "more than %d units on tile."
                             "  Postponing...",
-                            preq->source.value.max_tile_units),
+                            preq->source.value.max_tile_top_units),
                         city_link(pcity),
                         tgt_name,
-                        preq->source.value.max_tile_units);
+                        preq->source.value.max_tile_top_units);
           script_server_signal_emit(signal_name, ptarget,
-                                    pcity, "need_tileunits");
+                                    pcity, "need_tiletopunits");
         } else {
           notify_player(pplayer, city_tile(pcity),
                         E_CITY_CANTBUILD, ftc_server,
@@ -1863,12 +1896,12 @@ static bool worklist_item_postpone_req_vec(struct universal *target,
                             "%s can't build %s from the worklist; "
                             "fewer than %d units on tile."
                             "  Postponing...",
-                            preq->source.value.max_tile_units + 1),
+                            preq->source.value.max_tile_top_units + 1),
                         city_link(pcity),
                         tgt_name,
-                        preq->source.value.max_tile_units + 1);
+                        preq->source.value.max_tile_top_units + 1);
           script_server_signal_emit(signal_name, ptarget,
-                                    pcity, "need_tileunits");
+                                    pcity, "need_tiletopunits");
         }
         break;
       case VUT_AI_LEVEL:
diff --git a/server/ruleset/rssanity.c b/server/ruleset/rssanity.c
index e77f06dbf1..783c9c86a6 100644
--- a/server/ruleset/rssanity.c
+++ b/server/ruleset/rssanity.c
@@ -422,7 +422,8 @@ static bool sanity_check_req_set(rs_conversion_logger logger,
       /* Can have multiple, since it's flag based (wrapx & wrapy) */
     case VUT_EXTRA:
       /* Note that there can be more than 1 extra / tile. */
-    case VUT_MAXTILEUNITS:
+    case VUT_MAXTILETOTALUNITS:
+    case VUT_MAXTILETOPUNITS:
       /* Can require different numbers on e.g. local/adjacent tiles. */
     case VUT_NATION:
       /* Can require multiple nations at Team/Alliance/World range. */
diff --git a/tools/ruledit/univ_value.c b/tools/ruledit/univ_value.c
index 755cdb9a80..d94afab0dd 100644
--- a/tools/ruledit/univ_value.c
+++ b/tools/ruledit/univ_value.c
@@ -173,8 +173,11 @@ bool universal_value_initial(struct universal *src)
   case VUT_DIPLREL_UNITANY_O:
     src->value.diplrel = DS_WAR;
     return TRUE;
-  case VUT_MAXTILEUNITS:
-    src->value.max_tile_units = 0;
+  case VUT_MAXTILETOTALUNITS:
+    src->value.max_tile_total_units = 0;
+    return TRUE;
+  case VUT_MAXTILETOPUNITS:
+    src->value.max_tile_top_units = 0;
     return TRUE;
   case VUT_STYLE:
     if (game.control.num_styles <= 0) {
@@ -511,7 +514,8 @@ void universal_kind_values(struct universal *univ,
   case VUT_MINSIZE:
   case VUT_MINYEAR:
   case VUT_MINCALFRAG:
-  case VUT_MAXTILEUNITS:
+  case VUT_MAXTILETOTALUNITS:
+  case VUT_MAXTILETOPUNITS:
   case VUT_MINCULTURE:
   case VUT_MINFOREIGNPCT:
   case VUT_MINMOVES:
-- 
2.47.2

