From 770233f564f227da47340215c5c75219b6e37630 Mon Sep 17 00:00:00 2001
From: Ihnatus <ignatus31oct@mail.ru>
Date: Sun, 29 Jun 2025 23:29:05 +0300
Subject: [PATCH] Add specialist subtarget kind

See FCRM#1541

Signed-off-by: Ihnatus <ignatus31oct@mail.ru>
---
 client/gui-gtk-3.22/action_dialog.c | 21 ++++++++++++++++-----
 client/gui-gtk-3.22/menu.c          | 17 +++++++++++++++++
 client/gui-gtk-4.0/action_dialog.c  | 21 ++++++++++++++++-----
 client/gui-gtk-4.0/menu.c           | 17 +++++++++++++++++
 client/gui-gtk-5.0/action_dialog.c  | 21 ++++++++++++++++-----
 client/gui-gtk-5.0/menu.c           | 17 +++++++++++++++++
 client/gui-qt/dialogs.cpp           |  1 +
 client/gui-qt/menu.cpp              |  7 +++++++
 client/gui-sdl2/action_dialog.c     |  9 +++++++++
 client/gui-sdl3/action_dialog.c     |  9 +++++++++
 common/actres.h                     |  2 ++
 common/unit.c                       |  8 ++++++++
 server/actiontools.c                |  5 +++++
 server/savegame/savegame3.c         | 10 ++++++++++
 server/unithand.c                   |  8 ++++++++
 15 files changed, 158 insertions(+), 15 deletions(-)

diff --git a/client/gui-gtk-3.22/action_dialog.c b/client/gui-gtk-3.22/action_dialog.c
index f3124b4320..c228e6ed32 100644
--- a/client/gui-gtk-3.22/action_dialog.c
+++ b/client/gui-gtk-3.22/action_dialog.c
@@ -27,6 +27,7 @@
 #include "traderoutes.h"
 #include "movement.h"
 #include "research.h"
+#include "specialist.h"
 #include "unit.h"
 #include "unitlist.h"
 
@@ -87,6 +88,7 @@ struct action_data {
   int target_building_id;
   int target_tech_id;
   int target_extra_id;
+  int target_specialist_id;
 };
 
 /* TODO: maybe this should be in the dialog itself? */
@@ -103,6 +105,7 @@ static struct action_data *act_data(action_id act_id,
                                     int target_tile_id,
                                     int target_building_id,
                                     int target_tech_id,
+                                    int target_specialist_id,
                                     int tgt_extra_id)
 {
   struct action_data *data = fc_malloc(sizeof(*data));
@@ -114,6 +117,7 @@ static struct action_data *act_data(action_id act_id,
   data->target_tile_id = target_tile_id;
   data->target_building_id = target_building_id;
   data->target_tech_id = target_tech_id;
+  data->target_specialist_id = target_specialist_id;
   data->target_extra_id = tgt_extra_id;
 
   return data;
@@ -319,6 +323,13 @@ static void simple_action_callback(GtkWidget *w, gpointer data)
         failed = TRUE;
       }
       break;
+    case ASTK_SPECIALIST:
+      sub_target = args->target_specialist_id;
+      if (NULL == specialist_by_number(sub_target)) {
+        /* Did the ruleset change? */
+        failed = TRUE;
+      }
+      break;
     case ASTK_NONE:
     case ASTK_COUNT:
       /* Shouldn't happen. */
@@ -526,7 +537,7 @@ void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(bribe_unit_response),
                    act_data(paction->id, actor->id,
                             0, punit->id, 0,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -565,7 +576,7 @@ void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(bribe_stack_response),
                    act_data(paction->id, actor->id,
                             0, 0, ptile->index,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -994,7 +1005,7 @@ void popup_sabotage_dialog(struct unit *actor, struct city *pcity,
     create_improvements_list(client.conn.playing, pcity,
                              act_data(paction->id,
                                       actor->id, pcity->id, 0, 0,
-                                      0, 0, 0));
+                                      0, 0, 0, 0));
     gtk_window_present(GTK_WINDOW(spy_sabotage_shell));
   }
 }
