From 5d84c1a054bb07972d76b74c96a3611ade547af1 Mon Sep 17 00:00:00 2001
From: Marko Lindqvist <cazfi74@gmail.com>
Date: Mon, 26 May 2025 17:32:20 +0300
Subject: [PATCH 79/79] Add internal city action type "Finish Unit"

See RM #1452

Signed-off-by: Marko Lindqvist <cazfi74@gmail.com>
---
 common/actions.c                    | 49 +++++++++++++++++++++++++++--
 common/actions.h                    |  4 +++
 data/alien/actions.ruleset          |  8 ++++-
 data/civ1/actions.ruleset           |  6 ++++
 data/civ2/actions.ruleset           |  6 ++++
 data/civ2civ3/actions.ruleset       |  6 ++++
 data/classic/actions.ruleset        |  6 ++++
 data/goldkeep/actions.ruleset       |  6 ++++
 data/granularity/actions.ruleset    |  6 ++++
 data/multiplayer/actions.ruleset    |  6 ++++
 data/sandbox/actions.ruleset        |  6 ++++
 data/webperimental/actions.ruleset  |  6 ++++
 doc/README.actions                  |  2 ++
 gen_headers/enums/actions_enums.def |  1 +
 server/cityturn.c                   |  8 +++--
 server/ruleset/rscompat.c           |  6 ++++
 16 files changed, 125 insertions(+), 7 deletions(-)

