From 31fb4aeaec3b6ff769d31261d42e8548cd0824c3 Mon Sep 17 00:00:00 2001
From: Ihnatus <ignatus31oct@mail.ru>
Date: Wed, 28 May 2025 00:53:06 +0300
Subject: [PATCH] Introduce superspecialists.

Allows defining superspecialists in ruleset, adding them manually to savefiles, and sending to clients.

See FCRM#1375.

Signed-off-by: Ihnatus <ignatus31oct@mail.ru>
---
 ai/default/daieffects.c       |  13 ++-
 ai/default/daimilitary.c      |   4 +-
 ai/default/daiunit.c          |   4 +-
 client/agents/cma_core.c      |  12 +--
 client/citydlg_common.c       |  33 +++++++-
 client/citydlg_common.h       |   2 +
 client/gui-gtk-3.22/citydlg.c |  17 +++-
 client/gui-gtk-4.0/citydlg.c  |  17 +++-
 client/gui-gtk-5.0/citydlg.c  |  17 +++-
 client/gui-qt/citydlg.cpp     |  28 ++++++-
 client/gui-sdl2/citydlg.c     |  12 ++-
 client/gui-sdl3/citydlg.c     |   4 +-
 client/helpdata.c             |   5 ++
 client/packhand.c             |   4 +-
 common/aicore/cm.c            |  13 +--
 common/city.c                 |  25 ++++--
 common/city.h                 |   1 +
 common/networking/packets.def |   1 +
 common/specialist.c           |  50 ++++++++++++
 common/specialist.h           |  26 ++++++
 server/cityhand.c             |   8 +-
 server/citytools.c            |   6 +-
 server/cityturn.c             |   8 +-
 server/ruleset/ruleload.c     | 146 +++++++++++++++++++++++-----------
 server/savegame/savegame3.c   |   9 ++-
 server/score.c                |   4 +-
 tools/ruleutil/rulesave.c     |   4 +-
 27 files changed, 369 insertions(+), 104 deletions(-)