@@ -1062,7 +1073,7 @@ void popup_incite_dialog(struct unit *actor, struct city *pcity, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(incite_response),
                    act_data(paction->id, actor->id,
                             pcity->id, 0, 0,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -1415,7 +1426,7 @@ void popup_action_selection(struct unit *actor_unit,
                (target_unit) ? target_unit->id : IDENTITY_NUMBER_ZERO,
                (target_tile) ? target_tile->index : TILE_INDEX_NONE,
                /* No target_building or target_tech supplied. (Dec 2019) */
-               B_LAST, A_UNSET,
+               B_LAST, A_UNSET, -1,
                target_extra ? target_extra->id : EXTRA_NONE);
 
   /* Could be caused by the server failing to reply to a request for more
diff --git a/client/gui-gtk-3.22/menu.c b/client/gui-gtk-3.22/menu.c
index 22ce2b3a5b..4568948f8f 100644
--- a/client/gui-gtk-3.22/menu.c
+++ b/client/gui-gtk-3.22/menu.c
@@ -29,6 +29,7 @@
 #include "game.h"
 #include "government.h"
 #include "road.h"
+#include "specialist.h"
 #include "unit.h"
 
 /* client */
@@ -1633,6 +1634,14 @@ static void unit_goto_and_callback(GtkMenuItem *item, gpointer data)
       sub_target = extra_number(pextra);
     }
     break;
+  case ASTK_SPECIALIST:
+    {
+      struct specialist *pspec = g_object_get_data(G_OBJECT(item),
+                                                   "end_specialist");
+      fc_assert_ret(nullptr != pspec);
+      sub_target = specialist_number(pspec);
+    }
+    break;
   case ASTK_NONE:
     sub_target = NO_TARGET;
     break;
@@ -3113,6 +3122,14 @@ void real_menus_init(void)
                               extra_name_translation(pextra));
             } extra_type_iterate_end;
             break;
+          case ASTK_SPECIALIST:
+            specialist_type_iterate(spc) {
+              struct specialist *pspec = specialist_by_number(spc);
+
+              CREATE_SUB_ITEM(pspec, "end_specialist",
+                              specialist_plural_translation(pspec));
+            } specialist_type_iterate_end;
+            break;
           case ASTK_NONE:
             /* Should not be here. */
             fc_assert(action_get_sub_target_kind(paction) != ASTK_NONE);
diff --git a/client/gui-gtk-4.0/action_dialog.c b/client/gui-gtk-4.0/action_dialog.c
index af12630eac..3a0f3d6c07 100644
--- a/client/gui-gtk-4.0/action_dialog.c
+++ b/client/gui-gtk-4.0/action_dialog.c
@@ -27,6 +27,7 @@
 #include "traderoutes.h"
 #include "movement.h"
 #include "research.h"
+#include "specialist.h"
 #include "unit.h"
 #include "unitlist.h"
 
@@ -87,6 +88,7 @@ struct action_data {
   int target_building_id;
   int target_tech_id;
   int target_extra_id;
+  int target_specialist_id;
 };
 
 /* TODO: Maybe this should be in the dialog itself? */
@@ -103,6 +105,7 @@ static struct action_data *act_data(action_id act_id,
                                     int target_tile_id,
                                     int target_building_id,
                                     int target_tech_id,
+                                    int target_specialist_id,
                                     int tgt_extra_id)
 {
   struct action_data *data = fc_malloc(sizeof(*data));
@@ -114,6 +117,7 @@ static struct action_data *act_data(action_id act_id,
   data->target_tile_id = target_tile_id;
   data->target_building_id = target_building_id;
   data->target_tech_id = target_tech_id;
+  data->target_specialist_id = target_specialist_id;
   data->target_extra_id = tgt_extra_id;
 
   return data;
@@ -319,6 +323,13 @@ static void simple_action_callback(GtkWidget *w, gpointer data)
         failed = TRUE;
       }
       break;
+    case ASTK_SPECIALIST:
+      sub_target = args->target_specialist_id;
+      if (NULL == specialist_by_number(sub_target)) {
+        /* Did the ruleset change? */
+        failed = TRUE;
+      }
+      break;
     case ASTK_NONE:
     case ASTK_COUNT:
       /* Shouldn't happen. */
@@ -525,7 +536,7 @@ void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(bribe_unit_response),
                    act_data(paction->id, actor->id,
                             0, punit->id, 0,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -564,7 +575,7 @@ void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(bribe_stack_response),
                    act_data(paction->id, actor->id,
                             0, 0, ptile->index,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -991,7 +1002,7 @@ void popup_sabotage_dialog(struct unit *actor, struct city *pcity,
     create_improvements_list(client.conn.playing, pcity,
                              act_data(paction->id,
                                       actor->id, pcity->id, 0, 0,
-                                      0, 0, 0));
+                                      0, 0, 0, 0));
     gtk_window_present(GTK_WINDOW(spy_sabotage_shell));
   }
 }
