From 9d84f54839dfccd404573570e08169f530fd52c8 Mon Sep 17 00:00:00 2001
From: Marko Lindqvist <cazfi74@gmail.com>
Date: Sat, 29 Aug 2026 14:53:18 +0300
Subject: [PATCH 66/66] Add actres type "Scout"

See RM #2183

Signed-off-by: Marko Lindqvist <cazfi74@gmail.com>
---
 ai/default/daicity.c                 |  1 +
 ai/default/daidiplomacy.c            |  1 +
 common/actions.c                     |  5 ++++
 common/actres.c                      |  8 +++++++
 common/unit.c                        |  1 +
 common/unit.h                        |  1 +
 gen_headers/enums/fc_types_enums.def |  1 +
 server/advisors/advdata.c            |  1 +
 server/unithand.c                    | 36 ++++++++++++++++++++++++++++
 server/unittools.c                   | 12 ++++++++++
 10 files changed, 67 insertions(+)

diff --git a/ai/default/daicity.c b/ai/default/daicity.c
index c061096887..ddc5d549e2 100644
--- a/ai/default/daicity.c
+++ b/ai/default/daicity.c
@@ -1281,6 +1281,7 @@ static int action_target_neg_util(action_id act_id,
   /* Could be worse */
   case ACTRES_ESTABLISH_EMBASSY:
   case ACTRES_SPY_INVESTIGATE_CITY:
+  case ACTRES_SCOUT:
   case ACTRES_MARKETPLACE:
   case ACTRES_SPY_ESCAPE:
     /* TODO: Individual and well balanced values */
diff --git a/ai/default/daidiplomacy.c b/ai/default/daidiplomacy.c
index e2a4e4e1b7..2bde380eee 100644
--- a/ai/default/daidiplomacy.c
+++ b/ai/default/daidiplomacy.c
@@ -2105,6 +2105,7 @@ void dai_incident(struct ai_type *ait, enum incident_type type,
     case ACTRES_TRANSPORT_EMBARK:
     case ACTRES_HUT_ENTER:
     case ACTRES_HUT_FRIGHTEN:
+    case ACTRES_SCOUT:
     case ACTRES_ENABLER_CHECK:
     case ACTRES_NONE:
       /* Various */
diff --git a/common/actions.c b/common/actions.c
index 9a8c85f525..d3184d491b 100644
--- a/common/actions.c
+++ b/common/actions.c
@@ -2574,6 +2574,7 @@ action_actor_utype_hard_reqs_ok_full(const struct action *paction,
   case ACTRES_UNIT_MOVE:
   case ACTRES_TELEPORT:
   case ACTRES_TELEPORT_CONQUER:
+  case ACTRES_SCOUT:
   case ACTRES_ENABLER_CHECK:
   case ACTRES_NONE:
     /* No hard unit type requirements. */
@@ -2774,6 +2775,7 @@ action_hard_reqs_actor(const struct civ_map *nmap,
   case ACTRES_SPY_ATTACK:
   case ACTRES_HUT_ENTER:
   case ACTRES_HUT_FRIGHTEN:
+  case ACTRES_SCOUT:
   case ACTRES_ENABLER_CHECK:
   case ACTRES_NONE:
     /* No hard unit requirements. */
@@ -4347,6 +4349,9 @@ action_prob(const struct civ_map *nmap,
   case ACTRES_TELEPORT_CONQUER:
     chance = ACTPROB_CERTAIN;
     break;
+  case ACTRES_SCOUT:
+    chance = ACTPROB_CERTAIN;
+    break;
     /* Not UI action, so chance is meaningless */
   case ACTRES_ENABLER_CHECK:
     chance = ACTPROB_CERTAIN;
diff --git a/common/actres.c b/common/actres.c
index 0ee96f3e7b..30344631aa 100644
--- a/common/actres.c
+++ b/common/actres.c
@@ -233,6 +233,9 @@ static struct actres act_results[ACTRES_LAST] = {
   { ACT_TGT_COMPL_SIMPLE, ABK_NONE,          /* ACTRES_TELEPORT_CONQUER */
     FALSE, ACTIVITY_LAST, DRT_NONE,
     EC_NONE, ERM_NONE, ATK_TILE },
+  { ACT_TGT_COMPL_SIMPLE, ABK_NONE,          /* ACTRES_SCOUT */
+    FALSE, ACTIVITY_LAST, DRT_NONE,
+    EC_NONE, ERM_NONE, ATK_TILE },
   { ACT_TGT_COMPL_SIMPLE, ABK_NONE,          /* ACTRES_ENABLER_CHECK */
     FALSE, ACTIVITY_LAST, DRT_NONE,
     EC_NONE, ERM_NONE, ATK_SELF }
@@ -398,6 +401,7 @@ int actres_min_range_default(enum action_result result)
   case ACTRES_JOIN_CITY:
   case ACTRES_STEAL_MAPS:
   case ACTRES_BOMBARD:
+  case ACTRES_SCOUT:
   case ACTRES_SPY_NUKE:
   case ACTRES_DESTROY_CITY:
   case ACTRES_EXPEL_UNIT:
@@ -534,6 +538,7 @@ int actres_max_range_default(enum action_result result)
   case ACTRES_DISBAND_UNIT_RECOVER:
     return RS_DEFAULT_ACTION_MAX_RANGE;
   case ACTRES_BOMBARD:
+  case ACTRES_SCOUT:
     return RS_DEFAULT_ACTION_MAX_RANGE;
   case ACTRES_NUKE:
     return RS_DEFAULT_EXPLODE_NUCLEAR_MAX_RANGE;
@@ -630,6 +635,7 @@ bool actres_legal_target_kind(enum action_result result,
   case ACTRES_UNIT_MOVE:
   case ACTRES_TELEPORT:
   case ACTRES_TELEPORT_CONQUER:
+  case ACTRES_SCOUT:
     return tgt_kind == ATK_TILE;
   case ACTRES_CONQUER_EXTRAS:
     return tgt_kind == ATK_EXTRAS;
@@ -737,6 +743,7 @@ actres_sub_target_kind_default(enum action_result result)
   case ACTRES_UNIT_MOVE:
   case ACTRES_TELEPORT:
   case ACTRES_TELEPORT_CONQUER:
+  case ACTRES_SCOUT:
   case ACTRES_ENABLER_CHECK:
   case ACTRES_SPY_ESCAPE:
     return ASTK_NONE;
@@ -1655,6 +1662,7 @@ enum fc_tristate actres_possible(const struct civ_map *nmap,
   case ACTRES_STRIKE_PRODUCTION:
   case ACTRES_FORTIFY:
   case ACTRES_HOMELESS:
+  case ACTRES_SCOUT:
   case ACTRES_ENABLER_CHECK:
   case ACTRES_NONE:
     /* No known hard coded requirements. */
diff --git a/common/unit.c b/common/unit.c
index a25f5a15c2..1b484d1613 100644
--- a/common/unit.c
+++ b/common/unit.c
@@ -1760,6 +1760,7 @@ struct unit *unit_virtual_create(struct player *pplayer, struct city *pcity,
     punit->server.ord_city = 0;
 
     punit->server.vision = nullptr; /* No vision. */
+    punit->server.scout_vision = nullptr;
     punit->server.action_timestamp = 0;
     /* Must be an invalid turn number, and an invalid previous turn
      * number. */
diff --git a/common/unit.h b/common/unit.h
index f97a21520a..4c82e2b6c9 100644
--- a/common/unit.h
+++ b/common/unit.h
@@ -244,6 +244,7 @@ struct unit {
       int ord_city;
 
       struct vision *vision;
+      struct vision *scout_vision;
       time_t action_timestamp;
       int action_turn;
       struct unit_move_data *moving;
diff --git a/gen_headers/enums/fc_types_enums.def b/gen_headers/enums/fc_types_enums.def
index 629cf5f3ec..404e77db64 100644
--- a/gen_headers/enums/fc_types_enums.def
+++ b/gen_headers/enums/fc_types_enums.def
@@ -200,6 +200,7 @@ values
   /* TODO: Move close to "Move" */
   TELEPORT                     "Teleport"
   TELEPORT_CONQUER             "Teleport Conquer"
+  SCOUT                        "Scout"
 
   /* Hardcoded action that's just controlled by enablers */
   ENABLER_CHECK
diff --git a/server/advisors/advdata.c b/server/advisors/advdata.c
index e6e60856e8..1b8abb5f56 100644
--- a/server/advisors/advdata.c
+++ b/server/advisors/advdata.c
@@ -904,6 +904,7 @@ adv_want adv_gov_action_immunity_want(struct government *gov)
     case ACTRES_UNIT_MOVE:
     case ACTRES_TELEPORT:
     case ACTRES_TELEPORT_CONQUER:
+    case ACTRES_SCOUT:
     case ACTRES_ENABLER_CHECK:
     case ACTRES_MARKETPLACE:
     case ACTRES_FOUND_CITY:
diff --git a/server/unithand.c b/server/unithand.c
index 015749bed3..2e940fdd7e 100644
--- a/server/unithand.c
+++ b/server/unithand.c
@@ -177,6 +177,8 @@ static bool do_action_activity(struct unit *punit,
 static bool do_action_activity_targeted(struct unit *punit,
                                         const struct action *paction,
                                         struct extra_type **new_target);
+static bool do_scout(struct unit *punit, struct tile *ptile,
+                     const struct action *paction);
 static inline bool
 non_allied_not_listed_at(const struct player *pplayer,
                          const int *list, int n, const struct tile *ptile);
@@ -1138,6 +1140,7 @@ static struct player *need_war_player_hlp(const struct unit *actor,
   case ACTRES_PARADROP_CONQUER:
     /* Target is a tile but a city can block it. */
     fc_assert_action(action_get_target_kind(paction) == ATK_TILE, break);
+
     if (target_tile != nullptr
         && map_is_known_and_seen(target_tile, actor_player, V_MAIN)) {
       /* Seen tile unit savers */
@@ -1209,6 +1212,7 @@ static struct player *need_war_player_hlp(const struct unit *actor,
   case ACTRES_UNIT_MOVE:
   case ACTRES_TELEPORT:
   case ACTRES_TELEPORT_CONQUER:
+  case ACTRES_SCOUT:
   case ACTRES_ENABLER_CHECK:
   case ACTRES_SPY_ESCAPE:
   case ACTRES_NONE:
@@ -4019,6 +4023,10 @@ bool unit_perform_action(struct player *pplayer,
                                        paction->result == ACTRES_TELEPORT_CONQUER,
                                        FALSE, FALSE));
     break;
+  case ACTRES_SCOUT:
+    ACTION_PERFORM_UNIT_TILE(action_type, actor_unit, target_tile,
+                             do_scout(actor_unit, target_tile, paction));
+    break;
   case ACTRES_TRANSFORM_TERRAIN:
     ACTION_PERFORM_UNIT_TILE(action_type, actor_unit, target_tile,
                              do_action_activity(actor_unit, paction));
@@ -6863,6 +6871,34 @@ static bool unit_activity_targeted_internal(struct unit *punit,
   }
 }
 
+/**********************************************************************//**
+  Perform a scout action.
+
+  Returns TRUE iff action could be done, FALSE if it couldn't. Even if
+  this returns TRUE, unit may have died during the action.
+**************************************************************************/
+static bool do_scout(struct unit *punit, struct tile *ptile,
+                     const struct action *paction)
+{
+  struct player *owner = unit_owner(punit);
+
+  if (punit->server.scout_vision != nullptr
+      && punit->server.scout_vision->tile != ptile) {
+    vision_free(punit->server.scout_vision);
+    punit->server.scout_vision = nullptr;
+  }
+
+  if (punit->server.scout_vision == nullptr) {
+    const v_radius_t radius_sq = V_RADIUS(5, 0, 0);
+
+    punit->server.scout_vision = vision_new(owner, ptile);
+    vision_reveal_tiles(punit->server.scout_vision, TRUE);
+    vision_change_sight(punit->server.scout_vision, radius_sq);
+  }
+
+  return TRUE;
+}
+
 /**********************************************************************//**
   Receives route packages.
 **************************************************************************/
diff --git a/server/unittools.c b/server/unittools.c
index 4137571c12..2909ce2056 100644
--- a/server/unittools.c
+++ b/server/unittools.c
@@ -709,6 +709,13 @@ void finalize_unit_phase_beginning(struct player *pplayer)
     punit->changed_from = punit->activity;
     punit->changed_from_target = punit->activity_target;
     punit->changed_from_count = punit->activity_count;
+
+    if (punit->server.scout_vision != nullptr) {
+      vision_clear_sight(punit->server.scout_vision);
+      vision_free(punit->server.scout_vision);
+      punit->server.scout_vision = nullptr;
+    }
+
     send_unit_info(nullptr, punit);
   } unit_list_iterate_end;
 }
@@ -1837,6 +1844,11 @@ static void server_remove_unit_full(struct unit *punit, bool transported,
     vision_free(punit->server.vision);
     punit->server.vision = nullptr;
   }
+  if (punit->server.scout_vision != nullptr) {
+    vision_clear_sight(punit->server.scout_vision);
+    vision_free(punit->server.scout_vision);
+    punit->server.scout_vision = nullptr;
+  }
 
   packet.unit_id = punit->id;
   /* Send to onlookers. */
-- 
2.53.0

