From 8c69859358bbc7d709e2f9607fee86913119ea03 Mon Sep 17 00:00:00 2001
From: Ihnatus <ignatus31oct@mail.ru>
Date: Thu, 5 Jun 2025 00:56:55 +0300
Subject: [PATCH] Lua support for superspecialists

Introduces (Specialist).is_super and (City):add_specialist().
The latter also adds normal specialists without workers rearrange.
Corrects work of (City):reduce_specialists().

See FCRM#1399.

Signed-off-by: Ihnatus <ignatus31oct@mail.ru>
---
 common/scriptcore/api_game_methods.c | 11 ++++
 common/scriptcore/api_game_methods.h |  1 +
 common/scriptcore/tolua_game.pkg     |  4 ++
 server/cityturn.c                    | 72 ++++++++++++---------
 server/cityturn.h                    |  5 +-
 server/edithand.c                    |  5 +-
 server/scripting/api_server_edit.c   | 94 ++++++++++++++++++++++------
 server/scripting/api_server_edit.h   |  2 +
 server/scripting/tolua_server.pkg    |  7 +++
 server/unithand.c                    |  2 +-
 10 files changed, 151 insertions(+), 52 deletions(-)

diff --git a/common/scriptcore/api_game_methods.c b/common/scriptcore/api_game_methods.c
index cf5357a2a8..8ed3ece545 100644
--- a/common/scriptcore/api_game_methods.c
+++ b/common/scriptcore/api_game_methods.c
@@ -1169,6 +1169,17 @@ const char *api_methods_specialist_name_translation(lua_State *L,
   return specialist_plural_translation(s);
 }
 
+/**********************************************************************//**
+  Return if specialist is a superspecialist
+**************************************************************************/
+bool api_methods_specialist_is_super(lua_State *L, Specialist *s)
+{
+  LUASCRIPT_CHECK_STATE(L, FALSE);
+  LUASCRIPT_CHECK_SELF(L, s, FALSE);
+
+  return is_super_specialist(s);
+}
+
 /**********************************************************************//**
   Return the native x coordinate of the tile.
 **************************************************************************/
diff --git a/common/scriptcore/api_game_methods.h b/common/scriptcore/api_game_methods.h
index 88e2255e76..e9a8632d29 100644
--- a/common/scriptcore/api_game_methods.h
+++ b/common/scriptcore/api_game_methods.h
@@ -164,6 +164,7 @@ const char *api_methods_action_target_kind(lua_State *L, Action *pact);
 const char *api_methods_specialist_rule_name(lua_State *L, Specialist *s);
 const char *api_methods_specialist_name_translation(lua_State *L,
                                                     Specialist *s);
+bool api_methods_specialist_is_super(lua_State *L, Specialist *s);
 
 /* Tile */
 int api_methods_tile_nat_x(lua_State *L, Tile *ptile);