@@ -1059,7 +1070,7 @@ void popup_incite_dialog(struct unit *actor, struct city *pcity, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(incite_response),
                    act_data(paction->id, actor->id,
                             pcity->id, 0, 0,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -1410,7 +1421,7 @@ void popup_action_selection(struct unit *actor_unit,
                (target_unit) ? target_unit->id : IDENTITY_NUMBER_ZERO,
                (target_tile) ? target_tile->index : TILE_INDEX_NONE,
                /* No target_building or target_tech supplied. (Dec 2019) */
-               B_LAST, A_UNSET,
+               B_LAST, A_UNSET, -1,
                target_extra ? target_extra->id : EXTRA_NONE);
 
   /* Could be caused by the server failing to reply to a request for more
diff --git a/client/gui-gtk-4.0/menu.c b/client/gui-gtk-4.0/menu.c
index 15f769f4af..32789d05b2 100644
--- a/client/gui-gtk-4.0/menu.c
+++ b/client/gui-gtk-4.0/menu.c
@@ -29,6 +29,7 @@
 #include "game.h"
 #include "government.h"
 #include "road.h"
+#include "specialist.h"
 #include "unit.h"
 
 /* client */
@@ -2265,6 +2266,14 @@ static void unit_goto_and_callback(GSimpleAction *action,
       sub_target = extra_number(pextra);
     }
     break;
+  case ASTK_SPECIALIST:
+    {
+      struct specialist *pspec = g_object_get_data(G_OBJECT(action),
+                                                   "end_specialist");
+      fc_assert_ret(nullptr != pspec);
+      sub_target = specialist_number(pspec);
+    }
+    break;
   case ASTK_NONE:
     sub_target = NO_TARGET;
     break;
@@ -3503,6 +3512,14 @@ void real_menus_update(void)
                             extra_name_translation(pextra));
           } extra_type_iterate_end;
           break;
+        case ASTK_SPECIALIST:
+          specialist_type_iterate(spc) {
+            struct specialist *pspec = specialist_by_number(spc);
+
+            CREATE_SUB_ITEM(pspec, "end_specialist",
+                            specialist_plural_translation(pspec));
+          } specialist_type_iterate_end;
+          break;
         case ASTK_NONE:
           /* Should not be here. */
           fc_assert(action_get_sub_target_kind(paction) != ASTK_NONE);
diff --git a/client/gui-gtk-5.0/action_dialog.c b/client/gui-gtk-5.0/action_dialog.c
index bfa0e50c6a..1f9354079b 100644
--- a/client/gui-gtk-5.0/action_dialog.c
+++ b/client/gui-gtk-5.0/action_dialog.c
@@ -27,6 +27,7 @@
 #include "traderoutes.h"
 #include "movement.h"
 #include "research.h"
+#include "specialist.h"
 #include "unit.h"
 #include "unitlist.h"
 
@@ -87,6 +88,7 @@ struct action_data {
   int target_building_id;
   int target_tech_id;
   int target_extra_id;
+  int target_specialist_id;
 };
 
 /* TODO: Maybe this should be in the dialog itself? */
