From b1606441acf7ffb447b5826fdd6894e4531e66c0 Mon Sep 17 00:00:00 2001
From: Ihnatus <ignatus31oct@mail.ru>
Date: Sun, 29 Jun 2025 02:30:40 +0300
Subject: [PATCH] Add specialist unit typee property

See FCRM#1546

Signed-off-by: Ihnatus <ignatus31oct@mail.ru>
---
 client/helpdata.c             | 38 ++++++++++++++++++++++++-----------
 client/packhand.c             |  1 +
 common/actions.c              |  5 +++--
 common/actres.c               | 17 ++++++++++++++++
 common/networking/packets.def |  1 +
 common/unittype.h             |  2 ++
 server/ruleset/ruleload.c     | 16 +++++++++++++++
 server/unithand.c             | 37 ++++++++++++++++++++++++----------
 tools/ruleutil/rulesave.c     |  4 ++++
 9 files changed, 96 insertions(+), 25 deletions(-)

diff --git a/client/helpdata.c b/client/helpdata.c
index 93608edf86..e935bac2b4 100644
--- a/client/helpdata.c
+++ b/client/helpdata.c
@@ -2904,20 +2904,34 @@ char *helptext_unit(char *buf, size_t bufsz, struct player *pplayer,
                          "  %s initial population: %d.\n",
                          utype->city_size),
                      BULLET, utype->city_size);
+        if (is_super_specialist(utype->spec_type)) {
+          cat_snprintf(buf, bufsz,
+                       /* TRANS: * ... Great Artist ... */
+                       _("  %s the city starts with a %s superspecialist.\n"),
+                       BULLET, specialist_plural_translation(utype->spec_type));
+        }
         break;
       case ACTRES_JOIN_CITY:
