From 5cbc295d77048633887bf8a8993de29340da3f9e Mon Sep 17 00:00:00 2001
From: Ihnatus <ignatus31oct@mail.ru>
Date: Wed, 4 Jun 2025 21:28:46 +0300
Subject: [PATCH] Specialist-related Lua API

Methods to count specialists in cities, to check specialists
availability, and to get specialists output. Also, a single method
to edit specialists toggling them server-side.

See FCRM#1464

Signed-off-by: Ihnatus <ignatus31oct@mail.ru>
---
 common/scriptcore/api_game_effects.c | 64 ++++++++++++++++++++++++++++
 common/scriptcore/api_game_effects.h |  3 ++
 common/scriptcore/api_game_methods.c | 46 ++++++++++++++++++++
 common/scriptcore/api_game_methods.h |  5 +++
 common/scriptcore/tolua_game.pkg     |  9 ++++
 server/scripting/api_server_edit.c   | 44 +++++++++++++++++++
 server/scripting/api_server_edit.h   |  2 +
 server/scripting/tolua_server.pkg    |  7 +++
 8 files changed, 180 insertions(+)

diff --git a/common/scriptcore/api_game_effects.c b/common/scriptcore/api_game_effects.c
index b61999da8f..264505e814 100644
--- a/common/scriptcore/api_game_effects.c
+++ b/common/scriptcore/api_game_effects.c
@@ -162,6 +162,70 @@ int api_effects_tile_bonus(lua_State *L, Tile *ptile, City *pcity,
                                   etype);
 }
 
+/***********************************************************************//**
+  Get effect bonus for specialist.
+
+  @param  L            Lua State to use
+  @param  s            Specialist to get the effect value for
+  @param  d            Optional: City or Player for which the effect
+                       is calculated (if City, its owner becomes the player)
+  @param  output_id    Optional: Indetifier of the output related
+                       to the effect (e.g. Output to harvest from s)
+  @param  effect_type  Effect to check
+  @return              Effect bonus value
+***************************************************************************/
+int
+api_effects_specialist_bonus(lua_State *L, Specialist *s, lua_Object d,
+                             const char *output_id, const char *effect_type)
+{
+  struct req_context ctx = {.specialist = s};
+  enum effect_type etype;
+
+  LUASCRIPT_CHECK_STATE(L, 0);
+  LUASCRIPT_CHECK_ARG_NIL(L, s, 2, Specialist, 0);
+  LUASCRIPT_CHECK_ARG_NIL(L, effect_type, 5, string, 0);
+
+  etype = effect_type_by_name(effect_type, fc_strcasecmp);
+  if (!effect_type_is_valid(etype)) {
+    return 0;
+  }
+
+  if (output_id != NULL) {
+    enum output_type_id id = output_type_by_identifier(output_id);
+
+    if (O_LAST == id){
+      char msg[255];
+
+      fc_snprintf(msg, sizeof(msg),
+                  "Unknown output identifier \"%s\"", output_id);
+      LUASCRIPT_CHECK_ARG(L, id != O_LAST, 3, msg, 0); /* Fails */
+    }
+    ctx.output = get_output_type(id);
+  }
+
+  if (0 != d) {
+    tolua_Error e; /* ignored */
+
+    if (tolua_isusertype(L, d, "Player", 0, &e)) {
+      /* nil handled here */
+      ctx.player = (Player *) lua_touserdata(L, d);
+    } else if (tolua_isusertype(L, d, "City", 0, &e)) {
+      ctx.city = (City *) lua_touserdata(L, d);
+      if (ctx.city) {
+        ctx.player = city_owner(ctx.city);
+      }
+    } else {
+      char msg[255];
+
+      fc_snprintf(msg, sizeof(msg),
+                  "got '%s', City or Player expected", tolua_typename(L, d));
+      LUASCRIPT_CHECK_ARG(L, FALSE, 2, msg, 0); /* Fails */
+    }
+  }
+
+  return get_target_bonus_effects(nullptr, &ctx, nullptr, etype);
+}
+
 /***********************************************************************//**
   Returns the effect bonus at a tile and the specified unit.
   Unlike effects.unit_bonus() the city the effect is evaluated against is
diff --git a/common/scriptcore/api_game_effects.h b/common/scriptcore/api_game_effects.h
index 18d5a86972..acddeb2313 100644
--- a/common/scriptcore/api_game_effects.h
+++ b/common/scriptcore/api_game_effects.h
@@ -32,6 +32,9 @@ int api_effects_unit_bonus(lua_State *L, Unit *punit, Player *other_player,
                            const char *effect_type);
 int api_effects_tile_bonus(lua_State *L, Tile *ptile, City *pcity,
                            const char *output_id, const char *effect_type);
+int
+api_effects_specialist_bonus(lua_State *L, Specialist *s, lua_Object d,
+                             const char *output_id, const char *effect_type);
 int api_effects_unit_vs_tile_bonus(lua_State *L, Unit *punit, Tile *ptile,
                                    const char *effect_type);
 
diff --git a/common/scriptcore/api_game_methods.c b/common/scriptcore/api_game_methods.c
index 62e4ca8cfc..cf5357a2a8 100644
--- a/common/scriptcore/api_game_methods.c
+++ b/common/scriptcore/api_game_methods.c
@@ -416,6 +416,35 @@ int api_methods_city_nationality_citizens(lua_State *L, City *pcity,
   return citizens_nation_get(pcity, nationality->slot);
 }
 
+/**********************************************************************//**
+  Return number of specialists of type s working in pcity.
+  If no s is specified, return number of citizens employed as specialists.
+**************************************************************************/
+int api_methods_city_num_specialists(lua_State *L, City *pcity,
+                                     Specialist *s)
+{
+  LUASCRIPT_CHECK_STATE(L, 0);
+  LUASCRIPT_CHECK_SELF(L, pcity, 0);
+
+  if (nullptr != s) {
+    return pcity->specialists[specialist_index(s)];
+  } else {
+    return city_specialists(pcity);
+  }
+}
+
+/**********************************************************************//**
+  Return if pcity agrees with reqs of specialist s
+**************************************************************************/
+bool api_methods_city_can_employ(lua_State *L, City *pcity, Specialist *s)
+{
+  LUASCRIPT_CHECK_STATE(L, FALSE);
+  LUASCRIPT_CHECK_SELF(L, pcity, FALSE);
+  LUASCRIPT_CHECK_ARG_NIL(L, s, 3, Specialist, FALSE);
+
+  return city_can_use_specialist(pcity, specialist_index(s));
+}
+
 /**********************************************************************//**
    Return rule name for Government
 **************************************************************************/