@@ -167,6 +169,7 @@ static struct action_data *act_data(action_id act_id,
                                     int target_tile_id,
                                     int target_building_id,
                                     int target_tech_id,
+                                    int target_specialist_id,
                                     int tgt_extra_id)
 {
   struct action_data *data = fc_malloc(sizeof(*data));
@@ -178,6 +181,7 @@ static struct action_data *act_data(action_id act_id,
   data->target_tile_id = target_tile_id;
   data->target_building_id = target_building_id;
   data->target_tech_id = target_tech_id;
+  data->target_specialist_id = target_specialist_id;
   data->target_extra_id = tgt_extra_id;
 
   return data;
@@ -383,6 +387,13 @@ static void simple_action_callback(GtkWidget *w, gpointer data)
         failed = TRUE;
       }
       break;
+    case ASTK_SPECIALIST:
+      sub_target = args->target_specialist_id;
+      if (NULL == specialist_by_number(sub_target)) {
+        /* Did the ruleset change? */
+        failed = TRUE;
+      }
+      break;
     case ASTK_NONE:
     case ASTK_COUNT:
       /* Shouldn't happen. */
@@ -589,7 +600,7 @@ void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(bribe_unit_response),
                    act_data(paction->id, actor->id,
                             0, punit->id, 0,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -628,7 +639,7 @@ void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(bribe_stack_response),
                    act_data(paction->id, actor->id,
                             0, 0, ptile->index,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -1081,7 +1092,7 @@ void popup_sabotage_dialog(struct unit *actor, struct city *pcity,
     create_improvements_list(client.conn.playing, pcity,
                              act_data(paction->id,
                                       actor->id, pcity->id, 0, 0,
-                                      0, 0, 0));
+                                      0, 0, 0, 0));
     gtk_window_present(GTK_WINDOW(spy_sabotage_shell));
   }
 }
@@ -1149,7 +1160,7 @@ void popup_incite_dialog(struct unit *actor, struct city *pcity, int cost,
   g_signal_connect(shell, "response", G_CALLBACK(incite_response),
                    act_data(paction->id, actor->id,
                             pcity->id, 0, 0,
-                            0, 0, 0));
+                            0, 0, 0, 0));
 }
 
 /**********************************************************************//**
@@ -1500,7 +1511,7 @@ void popup_action_selection(struct unit *actor_unit,
                (target_unit) ? target_unit->id : IDENTITY_NUMBER_ZERO,
                (target_tile) ? target_tile->index : TILE_INDEX_NONE,
                /* No target_building or target_tech supplied. (Dec 2019) */
-               B_LAST, A_UNSET,
+               B_LAST, A_UNSET, -1,
                target_extra ? target_extra->id : EXTRA_NONE);
 
   /* Could be caused by the server failing to reply to a request for more
diff --git a/client/gui-gtk-5.0/menu.c b/client/gui-gtk-5.0/menu.c
index 5e30a7a88d..150d737920 100644
--- a/client/gui-gtk-5.0/menu.c
+++ b/client/gui-gtk-5.0/menu.c
@@ -29,6 +29,7 @@
 #include "game.h"
 #include "government.h"
 #include "road.h"
+#include "specialist.h"
 #include "unit.h"
 
 /* client */
@@ -2269,6 +2270,14 @@ static void unit_goto_and_callback(GSimpleAction *action,
       sub_target = extra_number(pextra);
     }
     break;
+  case ASTK_SPECIALIST:
+    {
+      struct specialist *pspec = g_object_get_data(G_OBJECT(action),
+                                                   "end_specialist");
+      fc_assert_ret(nullptr != pspec);
+      sub_target = specialist_number(pspec);
+    }
+    break;
   case ASTK_NONE:
     sub_target = NO_TARGET;
     break;
@@ -3507,6 +3516,14 @@ void real_menus_update(void)
                             extra_name_translation(pextra));
           } extra_type_iterate_end;
           break;
+        case ASTK_SPECIALIST:
+          specialist_type_iterate(spc) {
+            struct specialist *pspec = specialist_by_number(spc);
+
+            CREATE_SUB_ITEM(pspec, "end_specialist",
+                            specialist_plural_translation(pspec));
+          } specialist_type_iterate_end;
+          break;
         case ASTK_NONE:
           /* Should not be here. */
           fc_assert(action_get_sub_target_kind(paction) != ASTK_NONE);
diff --git a/client/gui-qt/dialogs.cpp b/client/gui-qt/dialogs.cpp
index 3e0a206290..bff5de0610 100644
--- a/client/gui-qt/dialogs.cpp
+++ b/client/gui-qt/dialogs.cpp
@@ -1419,6 +1419,7 @@ choice_dialog::choice_dialog(const QString title, const QString text,
   sub_target_id[ASTK_TECH] = A_UNSET;
   sub_target_id[ASTK_EXTRA] = EXTRA_NONE;
   sub_target_id[ASTK_EXTRA_NOT_THERE] = EXTRA_NONE;
+  sub_target_id[ASTK_SPECIALIST] = -1;
 
   targeted_unit = nullptr;
   // No buttons are added yet.
diff --git a/client/gui-qt/menu.cpp b/client/gui-qt/menu.cpp
index e62f45004c..af1aedc461 100644
--- a/client/gui-qt/menu.cpp
+++ b/client/gui-qt/menu.cpp
@@ -34,6 +34,7 @@
 #include "goto.h"
 #include "name_translation.h"
 #include "road.h"
+#include "specialist.h"
 #include "unit.h"
 
 // client
@@ -792,6 +793,12 @@ void go_act_menu::create()
                             extra_name_translation(pextra));
           } extra_type_iterate_end;
           break;