diff --git a/common/scriptcore/tolua_game.pkg b/common/scriptcore/tolua_game.pkg
index 092d488fd9..9773742c02 100644
--- a/common/scriptcore/tolua_game.pkg
+++ b/common/scriptcore/tolua_game.pkg
@@ -553,6 +553,10 @@ module Action {
 
 /* Module Specialist */
 module Specialist {
+  module properties {
+    bool api_methods_specialist_is_super
+      @ is_super (lua_State *L, Specialist *s);
+  }
   const char *api_methods_specialist_rule_name
     @ rule_name (lua_State *L, Specialist *self);
   const char *api_methods_specialist_name_translation
diff --git a/server/cityturn.c b/server/cityturn.c
index aab2462b17..fbf4f22fac 100644
--- a/server/cityturn.c
+++ b/server/cityturn.c
@@ -916,8 +916,9 @@ static void city_reset_foodbox(struct city *pcity, int new_size,
   Increase city size by one. We do not refresh borders or send info about
   the city to the clients as part of this function. There might be several
   calls to this function at once, and those actions are needed only once.
+  If s is not supplied, adds a specialist respecting the city preferences
 **************************************************************************/
-static bool city_increase_size(struct city *pcity)
+static bool city_increase_size(struct city *pcity, Specialist_type_id sid)
 {
   int new_food;
   int savings_pct = city_growth_granary_savings(pcity);
@@ -961,26 +962,31 @@ static bool city_increase_size(struct city *pcity)
   }
   pcity->food_stock = MIN(pcity->food_stock, new_food);
 
-  /* If there is enough food, and the city is big enough,
-   * make new citizens into scientists or taxmen -- Massimo */
+  if (sid >= 0) {
+    fc_assert_action(is_normal_specialist_id(sid), sid = DEFAULT_SPECIALIST);
+    pcity->specialists[sid]++;
+  } else {
+    /* If there is enough food, and the city is big enough,
+     * make new citizens into scientists or taxmen -- Massimo */
+
+    /* Ignore food if no square can be worked */
+    city_tile_iterate_skip_free_worked(nmap, city_map_radius_sq_get(pcity), pcenter,
+                                       ptile, _index, _x, _y) {
+      if (tile_worked(ptile) != pcity /* Quick test */
+       && city_can_work_tile(pcity, ptile)) {
+        have_square = TRUE;
+      }
+    } city_tile_iterate_skip_free_worked_end;
 
-  /* Ignore food if no square can be worked */
-  city_tile_iterate_skip_free_worked(nmap, city_map_radius_sq_get(pcity), pcenter,
-                                     ptile, _index, _x, _y) {
-    if (tile_worked(ptile) != pcity /* Quick test */
-     && city_can_work_tile(pcity, ptile)) {
-      have_square = TRUE;
+    if ((pcity->surplus[O_FOOD] >= 2 || !have_square)
+        && is_city_option_set(pcity, CITYO_SCIENCE_SPECIALISTS)) {
+      pcity->specialists[best_specialist(O_SCIENCE, pcity)]++;
+    } else if ((pcity->surplus[O_FOOD] >= 2 || !have_square)
+               && is_city_option_set(pcity, CITYO_GOLD_SPECIALISTS)) {
+      pcity->specialists[best_specialist(O_GOLD, pcity)]++;
+    } else {
+      pcity->specialists[DEFAULT_SPECIALIST]++; /* or else city is !sane */
     }
-  } city_tile_iterate_skip_free_worked_end;
-
-  if ((pcity->surplus[O_FOOD] >= 2 || !have_square)
-      && is_city_option_set(pcity, CITYO_SCIENCE_SPECIALISTS)) {
-    pcity->specialists[best_specialist(O_SCIENCE, pcity)]++;
-  } else if ((pcity->surplus[O_FOOD] >= 2 || !have_square)
-             && is_city_option_set(pcity, CITYO_GOLD_SPECIALISTS)) {
-    pcity->specialists[best_specialist(O_GOLD, pcity)]++;
-  } else {
-    pcity->specialists[DEFAULT_SPECIALIST]++; /* or else city is !sane */
   }
 
   /* Deprecated signal. Connect your lua functions to "city_size_change" that's
@@ -993,9 +999,12 @@ static bool city_increase_size(struct city *pcity)
 
 /**********************************************************************//**
   Do the city refresh after its size has increased, by any amount.
+  Any added citizens detected during check belong to nationality.
+  aaw means that the workers in the city will be auto-arranged.
 **************************************************************************/
 static void city_refresh_after_city_size_increase(struct city *pcity,
-                                                  struct player *nationality)
+                                                  struct player *nationality,
+                                                  bool aaw)
 {
   struct player *powner = city_owner(pcity);
 
@@ -1005,7 +1014,9 @@ static void city_refresh_after_city_size_increase(struct city *pcity,
   /* Refresh the city data; this also checks the squared city radius. */
   city_refresh(pcity);
 
-  auto_arrange_workers(pcity);
+  if (aaw) {
+    auto_arrange_workers(pcity);
+  }
 
   /* Update cities that have trade routes with us */
   trade_partners_iterate(pcity, pcity2) {
@@ -1027,9 +1038,14 @@ static void city_refresh_after_city_size_increase(struct city *pcity,
 
 /**********************************************************************//**
   Change the city size. Return TRUE iff the city is still alive afterwards.
+  If the size increases, the new citizens belong to nationality.
+  If sid is negative, tries to take best specialist according to city setting
+  but most times overwrites this selection in following auto-arrangement.
+  reason for signal ("script" or nullptr)
 **************************************************************************/
 bool city_change_size(struct city *pcity, citizens size,
-                      struct player *nationality, const char *reason)
+                      struct player *nationality,
+                      Specialist_type_id sid, const char *reason)
 {
   int change = size - city_size_get(pcity);
 
@@ -1040,7 +1056,7 @@ bool city_change_size(struct city *pcity, citizens size,
     int id = pcity->id;
 
     /* Increase city size until size reached, or increase fails */
-    while (size > current_size && city_increase_size(pcity)) {
+    while (size > current_size && city_increase_size(pcity, sid)) {
       /* TODO: This is currently needed only because there's
        *       deprecated script signal "city_growth" emitted.
        *       Check the need after signal has been dropped completely. */
@@ -1051,7 +1067,7 @@ bool city_change_size(struct city *pcity, citizens size,
       current_size++;
     }
 
-    city_refresh_after_city_size_increase(pcity, nationality);
+    city_refresh_after_city_size_increase(pcity, nationality, sid < 0);
 
     real_change = current_size - old_size;
 
@@ -1096,11 +1112,11 @@ static void city_populate(struct city *pcity, struct player *nationality)
     } else {
       bool success;
 
-      success = city_increase_size(pcity);
+      success = city_increase_size(pcity, -1);
       map_claim_border(pcity->tile, pcity->owner, -1);
 
       if (success) {
-        city_refresh_after_city_size_increase(pcity, nationality);
+        city_refresh_after_city_size_increase(pcity, nationality, TRUE);
         script_server_signal_emit("city_size_change", pcity,
                                   (lua_Integer)1, "growth");
       }
@@ -4198,10 +4214,10 @@ static bool do_city_migration(struct city *pcity_from,
 
   /* Increase size of receiver city */
   if (city_exist(to_id)) {
-    bool incr_success = city_increase_size(pcity_to);
+    bool incr_success = city_increase_size(pcity_to, -1);
 
     if (city_exist(to_id)) {
-      city_refresh_after_city_size_increase(pcity_to, pplayer_citizen);
+      city_refresh_after_city_size_increase(pcity_to, pplayer_citizen, TRUE);
       city_refresh_vision(pcity_to);
       if (city_refresh(pcity_to)) {
         auto_arrange_workers(pcity_to);
diff --git a/server/cityturn.h b/server/cityturn.h
index 9a2e503ef1..2e2f993074 100644
--- a/server/cityturn.h
+++ b/server/cityturn.h
@@ -32,8 +32,9 @@ void city_refresh_queue_processing(void);
 void auto_arrange_workers(struct city *pcity);        /* Will arrange the workers */
 void apply_cmresult_to_city(struct city *pcity, const struct cm_result *cmr);
 
-bool city_change_size(struct city *pcity, citizens new_size,
-                      struct player *nationality, const char *reason);
+bool city_change_size(struct city *pcity, citizens size,
+                      struct player *nationality,
+                      Specialist_type_id sid, const char *reason);
 bool city_reduce_size(struct city *pcity, citizens pop_loss,
                       struct player *destroyer, const char *reason);
 void city_repair_size(struct city *pcity, int change);
diff --git a/server/edithand.c b/server/edithand.c
index f6e69a27c6..eaa6dd8a12 100644
--- a/server/edithand.c
+++ b/server/edithand.c
@@ -731,7 +731,8 @@ void handle_edit_city_create(struct connection *pc, int owner, int tile,
 
   if (size > 1) {
     /* FIXME: Slow and inefficient for large size changes. */
-    city_change_size(pcity, CLIP(1, size, MAX_CITY_SIZE), pplayer, NULL);
+    city_change_size(pcity, CLIP(1, size, MAX_CITY_SIZE), pplayer,
+                     -1, nullptr);
     send_city_info(NULL, pcity);
   }
 
@@ -789,7 +790,7 @@ void handle_edit_city(struct connection *pc,
                   packet->size, city_link(pcity));
     } else {
       /* FIXME: Slow and inefficient for large size changes. */
-      city_change_size(pcity, packet->size, NULL, NULL);
+      city_change_size(pcity, packet->size, nullptr, -1, nullptr);
       changed = TRUE;
     }
   }
diff --git a/server/scripting/api_server_edit.c b/server/scripting/api_server_edit.c
index 57087d2643..cad8acdacc 100644
--- a/server/scripting/api_server_edit.c
+++ b/server/scripting/api_server_edit.c
@@ -778,15 +778,16 @@ void api_edit_remove_building(lua_State *L, City *pcity, Building_Type *impr)
 }
 
 /**********************************************************************//**
-  Reduce specialists of given type s in a way like toggling in the client.
+  Reduce specialists of given type s. Superspecialists are just reduced,
+  normal specialists are toggled in a way like toggling in the client.
   Does not place workers on map, just switches to another specialist.
   Does nothing if there is less than amount specialists s in pcity.
-  Return if given number could be repurposed.
+  Return if given number could be removed/repurposed.
 **************************************************************************/
 bool api_edit_city_reduce_specialists(lua_State *L, City *pcity,
                                       Specialist *s, int amount)
 {
-  Specialist_type_id from, to;
+  Specialist_type_id from;
 
   LUASCRIPT_CHECK_STATE(L, FALSE);
   LUASCRIPT_CHECK_SELF(L, pcity, FALSE);
@@ -797,28 +798,82 @@ bool api_edit_city_reduce_specialists(lua_State *L, City *pcity,
   if (pcity->specialists[from] < amount) {
     return FALSE;
   }
-  to = from;
-  do {
-    to = (to + 1) % specialist_count();
-  } while (to != from && !city_can_use_specialist(pcity, to));
 
-  if (to == from) {
-    /* We can use only the default specialist */
-    return FALSE;
-  } else {
-    /* City population must be correct */
-    fc_assert_ret_val_msg(pcity->specialists[to] <= MAX_CITY_SIZE - amount,
-                          FALSE, "Wrong specialist number in %s",
-                          city_name_get(pcity));
+  if (is_super_specialist_id(from)) {
+    /* Just reduce superspecialists */
     pcity->specialists[from] -= amount;
-    pcity->specialists[to] += amount;
+  } else {
+    /* Toggle normal specialist */
+    Specialist_type_id to = from;
+
+    do {
+      to = (to + 1) % normal_specialist_count();
+    } while (to != from && !city_can_use_specialist(pcity, to));
+
+    if (to == from) {
+      /* We can use only the default specialist */
+      return FALSE;
+    } else {
+      /* City population must be correct */
+      fc_assert_ret_val_msg(pcity->specialists[to] <= amount - MAX_CITY_SIZE,
+                            FALSE, "Wrong specialist number in %s",
+                            city_name_get(pcity));
+      pcity->specialists[from] -= amount;
+      pcity->specialists[to] += amount;
+    }
+  }
+
+  city_refresh(pcity);
+  /* sanity_check_city(pcity); -- hopefully we don't break things here? */
+  send_city_info(city_owner(pcity), pcity);
+
+  return TRUE;
+}
+
+/**********************************************************************//**
+  Add amount specialists of given type s to pcity, return true iff done.
+  For normal specialists, also increases city size at amount.
+  Fails if either pcity does not fulfill s->reqs or it does not have
+  enough space for given specialists or citizens number.
+**************************************************************************/
+bool api_edit_city_add_specialist(lua_State *L, City *pcity,
+                                  Specialist *s, int amount)
+{
+  Specialist_type_id sid;
+  int csize = 0;
 
+  LUASCRIPT_CHECK_STATE(L, FALSE);
+  LUASCRIPT_CHECK_SELF(L, pcity, FALSE);
+  LUASCRIPT_CHECK_ARG_NIL(L, s, 2, Specialist, FALSE);
+  LUASCRIPT_CHECK_ARG(L, amount >= 0, 3, "must be non-negative", FALSE);
+
+  sid = specialist_index(s);
+
+  if (!city_can_use_specialist(pcity, sid)) {
+    /* Can't employ this one */
+    return FALSE;
+  }
+  if (is_super_specialist(s)) {
+    if (pcity->specialists[sid] > MAX_CITY_SIZE - amount) {
+      /* No place for the specialist */
+      return FALSE;
+    }
+    pcity->specialists[sid] += amount;
     city_refresh(pcity);
-    /* sanity_check_city(pcity); -- hopefully we don't break things here? */
     send_city_info(city_owner(pcity), pcity);
+  } else {
+    csize = city_size_get(pcity);
 
-    return TRUE;
+    if (csize > MAX_CITY_SIZE - amount) {
+      /* No place for the specialist */
+      return FALSE;
+    }
+    city_change_size(pcity, csize + amount, city_owner(pcity), sid, "script");
+    city_refresh(pcity);
+    send_city_info(nullptr, pcity);
   }
+
+  return TRUE;
 }
 
 /**********************************************************************//**
@@ -1360,7 +1415,8 @@ void api_edit_change_city_size(lua_State *L, City *pcity, int change,
     nationality = city_owner(pcity);
   }
 
-  city_change_size(pcity, city_size_get(pcity) + change, nationality, "script");
+  city_change_size(pcity, city_size_get(pcity) + change, nationality,
+                   -1, "script");
 }
 
 /**********************************************************************//**
diff --git a/server/scripting/api_server_edit.h b/server/scripting/api_server_edit.h
index edd2aca00f..45c389a0f7 100644
--- a/server/scripting/api_server_edit.h
+++ b/server/scripting/api_server_edit.h
@@ -80,6 +80,8 @@ void api_edit_create_building(lua_State *L, City *pcity, Building_Type *impr);
 void api_edit_remove_building(lua_State *L, City *pcity, Building_Type *impr);
 bool api_edit_city_reduce_specialists(lua_State *L, City *pcity,
                                       Specialist *s, int amount);
+bool api_edit_city_add_specialist(lua_State *L, City *pcity,
+                                  Specialist *s, int amount);
 Player *api_edit_create_player(lua_State *L, const char *username,
                                Nation_Type *pnation, const char *ai);
 void api_edit_change_gold(lua_State *L, Player *pplayer, int amount);
diff --git a/server/scripting/tolua_server.pkg b/server/scripting/tolua_server.pkg
index 14d27045af..87ec1b7007 100644
--- a/server/scripting/tolua_server.pkg
+++ b/server/scripting/tolua_server.pkg
@@ -153,6 +153,9 @@ module edit {
   bool api_edit_city_reduce_specialists
     @ reduce_specialists (lua_State *L, City *pcity, Specialist *s,
                           int amount = 1);
+  bool api_edit_city_add_specialist
+    @ add_specialist (lua_State *L, City *pcity, Specialist *s,
+                      int amount = 1);
   void api_edit_create_owned_extra
     @ create_owned_extra (lua_State *L, Tile *ptile,
                           const char *name, Player *pplayer);
@@ -439,6 +442,10 @@ function City:reduce_specialists(spec, amount)
   return edit.reduce_specialists(self, spec, amount or 1)
 end
 
+function City:add_specialist(spec, amount)
+  return edit.add_specialist(self, spec, amount or 1)
+end
+
 function City:change_size(change, nationality)
   edit.change_city_size(self, change, nationality)
 end
diff --git a/server/unithand.c b/server/unithand.c
index e9945a04ee..0370768b5e 100644
--- a/server/unithand.c
+++ b/server/unithand.c
@@ -4304,7 +4304,7 @@ static bool city_build(struct player *pplayer, struct unit *punit,
 
     fc_assert_ret_val(pcity != NULL, FALSE);
 
-    city_change_size(pcity, size, nationality, NULL);
+    city_change_size(pcity, size, nationality, -1, nullptr);
   }
 
   /* May cause an incident even if the target tile is unclaimed. A ruleset
-- 
2.45.2

