From 0cab5fcc1f99e6ca3dadf3c220a20e36b2a8c7c6 Mon Sep 17 00:00:00 2001
From: Ihnatus <ignatus31oct@mail.ru>
Date: Mon, 7 Jul 2025 23:31:13 +0300
Subject: [PATCH] Correct airlift action possibility check

Don't airlift on non-allied unit tile.
Consider airlift to ally cities not blocked by counters if it is nowhere.

See RM#1564

Signed-off-by: Ihnatus <ignatus31oct@mail.ru>
---
 common/movement.c  | 44 +++++++++++++++++++++++++
 common/movement.h  |  4 +++
 common/unit.c      | 81 +++++++++++++++++++++++++++-------------------
 server/unittools.c |  2 +-
 4 files changed, 96 insertions(+), 35 deletions(-)

diff --git a/common/movement.c b/common/movement.c
index 034cc98dc7..a64ac325a8 100644
--- a/common/movement.c
+++ b/common/movement.c
@@ -310,6 +310,50 @@ bool can_exist_at_tile(const struct civ_map *nmap,
   return is_native_tile(utype, ptile);
 }
 
+/************************************************************************//**
+  Return if a unit of utype could possibly "exist" at the city tile of pcity
+  given the information known to pov_player. pcity is presumed to exist.
+  nmap is supposed to be client map.
+  This means it can physically be present on the tile (without the use of a
+  transporter). See can_exist_at_tile() for the omniscient check.
+****************************************************************************/
+bool could_exist_in_city(const struct civ_map *nmap,
+                         const struct player *pov_player,
+                         const struct unit_type *utype,
+                         const struct city *pcity)
+{
+  struct unit_class *uclass;
+  struct tile *ctile;
+
+  fc_assert_ret_val(nullptr != pcity && nullptr != utype, FALSE);
+
+  ctile = city_tile(pcity);
+  uclass = utype_class(utype);
+
+  if (uclass_has_flag(uclass, UCF_BUILD_ANYWHERE)) {
+    /* If the city stands, it can exist there */
+    return TRUE;
+  }
+  adjc_iterate(nmap, ctile, ptile) {
+    if (!tile_is_seen(ptile, pov_player)
+        || is_native_tile_to_class(uclass, ptile)) {
+      /* Could be native. This ignores a rare case when we don't see
+       * only the city center and any native terrain is NoCities */
+      return TRUE;
+    }
+  } adjc_iterate_end;
+
+  if (1 == game.info.citymindist
+      && is_city_channel_tile(nmap, uclass, ctile, nullptr)) {
+    /* FIXME: false negative results for city channels might appear */
+    /* Channeled. */
+    return TRUE;
+  }
+
+  /* It definitely can't exist there */
+  return FALSE;
+}
+
 /************************************************************************//**
   Return TRUE iff the unit can "exist" at this location.  This means it can
   physically be present on the tile (without the use of a transporter).  See
diff --git a/common/movement.h b/common/movement.h
index 611b679383..351e2ae298 100644
--- a/common/movement.h
+++ b/common/movement.h
@@ -98,6 +98,10 @@ bool is_native_near_tile(const struct civ_map *nmap,
 bool can_exist_at_tile(const struct civ_map *nmap,
                        const struct unit_type *utype,
                        const struct tile *ptile);
+bool could_exist_in_city(const struct civ_map *nmap,
+                         const struct player *pov_player,
+                         const struct unit_type *utype,
+                         const struct city *pcity);
 bool can_unit_exist_at_tile(const struct civ_map *nmap,
                             const struct unit *punit, const struct tile *ptile);
 bool can_unit_survive_at_tile(const struct civ_map *nmap,
diff --git a/common/unit.c b/common/unit.c
index 8b5084be0d..dd40303530 100644
--- a/common/unit.c
+++ b/common/unit.c
@@ -85,11 +85,13 @@ enum unit_airlift_result
 {
   const struct city *psrc_city = tile_city(unit_tile(punit));
   const struct player *punit_owner;
+  const struct tile *dst_tile = nullptr;
+  const struct unit_type *putype = unit_type_get(punit);
+  bool flagless = utype_has_flag(putype, UTYF_FLAGLESS);
   enum unit_airlift_result ok_result = AR_OK;
 
   if (0 == punit->moves_left
-      && !utype_may_act_move_frags(unit_type_get(punit),
-                                   ACTION_AIRLIFT, 0)) {
+      && !utype_may_act_move_frags(putype, ACTION_AIRLIFT, 0)) {
     /* No moves left. */
     return AR_NO_MOVES;
   }
@@ -103,7 +105,7 @@ enum unit_airlift_result
     return AR_OCCUPIED;
   }
 
-  if (NULL == psrc_city) {
+  if (nullptr == psrc_city) {
     /* No city there. */
     return AR_NOT_IN_CITY;
   }
@@ -113,13 +115,15 @@ enum unit_airlift_result
     return AR_BAD_DST_CITY;
   }
 