+        case ASTK_SPECIALIST:
+          specialist_type_iterate(spc) {
+            CREATE_SUB_ITEM(sub_target_menu, act_id, spc,
+                            specialist_plural_translation(specialist_by_number(spc)));
+          } specialist_type_iterate_end;
+          break;
         case ASTK_NONE:
           // Should not be here.
           fc_assert(action_get_sub_target_kind(paction) != ASTK_NONE);
diff --git a/client/gui-sdl2/action_dialog.c b/client/gui-sdl2/action_dialog.c
index 76f4709496..1230b8d986 100644
--- a/client/gui-sdl2/action_dialog.c
+++ b/client/gui-sdl2/action_dialog.c
@@ -25,6 +25,7 @@
 #include "game.h"
 #include "movement.h"
 #include "research.h"
+#include "specialist.h"
 #include "traderoutes.h"
 #include "unitlist.h"
 
@@ -808,6 +809,13 @@ static int simple_action_callback(struct widget *pwidget)
         failed = TRUE;
       }
       break;
+    case ASTK_SPECIALIST:
+      sub_target = diplomat_dlg->sub_target_id[ASTK_SPECIALIST];
+      if (nullptr == specialist_by_number(sub_target)) {
+        /* Did the ruleset change? */
+        failed = TRUE;
+      }
+      break;
     case ASTK_NONE:
     case ASTK_COUNT:
       /* Shouldn't happen. */