diff --git a/ai/default/daieffects.c b/ai/default/daieffects.c
index d4b8cd9985..01e6e0471e 100644
--- a/ai/default/daieffects.c
+++ b/ai/default/daieffects.c
@@ -61,11 +61,11 @@ static int get_entertainers(const struct city *pcity)
 {
   int providers = 0;
 
-  specialist_type_iterate(i) {
+  normal_specialist_type_iterate(i) {
     if (get_specialist_output(pcity, i, O_LUXURY) >= game.info.happy_cost) {
       providers += pcity->specialists[i];
     }
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   return providers;
 }
@@ -855,14 +855,19 @@ bool dai_can_requirement_be_met_in_city(const struct requirement *preq,
 
   case VUT_SPECIALIST:
     if (preq->present) {
+      if (is_super_specialist(preq->source.value.specialist)
+          && pcity->specialists[specialist_index(preq->source.value.specialist)] > 0) {
+        /* The superspecialist won't leave */
+        break;
+      }
       requirement_vector_iterate(&(preq->source.value.specialist)->reqs,
                                  sreq) {
         if (!dai_can_requirement_be_met_in_city(sreq, pplayer, pcity)) {
           return FALSE;
         }
       } requirement_vector_iterate_end;
-    } /* It is always possible to remove a specialist. */
-  break;
+    } /* Almost always there can be a specialist other than given one */
+    break;
 
   case VUT_NATIONALITY:
     /* Crude, but the right answer needs to consider civil wars. */
diff --git a/ai/default/daimilitary.c b/ai/default/daimilitary.c
index 429a15420a..06fb9d7abd 100644
--- a/ai/default/daimilitary.c
+++ b/ai/default/daimilitary.c
@@ -1812,13 +1812,13 @@ struct adv_choice *military_advisor_choose_build(struct ai_type *ait,
   }
 
   if (!martial_need) {
-    specialist_type_iterate(sp) {
+    normal_specialist_type_iterate(sp) {
       if (pcity->specialists[sp] > 0
           && get_specialist_output(pcity, sp, O_LUXURY) > 0) {
         martial_need = TRUE;
         break;
       }
-    } specialist_type_iterate_end;
+    } normal_specialist_type_iterate_end;
   }
 
   if (martial_need
diff --git a/ai/default/daiunit.c b/ai/default/daiunit.c
index 0f88066a72..50d45ccf22 100644
--- a/ai/default/daiunit.c
+++ b/ai/default/daiunit.c
@@ -2788,11 +2788,11 @@ static void dai_set_defenders(struct ai_type *ait, struct player *pplayer)
     int entertainers = 0;
     bool enough = FALSE;
 
-    specialist_type_iterate(sp) {
+    normal_specialist_type_iterate(sp) {
       if (get_specialist_output(pcity, sp, O_LUXURY) > 0) {
         entertainers += pcity->specialists[sp];
       }
-    } specialist_type_iterate_end;
+    } normal_specialist_type_iterate_end;
 
     martless_unhappy += entertainers; /* We want to use martial law instead
                                        * of entertainers. */
diff --git a/client/agents/cma_core.c b/client/agents/cma_core.c
index 8da2091080..3959d2937c 100644
--- a/client/agents/cma_core.c
+++ b/client/agents/cma_core.c
@@ -107,9 +107,9 @@ static bool fc_results_are_equal(const struct cm_result *result1,
   T(disorder);
   T(happy);
 
-  specialist_type_iterate(sp) {
+  normal_specialist_type_iterate(sp) {
     T(specialists[sp]);
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   output_type_iterate(ot) {
     T(surplus[ot]);
@@ -219,7 +219,7 @@ static bool apply_result_on_server(struct city *pcity,
   } city_tile_iterate_skip_free_worked_end;
 
   /* Change the excess non-default specialists to default. */
-  specialist_type_iterate(sp) {
+  normal_specialist_type_iterate(sp) {
     int i;
 
     if (sp == DEFAULT_SPECIALIST) {
@@ -235,7 +235,7 @@ static bool apply_result_on_server(struct city *pcity,
         first_request_id = last_request_id;
       }
     }
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   /* now all surplus people are DEFAULT_SPECIALIST */
 
@@ -260,7 +260,7 @@ static bool apply_result_on_server(struct city *pcity,
 
   /* Set all specialists except DEFAULT_SPECIALIST (all the unchanged
    * ones remain as DEFAULT_SPECIALIST). */
-  specialist_type_iterate(sp) {
+  normal_specialist_type_iterate(sp) {
     int i;
 
     if (sp == DEFAULT_SPECIALIST) {
@@ -276,7 +276,7 @@ static bool apply_result_on_server(struct city *pcity,
         first_request_id = last_request_id;
       }
     }
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   if (last_request_id == 0 || ALWAYS_APPLY_AT_SERVER) {
       /*
diff --git a/client/citydlg_common.c b/client/citydlg_common.c
index 7e3c801e52..cf818facb2 100644
--- a/client/citydlg_common.c
+++ b/client/citydlg_common.c
@@ -1204,6 +1204,8 @@ void get_city_dialog_airlift_value(const struct city *pcity,
   should be the happiness index from FEELING_BASE to FEELING_FINAL.
   "categories" should be an array large enough to hold all citizens
   (use MAX_CITY_SIZE to be on the safe side).
+  Note that superspecialists are not included. There can be theoretically
+  times more superspecialists working than MAX_CITY_SIZE.
   Return number of categories filled (presumed equal to city size).
 **************************************************************************/
 int get_city_citizen_types(struct city *pcity, enum citizen_feeling idx,
@@ -1226,11 +1228,11 @@ int get_city_citizen_types(struct city *pcity, enum citizen_feeling idx,
     categories[i] = CITIZEN_ANGRY;
   }
 
-  specialist_type_iterate(sp) {
+  normal_specialist_type_iterate(sp) {
     for (n = 0; n < pcity->specialists[sp]; n++, i++) {
       categories[i] = CITIZEN_SPECIALIST + sp;
     }
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   if (city_size_get(pcity) != i) {
     log_error("get_city_citizen_types() %d citizens "
@@ -1241,6 +1243,29 @@ int get_city_citizen_types(struct city *pcity, enum citizen_feeling idx,
   return i;
 }
 
+/**********************************************************************//**
+  Try to fill city superspecialists into categories[cat_len] array.
+  If succeeds, return how many are filled.
+  If there are too many of them, fill as many as fits
+  and return negative value.
+**************************************************************************/
+int city_try_fill_superspecialists(struct city *pcity, int cat_len,
+                                   enum citizen_category *categories)
+{
+  int i = 0, n;
+
+  super_specialist_type_iterate(sp) {
+    for (n = 0; n < pcity->specialists[sp]; n++, i++) {
+      if (i >= cat_len) {
+        return -1;
+      }
+      categories[i] = CITIZEN_SPECIALIST + sp;
+    }
+  } super_specialist_type_iterate_end;
+
+  return i;
+}
+
 /**********************************************************************//**
   Rotate the given specialist citizen to the next type of citizen.
 **************************************************************************/
@@ -1259,9 +1284,9 @@ void city_rotate_specialist(struct city *pcity, int citizen_index)
   /* Loop through all specialists in order until we find a usable one
    * (or run out of choices). */
   to = from;
-  fc_assert(to >= 0 && to < specialist_count());
+  fc_assert(is_normal_specialist_id(to));
   do {
-    to = (to + 1) % specialist_count();
+    to = (to + 1) % normal_specialist_count();
   } while (to != from && !city_can_use_specialist(pcity, to));
 
   if (from != to) {
diff --git a/client/citydlg_common.h b/client/citydlg_common.h
index 1c9a5b4438..f4a529992d 100644
--- a/client/citydlg_common.h
+++ b/client/citydlg_common.h
@@ -68,6 +68,8 @@ void get_city_dialog_airlift_value(const struct city *pcity,
 
 int get_city_citizen_types(struct city *pcity, enum citizen_feeling index,
                            enum citizen_category *categories);
+int city_try_fill_superspecialists(struct city *pcity, int cat_len,
+                                   enum citizen_category *categories);
 void city_rotate_specialist(struct city *pcity, int citizen_index);
 
 void activate_all_units(struct tile *ptile);
diff --git a/client/gui-gtk-3.22/citydlg.c b/client/gui-gtk-3.22/citydlg.c
index e10e303212..b113ac9449 100644
--- a/client/gui-gtk-3.22/citydlg.c
+++ b/client/gui-gtk-3.22/citydlg.c
@@ -1873,8 +1873,19 @@ static void city_dialog_update_citizens(struct city_dialog *pdialog)
   int citizen_bar_width, citizen_bar_height;
   struct city *pcity = pdialog->pcity;
   int num_citizens = get_city_citizen_types(pcity, FEELING_FINAL, categories);
+  int num_supers
+    = city_try_fill_superspecialists(pcity,
+                                     ARRAY_SIZE(categories) - num_citizens,
+                                     &categories[num_citizens]);
   cairo_t *cr;
 
+  if (num_supers >= 0) {
+    /* Just draw superspecialists in the common roster */
+    num_citizens += num_supers;
+  } else {
+    /* FIXME: show them in some compact way */
+    num_citizens = ARRAY_SIZE(categories);
+  }
   /* If there is not enough space we stack the icons. We draw from left to */
   /* right. width is how far we go to the right for each drawn pixmap. The */
   /* last icon is always drawn in full, and so has reserved                */
@@ -1938,9 +1949,13 @@ static void city_dialog_update_information(GtkWidget **info_ebox,
   struct city *pcity = pdialog->pcity;
   int granaryturns;
   int non_workers = city_specialists(pcity);
+  int supers = city_superspecialists(pcity);
 
   /* fill the buffers with the necessary info */
-  if (non_workers) {
+  if (supers) {
+    fc_snprintf(buf[INFO_SIZE], sizeof(buf[INFO_SIZE]), "%3d (%3d+%d)",
+                pcity->size, non_workers, supers);
+  } else if (non_workers) {
     fc_snprintf(buf[INFO_SIZE], sizeof(buf[INFO_SIZE]), "%3d (%3d)",
                 pcity->size, non_workers);
   } else {
diff --git a/client/gui-gtk-4.0/citydlg.c b/client/gui-gtk-4.0/citydlg.c
index a3027dfd64..b83adadf21 100644
--- a/client/gui-gtk-4.0/citydlg.c
+++ b/client/gui-gtk-4.0/citydlg.c
@@ -1789,8 +1789,19 @@ static void city_dialog_update_citizens(struct city_dialog *pdialog)
   int citizen_bar_width, citizen_bar_height;
   struct city *pcity = pdialog->pcity;
   int num_citizens = get_city_citizen_types(pcity, FEELING_FINAL, categories);
+  int num_supers
+    = city_try_fill_superspecialists(pcity,
+                                     ARRAY_SIZE(categories) - num_citizens,
+                                     &categories[num_citizens]);
   cairo_t *cr;
 
+  if (num_supers >= 0) {
+    /* Just draw superspecialists in the common roster */
+    num_citizens += num_supers;
+  } else {
+    /* FIXME: show them in some compact way */
+    num_citizens = ARRAY_SIZE(categories);
+  }
   /* If there is not enough space we stack the icons. We draw from left to */
   /* right. width is how far we go to the right for each drawn pixmap. The */
   /* last icon is always drawn in full, and so has reserved                */
@@ -1925,9 +1936,13 @@ static void city_dialog_update_information(GtkWidget **info_label,
   struct city *pcity = pdialog->pcity;
   int granaryturns;
   int non_workers = city_specialists(pcity);
+  int supers = city_superspecialists(pcity);
 
   /* fill the buffers with the necessary info */
-  if (non_workers) {
+  if (supers) {
+    fc_snprintf(buf[INFO_SIZE], sizeof(buf[INFO_SIZE]), "%3d (%3d+%d)",
+                pcity->size, non_workers, supers);
+  } else if (non_workers) {
     fc_snprintf(buf[INFO_SIZE], sizeof(buf[INFO_SIZE]), "%3d (%3d)",
                 pcity->size, non_workers);
   } else {
diff --git a/client/gui-gtk-5.0/citydlg.c b/client/gui-gtk-5.0/citydlg.c
index a319355eb7..452bceaaeb 100644
--- a/client/gui-gtk-5.0/citydlg.c
+++ b/client/gui-gtk-5.0/citydlg.c
@@ -2078,8 +2078,19 @@ static void city_dialog_update_citizens(struct city_dialog *pdialog)
   int citizen_bar_width, citizen_bar_height;
   struct city *pcity = pdialog->pcity;
   int num_citizens = get_city_citizen_types(pcity, FEELING_FINAL, categories);
+  int num_supers
+    = city_try_fill_superspecialists(pcity,
+                                     ARRAY_SIZE(categories) - num_citizens,
+                                     &categories[num_citizens]);
   cairo_t *cr;
 
+  if (num_supers >= 0) {
+    /* Just draw superspecialists in the common roster */
+    num_citizens += num_supers;
+  } else {
+    /* FIXME: show them in some compact way */
+    num_citizens = ARRAY_SIZE(categories);
+  }
   /* If there is not enough space we stack the icons. We draw from left to */
   /* right. width is how far we go to the right for each drawn pixmap. The */
   /* last icon is always drawn in full, and so has reserved                */
@@ -2214,9 +2225,13 @@ static void city_dialog_update_information(GtkWidget **info_label,
   struct city *pcity = pdialog->pcity;
   int granaryturns;
   int non_workers = city_specialists(pcity);
+  int supers = city_superspecialists(pcity);
 
   /* fill the buffers with the necessary info */
-  if (non_workers) {
+  if (supers) {
+    fc_snprintf(buf[INFO_SIZE], sizeof(buf[INFO_SIZE]), "%3d (%3d+%d)",
+                pcity->size, non_workers, supers);
+  } else if (non_workers) {
     fc_snprintf(buf[INFO_SIZE], sizeof(buf[INFO_SIZE]), "%3d (%3d)",
                 pcity->size, non_workers);
   } else {
diff --git a/client/gui-qt/citydlg.cpp b/client/gui-qt/citydlg.cpp
index a2184d6257..f3ac533f8d 100644
--- a/client/gui-qt/citydlg.cpp
+++ b/client/gui-qt/citydlg.cpp
@@ -2895,9 +2895,21 @@ void city_dialog::update_citizens()
   QPainter p;
   QPixmap *pix;
   int num_citizens = get_city_citizen_types(dlgcity, FEELING_FINAL, categories);
+  int num_supers
+    = city_try_fill_superspecialists(dlgcity,
+                                     ARRAY_SIZE(categories) - num_citizens,
+                                     &categories[num_citizens]);
+
   int w = tileset_small_sprite_width(tileset) / gui()->map_scale;
   int h = tileset_small_sprite_height(tileset) / gui()->map_scale;
 
+  if (num_supers >= 0) {
+    /* Just draw superspecialists in the common roster */
+    num_citizens += num_supers;
+  } else {
+    /* FIXME: show them in some compact way */
+    num_citizens = ARRAY_SIZE(categories);
+  }
   i = 1 + (num_citizens * 5 / 200);
   w = w  / i;
   QRect source_rect(0, 0, w, h);
@@ -3130,7 +3142,7 @@ void city_dialog::update_info_label()
   char buf_info[NUM_INFO_FIELDS][512];
   char buf_tooltip[NUM_INFO_FIELDS][512];
   int granaryturns;
-  int spec;
+  int spec, supers;
 
   for (int i = 0; i < NUM_INFO_FIELDS; i++) {
     buf_info[i][0] = '\0';
@@ -3139,11 +3151,19 @@ void city_dialog::update_info_label()
 
   // Fill the buffers with the necessary info
   spec = city_specialists(dlgcity);
+  supers = city_superspecialists(dlgcity);
+
   fc_snprintf(buf_info[INFO_CITIZEN], sizeof(buf_info[INFO_CITIZEN]),
               "%3d (%4d)", dlgcity->size, spec);
-  fc_snprintf(buf_tooltip[INFO_CITIZEN], sizeof(buf_tooltip[INFO_CITIZEN]),
-              _("Population: %d, Specialists: %d"),
-              dlgcity->size, spec);
+  if (supers) {
+    fc_snprintf(buf_tooltip[INFO_CITIZEN], sizeof(buf_tooltip[INFO_CITIZEN]),
+                _("Population: %d, Specialists: %d + %d"),
+                dlgcity->size, spec, supers);
+  } else {
+    fc_snprintf(buf_tooltip[INFO_CITIZEN], sizeof(buf_tooltip[INFO_CITIZEN]),
+                _("Population: %d, Specialists: %d"),
+                dlgcity->size, spec);
+  }
   fc_snprintf(buf_info[INFO_FOOD], sizeof(buf_info[INFO_FOOD]), "%3d (%+4d)",
               dlgcity->prod[O_FOOD], dlgcity->surplus[O_FOOD]);
   fc_snprintf(buf_info[INFO_SHIELD], sizeof(buf_info[INFO_SHIELD]),
diff --git a/client/gui-sdl2/citydlg.c b/client/gui-sdl2/citydlg.c
index 5ecba8f682..85fbb9214b 100644
--- a/client/gui-sdl2/citydlg.c
+++ b/client/gui-sdl2/citydlg.c
@@ -3303,11 +3303,14 @@ static void redraw_city_dialog(struct city *pcity)
 
   /* count != 0 */
   /* ==================================================== */
-  /* Draw Citizens */
+  /* Draw Citizens and Superspecialists*/
   count = (pcity->feel[CITIZEN_HAPPY][FEELING_FINAL] + pcity->feel[CITIZEN_CONTENT][FEELING_FINAL]
            + pcity->feel[CITIZEN_UNHAPPY][FEELING_FINAL] + pcity->feel[CITIZEN_ANGRY][FEELING_FINAL]
-           + city_specialists(pcity));
+           + city_specialists(pcity) + city_superspecialists(pcity));
 
+  /* FIXME: at great counts of superspecialists, the roster gets too crowded. */
+  /* Currently, we just truncate the roster. */
+  count = MAX(count, MAX_CITY_SIZE);
   buf = get_citizen_surface(CITIZEN_HAPPY, 0);
 
   if (count > 13) {
@@ -3324,6 +3327,7 @@ static void redraw_city_dialog(struct city *pcity)
   pcity_dlg->spec_area.y = pwindow->dst->dest_rect.y + dest.y;
   pcity_dlg->spec_area.w = count * step;
   pcity_dlg->spec_area.h = buf->h;
+  limit = dest.x + pcity_dlg->spec_area.w - step; /* Max dest.x to draw */
 
   if (pcity->feel[CITIZEN_HAPPY][FEELING_FINAL]) {
     for (i = 0; i < pcity->feel[CITIZEN_HAPPY][FEELING_FINAL]; i++) {
@@ -3371,6 +3375,10 @@ static void redraw_city_dialog(struct city *pcity)
       buf = adj_surf(get_citizen_surface(CITIZEN_SPECIALIST + spe, i));
 
       for (i = 0; i < pcity->specialists[spe]; i++) {
+        if (dest.x > limit) {
+          /* No place to draw the rest */
+          break;
+        }
         alphablit(buf, NULL, pwindow->dst->surface, &dest, 255);
         dest.x += step;
       }
diff --git a/client/gui-sdl3/citydlg.c b/client/gui-sdl3/citydlg.c
index d6853cb508..dc5f3cf957 100644
--- a/client/gui-sdl3/citydlg.c
+++ b/client/gui-sdl3/citydlg.c
@@ -3299,10 +3299,10 @@ static void redraw_city_dialog(struct city *pcity)
 
   /* count != 0 */
   /* ==================================================== */
-  /* Draw Citizens */
+  /* Draw Citizens and Superspecialists*/
   count = (pcity->feel[CITIZEN_HAPPY][FEELING_FINAL] + pcity->feel[CITIZEN_CONTENT][FEELING_FINAL]
            + pcity->feel[CITIZEN_UNHAPPY][FEELING_FINAL] + pcity->feel[CITIZEN_ANGRY][FEELING_FINAL]
-           + city_specialists(pcity));
+           + city_specialists(pcity) + city_superspecialists(pcity));
 
   buf = get_citizen_surface(CITIZEN_HAPPY, 0);
 
diff --git a/client/helpdata.c b/client/helpdata.c
index 1361cbead5..2440fb1f48 100644
--- a/client/helpdata.c
+++ b/client/helpdata.c
@@ -4304,6 +4304,11 @@ void helptext_specialist(char *buf, size_t bufsz, struct player *pplayer,
       cat_snprintf(buf, bufsz, "%s\n\n", _(text));
     } strvec_iterate_end;
   }
+  if (is_super_specialist(pspec)) {
+    cat_snprintf(buf, bufsz,
+                 _("Superspecialist: is not counted within city population,"
+                   "\ncan not be assigned to or from another occupation.\n"));
+  }
 
   /* Requirements for this specialist. */
   requirement_vector_iterate(&pspec->reqs, preq) {
diff --git a/client/packhand.c b/client/packhand.c
index fde5fcba5d..b10fa75eaa 100644
--- a/client/packhand.c
+++ b/client/packhand.c
@@ -782,7 +782,9 @@ void handle_city_info(const struct packet_city_info *packet)
   }
   specialist_type_iterate(sp) {
     pcity->specialists[sp] = packet->specialists[sp];
-    city_size_add(pcity, pcity->specialists[sp]);
+    if (is_normal_specialist_id(sp)) {
+      city_size_add(pcity, pcity->specialists[sp]);
+    }
   } specialist_type_iterate_end;
 
   if (city_size_get(pcity) != packet->size) {
diff --git a/common/aicore/cm.c b/common/aicore/cm.c
index fe8c0ceff0..a1da00e5df 100644
--- a/common/aicore/cm.c
+++ b/common/aicore/cm.c
@@ -715,8 +715,9 @@ static void apply_solution(struct cm_state *state,
   fc_assert_ret(0 == soln->idle);
 
   /* Clear all specialists, and remove all workers from fields (except
-   * the city center). */
-  memset(&pcity->specialists, 0, sizeof(pcity->specialists));
+   * the city center). Don't touch superspecialists. */
+  memset(&pcity->specialists, 0,
+         sizeof(pcity->specialists[0]) * normal_specialist_count());
 
   city_map_iterate(city_radius_sq, cindex, x, y) {
     if (is_free_worked_index(cindex)) {
@@ -1038,7 +1039,7 @@ static void init_specialist_lattice_nodes(struct tile_type_vector *lattice,
 
   /* for each specialist type, create a tile_type that has as production
    * the bonus for the specialist (if the city is allowed to use it) */
-  specialist_type_iterate(i) {
+  normal_specialist_type_iterate(i) {
     if (city_can_use_specialist(pcity, i)) {
       type.spec = i;
       output_type_iterate(output) {
@@ -1047,7 +1048,7 @@ static void init_specialist_lattice_nodes(struct tile_type_vector *lattice,
 
       tile_type_lattice_add(lattice, &type, 0);
     }
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 }
 
 /************************************************************************//**
@@ -2240,9 +2241,9 @@ int cm_result_specialists(const struct cm_result *result)
 {
   int count = 0;
 
-  specialist_type_iterate(spec) {
+  normal_specialist_type_iterate(spec) {
     count += result->specialists[spec];
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   return count;
 }
diff --git a/common/city.c b/common/city.c
index a96997c40a..5d1b6f179c 100644
--- a/common/city.c
+++ b/common/city.c
@@ -3337,16 +3337,31 @@ int city_waste(const struct city *pcity, Output_type_id otype, int total,
 }
 
 /**********************************************************************//**
-  Give the number of specialists in a city.
+  Give the number of citizens who are specialists in a city.
+  Does not count superspecialists
 **************************************************************************/
 citizens city_specialists(const struct city *pcity)
 {
   citizens count = 0;
 
-  specialist_type_iterate(sp) {
+  normal_specialist_type_iterate(sp) {
     fc_assert_ret_val(MAX_CITY_SIZE - count > pcity->specialists[sp], 0);
     count += pcity->specialists[sp];
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
+
+  return count;
+}
+
+/**********************************************************************//**
+  Give the number of superspecialists in a city
+**************************************************************************/
+int city_superspecialists(const struct city *pcity)
+{
+  int count = 0;
+
+  super_specialist_type_iterate(sp) {
+    count += pcity->specialists[sp];
+  } super_specialist_type_iterate_end;
 
   return count;
 }
@@ -3362,7 +3377,7 @@ Specialist_type_id best_specialist(Output_type_id otype,
   int best = DEFAULT_SPECIALIST;
   int val = get_specialist_output(pcity, best, otype);
 
-  specialist_type_iterate(i) {
+  normal_specialist_type_iterate(i) {
     if (!pcity || city_can_use_specialist(pcity, i)) {
       int val2 = get_specialist_output(pcity, i, otype);
 
@@ -3371,7 +3386,7 @@ Specialist_type_id best_specialist(Output_type_id otype,
 	val = val2;
       }
     }
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   return best;
 }
diff --git a/common/city.h b/common/city.h
index c7bc22d6a8..04bcf5c7ae 100644
--- a/common/city.h
+++ b/common/city.h
@@ -577,6 +577,7 @@ void city_size_add(struct city *pcity, int add);
 void city_size_set(struct city *pcity, citizens size);
 
 citizens city_specialists(const struct city *pcity);
+int city_superspecialists(const struct city *pcity);
 
 citizens player_content_citizens(const struct player *pplayer);
 citizens player_angry_citizens(const struct player *pplayer);
diff --git a/common/networking/packets.def b/common/networking/packets.def
index b25e1ec648..805b5c5d53 100644
--- a/common/networking/packets.def
+++ b/common/networking/packets.def
@@ -1967,6 +1967,7 @@ PACKET_RULESET_CONTROL = 155; sc, lsend
   UINT16 num_city_styles;
   UINT16 terrain_count;
   UINT16 num_specialist_types;
+  UINT16 num_normal_specialists;
   UINT16 num_nation_groups;
   UINT16 num_nation_sets;
 
diff --git a/common/specialist.c b/common/specialist.c
index 73af9341b5..85b2726efa 100644
--- a/common/specialist.c
+++ b/common/specialist.c
@@ -73,6 +73,14 @@ Specialist_type_id specialist_count(void)
   return game.control.num_specialist_types;
 }
 
+/**********************************************************************//**
+  Return the number of normal specialist_types.
+**************************************************************************/
+Specialist_type_id normal_specialist_count(void)
+{
+  return game.control.num_normal_specialists;
+}
+
 /**********************************************************************//**
   Return the specialist index.
 
@@ -166,6 +174,48 @@ const char *specialist_abbreviation_translation(const struct specialist *sp)
   return name_translation_get(&sp->abbreviation);
 }
 
+/**********************************************************************//**
+  If this is a superspecialist: is not included into city population,
+  is not controlled by player.
+**************************************************************************/
+bool is_super_specialist_id(Specialist_type_id sp)
+{
+  if (sp >= game.control.num_specialist_types) {
+    return FALSE;
+  }
+  return sp >= game.control.num_normal_specialists;
+}
+
+/**********************************************************************//**
+  If this is a normal specialist: included into city population,
+  can be reassigned by player.
+**************************************************************************/
+bool is_normal_specialist_id(Specialist_type_id sp)
+{
+  if (sp < 0) {
+    return FALSE;
+  }
+  return sp < game.control.num_normal_specialists;
+}
+
+/**********************************************************************//**
+  If this is a superspecialist: is not included into city population,
+  is not controlled by player. Assumes a valid specialist pointer.
+**************************************************************************/
+bool is_super_specialist(const struct specialist *sp)
+{
+  return is_super_specialist_id(specialist_index(sp));
+}
+
+/**********************************************************************//**
+  If this is a normal specialist: included into city population,
+  can be reassigned by player. Assumes a valid specialist pointer.
+**************************************************************************/
+bool is_normal_specialist(const struct specialist *sp)
+{
+  return is_normal_specialist_id(specialist_index(sp));
+}
+
 /**********************************************************************//**
   Return a string containing all the specialist abbreviations, for instance
   "E/S/T".
diff --git a/common/specialist.h b/common/specialist.h
index 30dcb3976a..9bfb59918e 100644
--- a/common/specialist.h
+++ b/common/specialist.h
@@ -47,6 +47,7 @@ extern int default_specialist;
 
 /* General specialist accessor functions. */
 Specialist_type_id specialist_count(void);
+Specialist_type_id normal_specialist_count(void);
 Specialist_type_id specialist_index(const struct specialist *sp);
 Specialist_type_id specialist_number(const struct specialist *sp);
 
@@ -58,6 +59,11 @@ const char *specialist_rule_name(const struct specialist *sp);
 const char *specialist_plural_translation(const struct specialist *sp);
 const char *specialist_abbreviation_translation(const struct specialist *sp);
 
+bool is_super_specialist_id(Specialist_type_id sp);
+bool is_normal_specialist_id(Specialist_type_id sp);
+bool is_super_specialist(const struct specialist *sp);
+bool is_normal_specialist(const struct specialist *sp);
+
 /* Ancillary routines */
 const char *specialists_abbreviation_string(void);
 const char *specialists_string(const citizens *specialist_list);
@@ -80,6 +86,26 @@ void specialists_free(void);
   }                                                                         \
 }
 
+#define normal_specialist_type_iterate(sp)             \
+{                     \
+  Specialist_type_id sp;                \
+                                                                            \
+  for (sp = 0; sp < normal_specialist_count(); sp++) {
+
+#define normal_specialist_type_iterate_end                                         \
+  }                                                                         \
+}
+
+#define super_specialist_type_iterate(sp)             \
+{                     \
+  Specialist_type_id sp;                \
+                                                                            \
+  for (sp = normal_specialist_count(); sp < specialist_count(); sp++) {
+
+#define super_specialist_type_iterate_end                                         \
+  }                                                                         \
+}
+
 #define specialist_type_re_active_iterate(_p)                               \
   specialist_type_iterate(_p##_) {                                          \
     struct specialist *_p = specialist_by_number(_p##_);                    \
diff --git a/server/cityhand.c b/server/cityhand.c
index 13f6e55256..5457b0d39d 100644
--- a/server/cityhand.c
+++ b/server/cityhand.c
@@ -99,8 +99,8 @@ void handle_city_change_specialist(struct player *pplayer, int city_id,
     return;
   }
 
-  if (to < 0 || to >= specialist_count()
-      || from < 0 || from >= specialist_count()
+  if (!is_normal_specialist_id(to)
+      || !is_normal_specialist_id(from)
       || !city_can_use_specialist(pcity, to)
       || pcity->specialists[from] == 0) {
     /* This could easily just be due to clicking faster on the specialist
@@ -222,12 +222,12 @@ void handle_city_make_worker(struct player *pplayer,
 
   city_map_update_worker(pcity, ptile);
 
-  specialist_type_iterate(i) {
+  normal_specialist_type_iterate(i) {
     if (pcity->specialists[i] > 0) {
       pcity->specialists[i]--;
       break;
     }
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   city_refresh(pcity);
   sanity_check_city(pcity);
diff --git a/server/citytools.c b/server/citytools.c
index 27d35c6fbe..12c5fec86b 100644
--- a/server/citytools.c
+++ b/server/citytools.c
@@ -2536,7 +2536,7 @@ void package_city(struct city *pcity, struct packet_city_info *packet,
                   bool dipl_invest)
 {
   int i;
-  int ppl = 0;
+  int ppl = 0; /* Counter of citizens for sanity check */
 
   fc_assert(!pcity->server.needs_arrange);
 
@@ -2563,7 +2563,9 @@ void package_city(struct city *pcity, struct packet_city_info *packet,
   packet->specialists_size = specialist_count();
   specialist_type_iterate(sp) {
     packet->specialists[sp] = pcity->specialists[sp];
-    ppl += packet->specialists[sp];
+    if (is_normal_specialist_id(sp)) {
+      ppl += packet->specialists[sp];
+    }
   } specialist_type_iterate_end;
 
   /* The nationality of the citizens. */
diff --git a/server/cityturn.c b/server/cityturn.c
index 9c3abdcd77..aab2462b17 100644
--- a/server/cityturn.c
+++ b/server/cityturn.c
@@ -302,9 +302,9 @@ void apply_cmresult_to_city(struct city *pcity,
     }
   } city_tile_iterate_skip_free_worked_end;
 
-  specialist_type_iterate(sp) {
+  normal_specialist_type_iterate(sp) {
     pcity->specialists[sp] = cmr->specialists[sp];
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 }
 
 /**********************************************************************//**
@@ -721,12 +721,12 @@ static citizens city_reduce_specialists(struct city *pcity, citizens change)
 
   fc_assert_ret_val(0 < change, 0);
 
-  specialist_type_iterate(sp) {
+  normal_specialist_type_iterate(sp) {
     citizens fix = MIN(want, pcity->specialists[sp]);
 
     pcity->specialists[sp] -= fix;
     want -= fix;
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   return change - want;
 }
diff --git a/server/ruleset/ruleload.c b/server/ruleset/ruleload.c
index 87f2a072fc..67547b2c8c 100644
--- a/server/ruleset/ruleload.c
+++ b/server/ruleset/ruleload.c
@@ -104,6 +104,7 @@
 #define RESOURCE_SECTION_PREFIX "resource_"
 #define GOODS_SECTION_PREFIX "goods_"
 #define SPECIALIST_SECTION_PREFIX "specialist_"
+#define SUPER_SPECIALIST_SECTION_PREFIX "super_specialist_"
 #define TERRAIN_SECTION_PREFIX "terrain_"
 #define UNIT_CLASS_SECTION_PREFIX "unitclass_"
 #define UNIT_SECTION_PREFIX "unit_"
@@ -206,6 +207,9 @@ static bool load_ruleset_veteran(struct section_file *file,
                                  const char *path,
                                  struct veteran_system **vsystem, char *err,
                                  size_t err_len);
+static bool load_specialist(const struct section *psection,
+                            struct specialist *s,
+                            struct section_file *file);
 
 char *script_buffer = NULL;
 char *parser_buffer = NULL;
@@ -5820,6 +5824,49 @@ static bool load_muuk_as_action_auto(struct section_file *file,
                                       filename));
 }
 
+/**********************************************************************//**
+  Load a single specialist or superspecialist section.
+  Return if everything ran ok
+**************************************************************************/
+static bool load_specialist(const struct section *psection,
+                            struct specialist *s,
+                            struct section_file *file)
+{
+  struct requirement_vector *reqs;
+  const char *sec_name = section_name(psection);
+  const char *item, *tag;
+
+  if (!ruleset_load_names(&s->name, nullptr, file, sec_name)) {
+    return FALSE;
+  }
+
+  item = secfile_lookup_str_default(file, untranslated_name(&s->name),
+                                    "%s.short_name", sec_name);
+  name_set(&s->abbreviation, nullptr, item);
+
+  tag = secfile_lookup_str(file, "%s.graphic", sec_name);
+  if (nullptr == tag) {
+    ruleset_error(nullptr, LOG_ERROR,
+                  "\"%s\": No graphic tag for specialist at %s.",
+                  secfile_name(file), sec_name);
+    return FALSE;
+  }
+  sz_strlcpy(s->graphic_str, tag);
+  sz_strlcpy(s->graphic_alt,
+             secfile_lookup_str_default(file, "-",
+                                        "%s.graphic_alt", sec_name));
+
+  reqs = lookup_req_list(file, sec_name, "reqs", specialist_rule_name(s));
+  if (nullptr == reqs) {
+    return FALSE;
+  }
+  requirement_vector_copy(&s->reqs, reqs);
+
+  s->helptext = lookup_strvec(file, sec_name, "helptext");
+
+  return TRUE;
+}
+
 /**********************************************************************//**
   Load cities.ruleset file
 **************************************************************************/
@@ -5827,9 +5874,10 @@ static bool load_ruleset_cities(struct section_file *file,
                                 struct rscompat_info *compat)
 {
   const char *filename = secfile_name(file);
-  const char *item;
-  struct section_list *sec;
+  struct section_list *sec, *ssec;
+  int specs_count;
   bool ok = TRUE;
+  int i = 0;
 
   if (!rscompat_check_cap_and_version(file, filename, compat)) {
     return FALSE;
@@ -5840,57 +5888,38 @@ static bool load_ruleset_cities(struct section_file *file,
 
   /* Specialist options */
   sec = secfile_sections_by_name_prefix(file, SPECIALIST_SECTION_PREFIX);
+
   if (!sec) {
-    ruleset_error(NULL, LOG_ERROR, "\"%s\": No specialists.", filename);
-    ok = FALSE;
-  } else if (section_list_size(sec) >= SP_MAX) {
-    ruleset_error(NULL, LOG_ERROR,
-                  "\"%s\": Too many specialists (%d, max %d).",
-                  filename, section_list_size(sec), SP_MAX);
+    ruleset_error(nullptr, LOG_ERROR, "\"%s\": No normal specialists.", filename);
+
     ok = FALSE;
+  } else {
+    specs_count = section_list_size(sec);
+    ssec = secfile_sections_by_name_prefix(file, SUPER_SPECIALIST_SECTION_PREFIX);
+    /* Superspecialists are optional */
+    if (ssec) {
+      specs_count += section_list_size(ssec);
+    }
+    if (specs_count >= SP_MAX) {
+        ruleset_error(nullptr, LOG_ERROR,
+                      "\"%s\": Too many specialists (%d, max %d).",
+                      filename, specs_count, SP_MAX);
+        ok = FALSE;
+    }
   }
 
-  if (ok) {
-    int i = 0;
-    const char *tag;
 
-    game.control.num_specialist_types = section_list_size(sec);
+  if (ok) {
+    game.control.num_specialist_types = specs_count;
 
     section_list_iterate(sec, psection) {
       struct specialist *s = specialist_by_number(i);
-      struct requirement_vector *reqs;
-      const char *sec_name = section_name(psection);
 
-      if (!ruleset_load_names(&s->name, NULL, file, sec_name)) {
-        ok = FALSE;
-        break;
-      }
-
-      item = secfile_lookup_str_default(file, untranslated_name(&s->name),
-                                        "%s.short_name", sec_name);
-      name_set(&s->abbreviation, NULL, item);
-
-      tag = secfile_lookup_str(file, "%s.graphic", sec_name);
-      if (tag == NULL) {
-        ruleset_error(NULL, LOG_ERROR,
-                      "\"%s\": No graphic tag for specialist at %s.",
-                      filename, sec_name);
-        ok = FALSE;
-        break;
-      }
-      sz_strlcpy(s->graphic_str, tag);
-      sz_strlcpy(s->graphic_alt,
-                 secfile_lookup_str_default(file, "-",
-                                            "%s.graphic_alt", sec_name));
+      ok = load_specialist(psection, s, file);
 
-      reqs = lookup_req_list(file, sec_name, "reqs", specialist_rule_name(s));
-      if (reqs == NULL) {
-        ok = FALSE;
+      if (!ok) {
         break;
       }
-      requirement_vector_copy(&s->reqs, reqs);
-
-      s->helptext = lookup_strvec(file, sec_name, "helptext");
 
       if (requirement_vector_size(&s->reqs) == 0 && DEFAULT_SPECIALIST == -1) {
         DEFAULT_SPECIALIST = i;
@@ -5899,14 +5928,37 @@ static bool load_ruleset_cities(struct section_file *file,
     } section_list_iterate_end;
   }
 
-  if (ok && DEFAULT_SPECIALIST == -1) {
-    ruleset_error(NULL, LOG_ERROR,
-                  "\"%s\": must have zero reqs for at least one "
-                  "specialist type.", filename);
-    ok = FALSE;
+  if (ok){
+    if (DEFAULT_SPECIALIST == -1) {
+      ruleset_error(nullptr, LOG_ERROR,
+                    "\"%s\": must have zero reqs for at least one "
+                    "specialist type.", filename);
+      ok = FALSE;
+    } else {
+      game.control.num_normal_specialists = i;
+    }
   }
   section_list_destroy(sec);
-  sec = NULL;
+  sec = nullptr;
+
+  if (ssec) {
+    if (ok) {
+      section_list_iterate(ssec, psection) {
+        struct specialist *s = specialist_by_number(i);
+
+        ok = load_specialist(psection, s, file);
+
+        if (!ok) {
+          break;
+        }
+
+        i++;
+      } section_list_iterate_end;
+    }
+
+    section_list_destroy(ssec);
+    ssec = nullptr;
+  }
 
   if (ok) {
     /* City Parameters */
diff --git a/server/savegame/savegame3.c b/server/savegame/savegame3.c
index 8152e5faa7..6dcbfa640a 100644
--- a/server/savegame/savegame3.c
+++ b/server/savegame/savegame3.c
@@ -5209,12 +5209,15 @@ static bool sg_load_player_city(struct loaddata *loading, struct player *plr,
   city_size_set(pcity, size);
 
   for (i = 0; i < loading->specialist.size; i++) {
+    Specialist_type_id si = specialist_index(loading->specialist.order[i]);
+
     sg_warn_ret_val(secfile_lookup_int(loading->file, &value, "%s.nspe%d",
                                        citystr, i),
                     FALSE, "%s", secfile_error());
-    pcity->specialists[specialist_index(loading->specialist.order[i])]
-      = (citizens)value;
-    sp_count += value;
+    pcity->specialists[si] = (citizens)value;
+    if (is_normal_specialist_id(si)) {
+      sp_count += value;
+    }
   }
 
   partner = secfile_lookup_int_default(loading->file, 0, "%s.traderoute0", citystr);
diff --git a/server/score.c b/server/score.c
index ca53caf1ad..b9ed50b242 100644
--- a/server/score.c
+++ b/server/score.c
@@ -386,9 +386,9 @@ int total_player_citizens(const struct player *pplayer)
                + pplayer->score.unhappy
                + pplayer->score.angry);
 
-  specialist_type_iterate(sp) {
+  normal_specialist_type_iterate(sp) {
     count += pplayer->score.specialists[sp];
-  } specialist_type_iterate_end;
+  } normal_specialist_type_iterate_end;
 
   return count;
 }
diff --git a/tools/ruleutil/rulesave.c b/tools/ruleutil/rulesave.c
index 1cbe6bfbdf..82dcdec9b0 100644
--- a/tools/ruleutil/rulesave.c
+++ b/tools/ruleutil/rulesave.c
@@ -685,7 +685,9 @@ static bool save_cities_ruleset(const char *filename, const char *name)
     struct specialist *s = specialist_by_number(sp);
     char path[512];
 
-    fc_snprintf(path, sizeof(path), "specialist_%d", sect_idx++);
+    fc_snprintf(path, sizeof(path),
+                is_super_specialist_id(sp)
+                ? "super_specialist_%d" : "specialist_%d", sect_idx++);
 
     save_name_translation(sfile, &(s->name), path);
 
-- 
2.45.2