diff --git a/common/actions.c b/common/actions.c
index 4adabda34f..ffde1bed9d 100644
--- a/common/actions.c
+++ b/common/actions.c
@@ -74,6 +74,9 @@ unit_action_new(action_id id,
 static struct action *
 player_action_new(action_id id,
                   enum action_result result);
+static struct action *
+city_action_new(action_id id,
+                enum action_result result);
 
 static bool is_enabler_active(const struct action_enabler *enabler,
                               const struct req_context *actor,
@@ -822,6 +825,9 @@ static void hard_code_actions(void)
   actions[ACTION_CIVIL_WAR] =
       player_action_new(ACTION_CIVIL_WAR, ACTRES_ENABLER_CHECK);
 
+  actions[ACTION_FINISH_UNIT] =
+      city_action_new(ACTION_FINISH_UNIT, ACTRES_ENABLER_CHECK);
+
   /* The structure even for these need to be created, for
    * the action_id_rule_name() to work on iterations. */
 
@@ -1051,9 +1057,20 @@ unit_action_new(action_id id,
 /**********************************************************************//**
   Create a new action performed by a player actor.
 **************************************************************************/
-static struct action *
-player_action_new(action_id id,
-                  enum action_result result)
+static struct action *player_action_new(action_id id,
+                                        enum action_result result)
+{
+  struct action *act = action_new(id, result,
+                                  0, 0, FALSE);
+
+  return act;
+}
+
+/**********************************************************************//**
+  Create a new action performed by a city actor.
+**************************************************************************/
+static struct action *city_action_new(action_id id,
+                                      enum action_result result)
 {
   struct action *act = action_new(id, result,
                                   0, 0, FALSE);
@@ -3501,6 +3518,23 @@ bool is_action_enabled_player(const struct civ_map *nmap,
                            nullptr, nullptr, nullptr);
 }
 
+/**********************************************************************//**
+  Returns TRUE if actor_city can do wanted_action as far as
+  action enablers are concerned.
+**************************************************************************/
+bool is_action_enabled_city(const struct civ_map *nmap,
+                            const action_id wanted_action,
+                            const struct city *actor_city)
+{
+  return is_action_enabled(nmap, wanted_action,
+                           &(const struct req_context) {
+                             .player = city_owner(actor_city),
+                             .city = actor_city,
+                             .tile = city_tile(actor_city)
+                           },
+                           nullptr, nullptr, nullptr);
+}
+
 /**********************************************************************//**
   Find out if the action is enabled, may be enabled or isn't enabled given
   what the player owning the actor knowns.
@@ -6085,6 +6119,7 @@ const char *action_ui_name_ruleset_var_name(int act)
   case ACTION_GAIN_VETERANCY:
   case ACTION_ESCAPE:
   case ACTION_CIVIL_WAR:
+  case ACTION_FINISH_UNIT:
     fc_assert(!action_id_is_internal(act)); /* Fail always */
     break;
   case ACTION_COUNT:
@@ -6451,6 +6486,8 @@ const char *action_ui_name_default(int act)
     return N_("%sEscape%s");
   case ACTION_CIVIL_WAR:
     return N_("%sCivil War%s");
+  case ACTION_FINISH_UNIT:
+    return N_("%sFinish Unit%s");
   case ACTION_COUNT:
     fc_assert(act != ACTION_COUNT);
     break;
@@ -6594,6 +6631,7 @@ const char *action_min_range_ruleset_var_name(int act)
   case ACTION_GAIN_VETERANCY:
   case ACTION_ESCAPE:
   case ACTION_CIVIL_WAR:
+  case ACTION_FINISH_UNIT:
     /* Min range is not ruleset changeable */
     return NULL;
   case ACTION_NUKE:
@@ -6763,6 +6801,7 @@ const char *action_max_range_ruleset_var_name(int act)
   case ACTION_GAIN_VETERANCY:
   case ACTION_ESCAPE:
   case ACTION_CIVIL_WAR:
+  case ACTION_FINISH_UNIT:
     /* Max range is not ruleset changeable */
     return NULL;
   case ACTION_HELP_WONDER:
@@ -6966,6 +7005,7 @@ const char *action_target_kind_ruleset_var_name(int act)
   case ACTION_GAIN_VETERANCY:
   case ACTION_ESCAPE:
   case ACTION_CIVIL_WAR:
+  case ACTION_FINISH_UNIT:
     /* Target kind is not ruleset changeable */
     return NULL;
   case ACTION_NUKE:
@@ -7137,6 +7177,7 @@ const char *action_actor_consuming_always_ruleset_var_name(action_id act)
   case ACTION_GAIN_VETERANCY:
   case ACTION_ESCAPE:
   case ACTION_CIVIL_WAR:
+  case ACTION_FINISH_UNIT:
     /* Actor consuming always is not ruleset changeable */
     return NULL;
   case ACTION_FOUND_CITY:
@@ -7347,6 +7388,7 @@ const char *action_blocked_by_ruleset_var_name(const struct action *act)
   case ACTION_HUT_FRIGHTEN4:
   case ACTION_GAIN_VETERANCY:
   case ACTION_CIVIL_WAR:
+  case ACTION_FINISH_UNIT:
   case ACTION_ESCAPE:
   case ACTION_USER_ACTION1:
   case ACTION_USER_ACTION2:
@@ -7526,6 +7568,7 @@ action_post_success_forced_ruleset_var_name(const struct action *act)
   case ACTION_GAIN_VETERANCY:
   case ACTION_ESCAPE:
   case ACTION_CIVIL_WAR:
+  case ACTION_FINISH_UNIT:
   case ACTION_USER_ACTION1:
   case ACTION_USER_ACTION2:
   case ACTION_USER_ACTION3:
diff --git a/common/actions.h b/common/actions.h
index cab561207b..5a76594cc9 100644
--- a/common/actions.h
+++ b/common/actions.h
@@ -562,6 +562,10 @@ bool is_action_enabled_player(const struct civ_map *nmap,
                               const action_id wanted_action,
                               const struct player *actor_plr);
 
+bool is_action_enabled_city(const struct civ_map *nmap,
+                            const action_id wanted_action,
+                            const struct city *actor_city);
+
 struct act_prob action_prob_vs_city(const struct civ_map *nmap,
                                     const struct unit *actor,
                                     const action_id act_id,
diff --git a/data/alien/actions.ruleset b/data/alien/actions.ruleset
index 067a39a4fc..467290f846 100644
--- a/data/alien/actions.ruleset
+++ b/data/alien/actions.ruleset
@@ -993,5 +993,11 @@ actor_reqs    =
 [enabler_gain_veterancy]
 action        = "Gain Veterancy"
 actor_reqs    =
-    { "type",           "name",         "range", "present"
+    { "type", "name", "range", "present"
+    }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
     }
diff --git a/data/civ1/actions.ruleset b/data/civ1/actions.ruleset
index 62fde2c4f3..6e65fa69b8 100644
--- a/data/civ1/actions.ruleset
+++ b/data/civ1/actions.ruleset
@@ -798,3 +798,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/data/civ2/actions.ruleset b/data/civ2/actions.ruleset
index 674e1aa2fd..b33470acce 100644
--- a/data/civ2/actions.ruleset
+++ b/data/civ2/actions.ruleset
@@ -1114,3 +1114,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/data/civ2civ3/actions.ruleset b/data/civ2civ3/actions.ruleset
index 00f41a252d..adb9013cfb 100644
--- a/data/civ2civ3/actions.ruleset
+++ b/data/civ2civ3/actions.ruleset
@@ -1439,3 +1439,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/data/classic/actions.ruleset b/data/classic/actions.ruleset
index 5571cec63d..5139eada76 100644
--- a/data/classic/actions.ruleset
+++ b/data/classic/actions.ruleset
@@ -1318,3 +1318,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/data/goldkeep/actions.ruleset b/data/goldkeep/actions.ruleset
index 0193247292..0dd8876269 100644
--- a/data/goldkeep/actions.ruleset
+++ b/data/goldkeep/actions.ruleset
@@ -1387,3 +1387,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/data/granularity/actions.ruleset b/data/granularity/actions.ruleset
index 696ab8e4a6..5905e51f94 100644
--- a/data/granularity/actions.ruleset
+++ b/data/granularity/actions.ruleset
@@ -555,3 +555,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/data/multiplayer/actions.ruleset b/data/multiplayer/actions.ruleset
index e9b9f1e1b5..dd57ce6d7a 100644
--- a/data/multiplayer/actions.ruleset
+++ b/data/multiplayer/actions.ruleset
@@ -1300,3 +1300,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/data/sandbox/actions.ruleset b/data/sandbox/actions.ruleset
index d1b911d00f..e9ccdc858b 100644
--- a/data/sandbox/actions.ruleset
+++ b/data/sandbox/actions.ruleset
@@ -2847,3 +2847,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/data/webperimental/actions.ruleset b/data/webperimental/actions.ruleset
index 877cadae50..63f2bf67e7 100644
--- a/data/webperimental/actions.ruleset
+++ b/data/webperimental/actions.ruleset
@@ -1566,3 +1566,9 @@ action        = "Civil War"
 actor_reqs    =
     { "type", "name", "range", "present"
     }
+
+[enabler_finish_unit]
+action        = "Finish Unit"
+actor_reqs    =
+    { "type", "name", "range", "present"
+    }
diff --git a/doc/README.actions b/doc/README.actions
index 99e6de9c0d..bc4acd5159 100644
--- a/doc/README.actions
+++ b/doc/README.actions
@@ -1574,6 +1574,8 @@ Internal actions
  * Unit's veterancy system must have veterancy levels for the unit
    to advance to.
 
+"Finish Unit" - city can finish building a unit
+
 Ruleset defined actions
 =======================
 User actions are "blank". The ruleset does everything they do. The following
diff --git a/gen_headers/enums/actions_enums.def b/gen_headers/enums/actions_enums.def
index a816014a38..c9f1766a6d 100644
--- a/gen_headers/enums/actions_enums.def
+++ b/gen_headers/enums/actions_enums.def
@@ -145,6 +145,7 @@ values
   ACTION_GAIN_VETERANCY                   "Gain Veterancy"
   ACTION_ESCAPE                           "Escape"
   ACTION_CIVIL_WAR                        "Civil War"
+  ACTION_FINISH_UNIT                      "Finish Unit"
 
   # User actions
   ACTION_USER_ACTION1                     "User Action 1"
diff --git a/server/cityturn.c b/server/cityturn.c
index 96c1104474..ded844b8c7 100644
--- a/server/cityturn.c
+++ b/server/cityturn.c
@@ -2712,7 +2712,7 @@ static bool city_build_building(struct player *pplayer, struct city *pcity)
     pcity->before_change_shields -= cost;
     pcity->shield_stock -= cost;
     pcity->turn_last_built = game.info.turn;
-    /* to eliminate micromanagement */
+    /* To eliminate micromanagement */
     if (is_great_wonder(pimprove)) {
       notify_player(NULL, city_tile(pcity), E_WONDER_BUILD, ftc_server,
                     _("The %s have finished building %s in %s."),
@@ -2901,7 +2901,8 @@ static bool city_build_unit(struct player *pplayer, struct city *pcity)
     return city_exist(saved_city_id);
   }
 
-  if (pcity->shield_stock >= unit_shield_cost) {
+  if (pcity->shield_stock >= unit_shield_cost
+      && is_action_enabled_city(nmap, ACTION_FINISH_UNIT, pcity)) {
     int pop_cost = utype_pop_value(utype, pcity);
     struct unit *punit;
 
@@ -3032,10 +3033,11 @@ static bool city_build_stuff(struct player *pplayer, struct city *pcity)
   case VUT_UTYPE:
     return city_build_unit(pplayer, pcity);
   default:
-    /* must never happen! */
+    /* Must never happen! */
     fc_assert(FALSE);
     break;
   };
+
   return FALSE;
 }
 
diff --git a/server/ruleset/rscompat.c b/server/ruleset/rscompat.c
index f4fc528453..84e3347bfa 100644
--- a/server/ruleset/rscompat.c
+++ b/server/ruleset/rscompat.c
@@ -492,12 +492,18 @@ static bool effect_list_compat_cb(struct effect *peffect, void *data)
 **************************************************************************/
 void rscompat_postprocess(struct rscompat_info *info)
 {
+  struct action_enabler *enabler;
+
   if (!info->compat_mode || info->version >= RSFORMAT_CURRENT) {
     /* There isn't anything here that should be done outside of compat
      * mode. */
     return;
   }
 
+  enabler = action_enabler_new();
+  enabler->action = ACTION_FINISH_UNIT;
+  action_enabler_add(enabler);
+
   /* Upgrade existing effects. Done before new effects are added to prevent
    * the new effects from being upgraded by accident. */
   iterate_effect_cache(effect_list_compat_cb, info);
-- 
2.47.2