@@ -1030,6 +1038,7 @@ void popup_action_selection(struct unit *actor_unit,
   /* No target building or target tech supplied. (Feb 2020) */
   diplomat_dlg->sub_target_id[ASTK_BUILDING] = B_LAST;
   diplomat_dlg->sub_target_id[ASTK_TECH] = A_UNSET;
+  diplomat_dlg->sub_target_id[ASTK_SPECIALIST] = -1;
 
   if (target_extra) {
     diplomat_dlg->sub_target_id[ASTK_EXTRA] = extra_number(target_extra);
diff --git a/client/gui-sdl3/action_dialog.c b/client/gui-sdl3/action_dialog.c
index ab9b899357..10a3413e40 100644
--- a/client/gui-sdl3/action_dialog.c
+++ b/client/gui-sdl3/action_dialog.c
@@ -25,6 +25,7 @@
 #include "game.h"
 #include "movement.h"
 #include "research.h"
+#include "specialist.h"
 #include "traderoutes.h"
 #include "unitlist.h"
 
@@ -808,6 +809,13 @@ static int simple_action_callback(struct widget *pwidget)
         failed = TRUE;
       }
       break;
+    case ASTK_SPECIALIST:
+      sub_target = diplomat_dlg->sub_target_id[ASTK_SPECIALIST];
+      if (nullptr == specialist_by_number(sub_target)) {
+        /* Did the ruleset change? */
+        failed = TRUE;
+      }
+      break;
     case ASTK_NONE:
     case ASTK_COUNT:
       /* Shouldn't happen. */
@@ -1030,6 +1038,7 @@ void popup_action_selection(struct unit *actor_unit,
   /* No target building or target tech supplied. (Feb 2020) */
   diplomat_dlg->sub_target_id[ASTK_BUILDING] = B_LAST;
   diplomat_dlg->sub_target_id[ASTK_TECH] = A_UNSET;
+  diplomat_dlg->sub_target_id[ASTK_SPECIALIST] = -1;
 
   if (target_extra) {
     diplomat_dlg->sub_target_id[ASTK_EXTRA] = extra_number(target_extra);
diff --git a/common/actres.h b/common/actres.h
index 4d18a9e907..551479c143 100644
--- a/common/actres.h
+++ b/common/actres.h
@@ -66,6 +66,8 @@ struct req_context;
 #define SPECENUM_VALUE3NAME N_("extras on")
 #define SPECENUM_VALUE4 ASTK_EXTRA_NOT_THERE
 #define SPECENUM_VALUE4NAME N_("create extras on")
+#define SPECENUM_VALUE5 ASTK_SPECIALIST
+#define SPECENUM_VALUE5NAME N_("specialist")
 #define SPECENUM_COUNT ASTK_COUNT
 #include "specenum_gen.h"
 
diff --git a/common/unit.c b/common/unit.c
index 8b5084be0d..91733571ed 100644
--- a/common/unit.c
+++ b/common/unit.c
@@ -35,6 +35,7 @@
 #include "packets.h"
 #include "player.h"
 #include "road.h"
+#include "specialist.h"
 #include "tech.h"
 #include "traderoutes.h"
 #include "unitlist.h"
@@ -2861,6 +2862,13 @@ bool unit_order_list_is_sane(const struct civ_map *nmap,
           }
         }
         break;
+      case ASTK_SPECIALIST:
+        if (!specialist_by_number(orders[i].sub_target)) {
+          log_error("at index %d, cannot do %s without a target.", i,
+                    action_id_rule_name(orders[i].action));
+          return FALSE;
+        }
+        break;
       case ASTK_NONE:
         /* No validation required. */
         break;
diff --git a/server/actiontools.c b/server/actiontools.c
index dc31738a9a..a1faf55e48 100644
--- a/server/actiontools.c
+++ b/server/actiontools.c
@@ -934,6 +934,11 @@ int action_sub_target_id_for_action(const struct action *paction,
       }
     } extra_type_re_active_iterate_end;
     break;
+  case ASTK_SPECIALIST:
+    /* Implement if a specialist sub targeted action becomes flexible */
+    fc_assert_ret_val(paction->target_complexity == ACT_TGT_COMPL_FLEXIBLE,
+                      NO_TARGET);
+    break;
   case ASTK_COUNT:
     /* Should not exist. */
     fc_assert_ret_val(action_get_sub_target_kind(paction) != ASTK_COUNT,
diff --git a/server/savegame/savegame3.c b/server/savegame/savegame3.c
index 38e6d331e2..79b9bfc19e 100644
--- a/server/savegame/savegame3.c
+++ b/server/savegame/savegame3.c
@@ -6537,6 +6537,16 @@ static bool sg_load_player_unit(struct loaddata *loading,
             /* These take an extra. */
             action_wants_extra = TRUE;
             break;
+          case ASTK_SPECIALIST:
+            /* A valid specialist must be supplied. */
+            if (!loading->specialist.order[order_sub_tgt]) {
+              log_sg("Cannot find specialist %d for %s to become",
+                     order_sub_tgt, unit_rule_name(punit));
+              order->sub_target = NO_TARGET;
+            } else {
+              order->sub_target = specialist_index(loading->specialist.order[order_sub_tgt]);
+            }
+            break;
           case ASTK_NONE:
             /* None of these can take a sub target. */
             fc_assert_msg(order_sub_tgt == -1,
diff --git a/server/unithand.c b/server/unithand.c
index 46e72bc7ec..4b70023de9 100644
--- a/server/unithand.c
+++ b/server/unithand.c
@@ -3380,6 +3380,7 @@ bool unit_perform_action(struct player *pplayer,
   struct impr_type *sub_tgt_impr;
   struct unit *punit = NULL;
   struct city *pcity = NULL;
+  struct specialist *sub_tgt_spec = nullptr;
   const struct civ_map *nmap = &(wld.map);
 
   if (!action_id_exists(action_type)) {
@@ -3459,6 +3460,7 @@ bool unit_perform_action(struct player *pplayer,
   }
 
   sub_tgt_impr = improvement_by_number(sub_tgt_id);
+  sub_tgt_spec = specialist_by_number(sub_tgt_id);
 
   /* Sub targets should now be assigned */
   switch (paction->sub_target_kind) {
@@ -3481,6 +3483,12 @@ bool unit_perform_action(struct player *pplayer,
       return FALSE;
     }
     break;
+  case ASTK_SPECIALIST:
+    if (sub_tgt_spec == nullptr) {
+      /* Missing sub target */
+      return FALSE;
+    }
+    break;
   case ASTK_COUNT:
     break;
   }
-- 
2.45.2