-  if (pdest_city
-      && (NULL == restriction
-          || (tile_get_known(city_tile(pdest_city), restriction)
-              == TILE_KNOWN_SEEN))
-      && !can_unit_exist_at_tile(nmap, punit, city_tile(pdest_city))) {
-    /* Can't exist at the destination tile. */
-    return AR_BAD_DST_CITY;
+  if (nullptr != pdest_city) {
+    dst_tile = city_tile(pdest_city);
+
+    if (nullptr != restriction
+        ? !can_exist_at_tile(nmap, putype, dst_tile)
+        : !could_exist_in_city(nmap, restriction, putype, pdest_city)) {
+      /* Can't exist at the destination tile. */
+      return AR_BAD_DST_CITY;
+    }
   }
 
   punit_owner = unit_owner(punit);
@@ -134,35 +138,44 @@ enum unit_airlift_result
     return AR_BAD_SRC_CITY;
   }
 
-  if (pdest_city
-      && punit_owner != city_owner(pdest_city)
-      && !(game.info.airlifting_style & AIRLIFTING_ALLIED_DEST
-           && pplayers_allied(punit_owner, city_owner(pdest_city)))) {
-    /* Not allowed to airlift to this destination. */
-    return AR_BAD_DST_CITY;
+  /* Check diplomatic possibility of the destination */
+  if (nullptr != pdest_city) {
+    if (punit_owner != city_owner(pdest_city)) {
+      if (!(game.info.airlifting_style & AIRLIFTING_ALLIED_DEST
+            && pplayers_allied(punit_owner, city_owner(pdest_city)))
+          || flagless || is_non_allied_unit_tile(dst_tile, punit_owner, FALSE)) {
+        /* Not allowed to airlift to this destination. */
+        return AR_BAD_DST_CITY;
+      }
+    } else if (flagless
+               && is_non_allied_unit_tile(dst_tile, punit_owner, TRUE)) {
+      /* Foreign units block airlifting to this destination */
+      return AR_BAD_DST_CITY;
+    }
   }
 
-  if (NULL == restriction || city_owner(psrc_city) == restriction) {
-    /* We know for sure whether or not src can airlift this turn. */
-    if (0 >= psrc_city->airlift
-        && (!(game.info.airlifting_style & AIRLIFTING_UNLIMITED_SRC)
-            || !game.info.airlift_from_always_enabled)) {
-      /* The source cannot airlift for this turn (maybe already airlifted
-       * or no airport).
-       * See also do_airline() in server/unittools.h. */
-      return AR_SRC_NO_FLIGHTS;
-    } /* else, there is capacity; continue to other checks */
-  } else {
-    /* We don't have access to the 'airlift' field. Assume it's OK; can
-     * only find out for sure by trying it. */
-    ok_result = AR_OK_SRC_UNKNOWN;
+  /* Check airlift capacities */
+  if (!game.info.airlift_from_always_enabled) {
+    if (nullptr == restriction || city_owner(psrc_city) == restriction) {
+      /* We know for sure whether or not src can airlift this turn. */
+      if (0 >= psrc_city->airlift
+          && !(game.info.airlifting_style & AIRLIFTING_UNLIMITED_SRC)) {
+        /* The source cannot airlift for this turn (maybe already airlifted
+         * or no airport).
+         * See also do_airline() in server/unittools.h. */
+        return AR_SRC_NO_FLIGHTS;
+      } /* else, there is capacity; continue to other checks */
+    } else {
+      /* We don't have access to the 'airlift' field. Assume it's OK; can
+       * only find out for sure by trying it. */
+      ok_result = AR_OK_SRC_UNKNOWN;
+    }
   }
 
-  if (pdest_city) {
-    if (NULL == restriction || city_owner(pdest_city) == restriction) {
+  if (nullptr != pdest_city && !game.info.airlift_to_always_enabled) {
+    if (nullptr == restriction || city_owner(pdest_city) == restriction) {
       if (0 >= pdest_city->airlift
-          && (!(game.info.airlifting_style & AIRLIFTING_UNLIMITED_DEST)
-              || !game.info.airlift_to_always_enabled)) {
+          && !(game.info.airlifting_style & AIRLIFTING_UNLIMITED_DEST)) {
         /* The destination cannot support airlifted units for this turn
          * (maybe already airlifted or no airport).
          * See also do_airline() in server/unittools.h. */
diff --git a/server/unittools.c b/server/unittools.c
index 8e0cf8d8e3..b7f279d890 100644
--- a/server/unittools.c
+++ b/server/unittools.c
@@ -4072,7 +4072,7 @@ static struct unit_move_data_list *construct_move_data_list(struct unit *punit,
 /**********************************************************************//**
   Moves a unit. No checks whatsoever! This is meant as a practical
   function for other functions, like do_airline(), which do the checking
-  themselves.
+  either by themselves or by their callers
 
   If you move a unit you should always use this function, as it also sets
   the transport status of the unit correctly. Note that the source tile (the
-- 
2.45.2