@@ -789,6 +818,23 @@ bool api_methods_player_can_build_impr_direct(lua_State *L, Player *pplayer,
   return can_player_build_improvement_direct(pplayer, itype);
 }
 
+/**********************************************************************//**
+  Return if pplayer agrees with Player+-ranged reqs of specialist s
+**************************************************************************/
+bool api_methods_player_can_employ(lua_State *L, Player *pplayer,
+                                   Specialist *s)
+{
+  LUASCRIPT_CHECK_STATE(L, FALSE);
+  LUASCRIPT_CHECK_SELF(L, pplayer, FALSE);
+  LUASCRIPT_CHECK_ARG_NIL(L, s, 3, Specialist, FALSE);
+
+  return
+    are_reqs_active_ranges(REQ_RANGE_PLAYER, REQ_RANGE_WORLD,
+                              &(const struct req_context) {
+                                .player = pplayer,
+                              }, nullptr, &s->reqs, RPT_POSSIBLE);
+}
+
 /**********************************************************************//**
   Find player's primary capital, if known
 **************************************************************************/
diff --git a/common/scriptcore/api_game_methods.h b/common/scriptcore/api_game_methods.h
index a95cbeb13f..88e2255e76 100644
--- a/common/scriptcore/api_game_methods.h
+++ b/common/scriptcore/api_game_methods.h
@@ -67,6 +67,9 @@ bool api_methods_is_capital(lua_State *L, City *pcity);
 bool api_methods_is_primary_capital(lua_State *L, City *pcity);
 int api_methods_city_nationality_citizens(lua_State *L, City *pcity,
                                           Player *nationality);
+int api_methods_city_num_specialists(lua_State *L, City *pcity,
+                                     Specialist *s);
+bool api_methods_city_can_employ(lua_State *L, City *pcity, Specialist *s);
 
 /* Counter */
 const char *api_methods_counter_rule_name(lua_State *L, Counter *c);