-        cat_snprintf(buf, bufsz,
-                     /* TRANS: the %d is population. */
-                     PL_("  %s max target size: %d.\n",
-                         "  %s max target size: %d.\n",
-                         game.info.add_to_size_limit - utype->pop_cost),
-                     BULLET, game.info.add_to_size_limit - utype->pop_cost);
-        cat_snprintf(buf, bufsz,
-                     /* TRANS: the %d is the population added. */
-                     PL_("  %s adds %d population.\n",
-                         "  %s adds %d population.\n",
-                         utype->pop_cost),
-                     BULLET, utype->pop_cost);
+        if (utype->pop_cost > 0 ){
+          cat_snprintf(buf, bufsz,
+                       /* TRANS: the %d is population. */
+                       PL_("  %s max target size: %d.\n",
+                           "  %s max target size: %d.\n",
+                           game.info.add_to_size_limit - utype->pop_cost),
+                       BULLET, game.info.add_to_size_limit - utype->pop_cost);
+          cat_snprintf(buf, bufsz,
+                       /* TRANS: the %d is the population added. */
+                       PL_("  %s adds %d population.\n",
+                           "  %s adds %d population.\n",
+                           utype->pop_cost),
+                       BULLET, utype->pop_cost);
+        }
+        if (is_super_specialist(utype->spec_type)) {
+          cat_snprintf(buf, bufsz,
+                       /* TRANS: * ... Great Artist ... */
+                       _("  %s adds a %s superspecialist to the city.\n"),
+                       BULLET, specialist_plural_translation(utype->spec_type));
+        }
         break;
       case ACTRES_BOMBARD:
         cat_snprintf(buf, bufsz,
diff --git a/client/packhand.c b/client/packhand.c
index 29406b5d58..fbbff93dfd 100644
--- a/client/packhand.c
+++ b/client/packhand.c
@@ -3747,6 +3747,7 @@ void handle_ruleset_unit(const struct packet_ruleset_unit *p)
   u->uclass             = uclass_by_number(p->unit_class_id);
   u->build_cost         = p->build_cost;
   u->pop_cost           = p->pop_cost;
+  u->spec_type          = specialist_by_number(p->spectype_id);
   u->attack_strength    = p->attack_strength;
   u->defense_strength   = p->defense_strength;
   u->move_rate          = p->move_rate;
diff --git a/common/actions.c b/common/actions.c
index 4adabda34f..99680d5da6 100644
--- a/common/actions.c
+++ b/common/actions.c
@@ -32,6 +32,7 @@
 #include "oblig_reqs.h"
 #include "research.h"
 #include "server_settings.h"
+#include "specialist.h"
 #include "unit.h"
 
 
@@ -2401,8 +2402,8 @@ action_actor_utype_hard_reqs_ok_full(const struct action *paction,
 {
   switch (paction->result) {
   case ACTRES_JOIN_CITY:
-    if (actor_unittype->pop_cost <= 0) {
-      /* Reason: Must have population to add. */
+    if (actor_unittype->pop_cost <= 0 && !is_super_specialist(actor_unittype->spec_type)) {
+      /* Reason: Must have something to add. */
       return FALSE;
     }
     break;
diff --git a/common/actres.c b/common/actres.c
index ae95369ef7..3b4d16daf3 100644
--- a/common/actres.c
+++ b/common/actres.c
@@ -25,6 +25,7 @@
 #include "movement.h"
 #include "player.h"
 #include "requirements.h"
+#include "specialist.h"
 #include "tile.h"
 #include "traderoutes.h"
 
@@ -1009,6 +1010,7 @@ enum fc_tristate actres_possible(const struct civ_map *nmap,
   case ACTRES_JOIN_CITY:
     {
       int new_pop;
+      Specialist_type_id sid;
 
       if (!omniscient
           && !player_can_see_city_externals(actor->player, target->city)) {
@@ -1031,6 +1033,21 @@ enum fc_tristate actres_possible(const struct civ_map *nmap,
          * VisibleByOthers. */
         return TRI_NO;
       }
+
+      sid = specialist_index(unit_type_get(actor->unit)->spec_type);
+      if (DEFAULT_SPECIALIST != sid) {
+        if (!city_can_use_specialist(target->city, sid)) {
+          /* Respect specialist reqs */
+          /* Potential info leak about if they are fulfilled */
+          return TRI_NO;
+        }
+        if (is_super_specialist_id(sid)
+            && target->city->specialists[sid] >= MAX_CITY_SIZE) {
+          /* No place to add a superspecialist */
+          /* Info leak on city superspecialists but it happens too rarely */
+          return TRI_NO;
+        }
+      }
     }
 
     break;
diff --git a/common/networking/packets.def b/common/networking/packets.def
index 4f72d162c5..545373f3f4 100644
--- a/common/networking/packets.def
+++ b/common/networking/packets.def
@@ -1418,6 +1418,7 @@ PACKET_RULESET_UNIT = 140; sc, lsend
   UINT8 unit_class_id;
   UINT16 build_cost;
   UINT8 pop_cost;
+  UINT8 spectype_id;
   UINT8 attack_strength;
   UINT8 defense_strength;
   MOVEFRAGS move_rate;
diff --git a/common/unittype.h b/common/unittype.h
index eabfa79748..688324585f 100644
--- a/common/unittype.h
+++ b/common/unittype.h
@@ -517,6 +517,8 @@ struct unit_type {
   int build_cost;                       /* Use wrappers to access this. */
   int pop_cost;                         /* Number of workers the unit contains
                                          * (e.g., settlers, engineers) */
+  struct specialist *spec_type; /* affects only founding and adding to cities */
+
   int attack_strength;
   int defense_strength;
   int move_rate;
diff --git a/server/ruleset/ruleload.c b/server/ruleset/ruleload.c
index aa7b97b85e..bfb741e90e 100644
--- a/server/ruleset/ruleload.c
+++ b/server/ruleset/ruleload.c
@@ -2406,6 +2406,21 @@ static bool load_ruleset_units(struct section_file *file,
                                                  "%s.city_slots", sec_name);
       u->city_size = secfile_lookup_int_default(file, 1,
                                                 "%s.city_size", sec_name);
+      if ((sval = secfile_lookup_str(file, "%s.specialist", sec_name))) {
+        if (!(u->spec_type = specialist_by_rule_name(sval))) {
+          ruleset_error(nullptr, LOG_ERROR,
+                        "\"%s\" unit_type \"%s\":"
+                        " bad specialist \"%s\".",
+                        filename, utype_rule_name(u), sval);
+          ok = FALSE;
+        }
+      } else {
+        /* Specialists must have been processed before */
+        fc_assert_action(DEFAULT_SPECIALIST >= 0
+                         && specialist_by_number(DEFAULT_SPECIALIST),
+                         ok = FALSE; break);
+        u->spec_type = specialist_by_number(DEFAULT_SPECIALIST);
+      }
 
       sval = secfile_lookup_str_default(file, transp_def_type_name(TDT_ALIGHT),
                                         "%s.tp_defense", sec_name);
@@ -8068,6 +8083,7 @@ static void send_ruleset_units(struct conn_list *dest)
     packet.unit_class_id = uclass_number(utype_class(u));
     packet.build_cost = u->build_cost;
     packet.pop_cost = u->pop_cost;
+    packet.spectype_id = specialist_index(u->spec_type);
     packet.attack_strength = u->attack_strength;
     packet.defense_strength = u->defense_strength;
     packet.move_rate = u->move_rate;
diff --git a/server/unithand.c b/server/unithand.c
index 0370768b5e..941e2fcd2f 100644
--- a/server/unithand.c
+++ b/server/unithand.c
@@ -4214,6 +4214,7 @@ static bool city_add_unit(struct player *pplayer, struct unit *punit,
 {
   int amount = unit_pop_value(punit);
   const struct unit_type *act_utype;
+  Specialist_type_id spec_id = DEFAULT_SPECIALIST;
 
   /* Sanity check: The actor is still alive. */
   fc_assert_ret_val(punit, FALSE);
@@ -4223,11 +4224,21 @@ static bool city_add_unit(struct player *pplayer, struct unit *punit,
   /* Sanity check: The target city still exists. */
   fc_assert_ret_val(pcity, FALSE);
 
-  fc_assert_ret_val(amount > 0, FALSE);
-
   city_size_add(pcity, amount);
+  if (is_super_specialist(act_utype->spec_type)) {
+    Specialist_type_id sspec = specialist_index(act_utype->spec_type);
+
+    fc_assert_ret_val(pcity->specialists[sspec] < MAX_CITY_SIZE, FALSE);
+    pcity->specialists[sspec]++;
+  } else {
+    fc_assert_ret_val(amount > 0, FALSE);
+    /* Hardly much needed but let it be */
+    spec_id = specialist_index(act_utype->spec_type);
+  }
   /* Make the new people something, otherwise city fails the checks */
-  pcity->specialists[DEFAULT_SPECIALIST] += amount;
+  fc_assert_ret_val(MAX_CITY_SIZE - pcity->specialists[spec_id] >= amount,
+                    FALSE);
+  pcity->specialists[spec_id] += amount;
   citizens_update(pcity, unit_nationality(punit));
   /* Refresh the city data. */
   city_refresh(pcity);
@@ -4280,6 +4291,8 @@ static bool city_build(struct player *pplayer, struct unit *punit,
   struct player *nationality;
   struct player *towner;
   const struct unit_type *act_utype;
+  Specialist_type_id sid;
+  struct city *pcity;
 
   /* Sanity check: The actor still exists. */
   fc_assert_ret_val(pplayer, FALSE);
@@ -4294,17 +4307,19 @@ static bool city_build(struct player *pplayer, struct unit *punit,
   }
 
   act_utype = unit_type_get(punit);
-
+  sid = specialist_index(act_utype->spec_type);
   nationality = unit_nationality(punit);
 
   create_city(pplayer, ptile, name, nationality);
-  size = unit_type_get(punit)->city_size;
-  if (size > 1) {
-    struct city *pcity = tile_city(ptile);
-
-    fc_assert_ret_val(pcity != NULL, FALSE);
-
-    city_change_size(pcity, size, nationality, -1, nullptr);
+  size = act_utype->city_size;
+  pcity = tile_city(ptile); /* A callback may destroy or replace it any time */
+  if (nullptr != pcity) {
+    if (is_super_specialist_id(sid) && pcity->specialists[sid] < MAX_CITY_SIZE) {
+      pcity->specialists[sid]++;
+    }
+    if (size > 1) {
+      city_change_size(pcity, size, nationality, -1, nullptr);
+    }
   }
 
   /* May cause an incident even if the target tile is unclaimed. A ruleset
diff --git a/tools/ruleutil/rulesave.c b/tools/ruleutil/rulesave.c
index 3ec71eadaa..71e0c703bc 100644
--- a/tools/ruleutil/rulesave.c
+++ b/tools/ruleutil/rulesave.c
@@ -3266,6 +3266,10 @@ static bool save_units_ruleset(const char *filename, const char *name)
 
       secfile_insert_int(sfile, put->build_cost, "%s.build_cost", path);
       secfile_insert_int(sfile, put->pop_cost, "%s.pop_cost", path);
+      if (DEFAULT_SPECIALIST < 0 || DEFAULT_SPECIALIST != specialist_index(put->spec_type)) {
+        secfile_insert_str(sfile, specialist_rule_name(put->spec_type),
+                           "%s.specialist", path);
+      }
       secfile_insert_int(sfile, put->attack_strength, "%s.attack", path);
       secfile_insert_int(sfile, put->defense_strength, "%s.defense", path);
       secfile_insert_int(sfile, put->move_rate / SINGLE_MOVE, "%s.move_rate", path);
-- 
2.45.2