@@ -125,6 +128,8 @@ bool api_methods_player_can_build_unit_direct(lua_State *L, Player *pplayer,
                                               Unit_Type *utype);
 bool api_methods_player_can_build_impr_direct(lua_State *L, Player *pplayer,
                                               Building_Type *itype);
+bool api_methods_player_can_employ(lua_State *L, Player *pplayer,
+                                   Specialist *s);
 
 City *api_methods_player_primary_capital(lua_State *L, Player *pplayer);
 
diff --git a/common/scriptcore/tolua_game.pkg b/common/scriptcore/tolua_game.pkg
index 723ff23ec4..092d488fd9 100644
--- a/common/scriptcore/tolua_game.pkg
+++ b/common/scriptcore/tolua_game.pkg
@@ -234,6 +234,8 @@ module Player {
     @ can_build_direct(lua_State *L, Player *pplayer, Building_Type *itype);
   bool api_methods_player_can_build_unit_direct
     @ can_build_direct(lua_State *L, Player *pplayer, Unit_Type *utype);
+  bool api_methods_player_can_employ
+    @ can_employ(lua_State *L, Player *pplayer, Specialist *s);
 
   City *api_methods_player_primary_capital
     @ primary_capital(lua_State *L, Player *pplayer);
@@ -305,6 +307,10 @@ module City {
 
   int api_methods_city_nationality_citizens
     @ nationality_citizens(lua_State *L, City *self, Player *nationality);
+  bool api_methods_city_can_employ
+    @ can_employ(lua_State *L, City *pcity, Specialist *s);
+  int api_methods_city_num_specialists
+    @ num_specialists (lua_State *L, City *pcity, Specialist *s);
 }
 
 $[
@@ -672,6 +678,9 @@ module effects {
   int api_effects_tile_bonus
     @ tile_bonus(lua_State *L, Tile *ptile, City *pcity,
                  const char *output_id, const char *effect_type);
+  int api_effects_specialist_bonus
+    @ specialist_bonus (lua_State *L, Specialist *s, lua_Object d,
+                        const char *output_id, const char *effect_type);
   int api_effects_unit_vs_tile_bonus
     @ unit_vs_tile_bonus(lua_State *L, Unit *punit, Tile *ptile,
                          const char *effect_type);
diff --git a/server/scripting/api_server_edit.c b/server/scripting/api_server_edit.c
index 5ab0b2124b..a8a8a084ea 100644
--- a/server/scripting/api_server_edit.c
+++ b/server/scripting/api_server_edit.c
@@ -777,6 +777,50 @@ 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.
+  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.
+**************************************************************************/
+bool api_edit_city_reduce_specialists(lua_State *L, City *pcity,
+                                      Specialist *s, int amount)
+{
+  Specialist_type_id from, to;
+
+  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);
+
+  from = specialist_index(s);
+  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((int) 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;
+  }
+}
+
 /**********************************************************************//**
   Create a new player.
 **************************************************************************/
diff --git a/server/scripting/api_server_edit.h b/server/scripting/api_server_edit.h
index 59c7ead2f0..edd2aca00f 100644
--- a/server/scripting/api_server_edit.h
+++ b/server/scripting/api_server_edit.h
@@ -78,6 +78,8 @@ void api_edit_remove_city(lua_State *L, City *pcity);
 bool api_edit_transfer_city(lua_State *L, City *pcity, Player *new_owner);
 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);
 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 d567e1ec57..ff1f936104 100644
--- a/server/scripting/tolua_server.pkg
+++ b/server/scripting/tolua_server.pkg
@@ -150,6 +150,9 @@ module edit {
     @ create_building (lua_State *L, City *pcity, Building_Type *impr);
   void api_edit_remove_building
     @ remove_building (lua_State *L, City *pcity, Building_Type *impr);
+  bool api_edit_city_reduce_specialists
+    @ reduce_specialists (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);
@@ -432,6 +435,10 @@ function City:remove_building(impr)
   edit.remove_building(self, impr)
 end
 
+function City:reduce_specialists(spec, amount)
+  return edit.reduce_specialists(self, spec, amount)
+end
+
 function City:change_size(change, nationality)
   edit.change_city_size(self, change, nationality)
 end
-- 
2.45.2

