From 09953bd51e85604206696edb67423cd2775ee59b Mon Sep 17 00:00:00 2001
From: Ihnatus <ignatus31oct@mail.ru>
Date: Tue, 27 Sep 2022 01:12:03 +0300
Subject: [PATCH] Split researched bulbs on free and bound

This abandons got_tech flags. Caravan or Lua bonuses now go to free
bulbs and are not lost or penalized on switch between saved and
new researched tech.

See OSDN#45685

Signed-off-by: Ihnatus <ignatus31oct@mail.ru>
---
 ai/default/aitech.c                        |  4 ++-
 common/research.h                          |  8 ++---
 server/cityturn.c                          |  2 +-
 server/plrhand.c                           |  2 +-
 server/savegame/savegame2.c                |  6 +++-
 server/savegame/savegame3.c                | 20 +++++++++----
 server/scripting/api_server_edit.c         |  2 +-
 server/scripting/api_server_game_methods.c | 23 +++++++-------
 server/scripting/api_server_game_methods.h |  2 +-
 server/scripting/tolua_server.pkg          |  4 +--
 server/srv_main.c                          |  7 ++---
 server/techtools.c                         | 35 ++++++++++++++--------
 server/techtools.h                         |  3 +-
 server/unithand.c                          |  2 +-
 14 files changed, 72 insertions(+), 48 deletions(-)

diff --git a/ai/default/aitech.c b/ai/default/aitech.c
index 305b464604..0eec7d64a4 100644
--- a/ai/default/aitech.c
+++ b/ai/default/aitech.c
@@ -330,7 +330,9 @@ void dai_manage_tech(struct ai_type *ait, struct player *pplayer)
   struct ai_tech_choice choice, goal;
   struct research *research = research_get(pplayer);
   /* Penalty for switching research */
-  int penalty = (research->got_tech ? 0 : research->bulbs_researched);
+  /* FIXME: get real penalty with game.server.techpenalty and multiresearch */
+  int penalty = research->bulbs_researched - research->free_bulbs;
+  penalty = MAX(penalty, 0);
 
   /* Even when we let human to do the final decision, we keep our
    * wants correctly calculated. Add effect values in */
diff --git a/common/research.h b/common/research.h
index 718f843031..f0faba3377 100644
--- a/common/research.h
+++ b/common/research.h
@@ -62,11 +62,9 @@ struct research {
   Tech_type_id researching_saved;
   int bulbs_researching_saved;
 
-  /* If the player completed a research this turn, this value is turned on
-   * and changing targets may be done without penalty. */
-  bool got_tech;
-  /* The same as got_tech but flipped back in choose_tech() */
-  bool got_tech_multi;
+  /* For this amount of bulbs, changing targets this turn
+   * may be done without penalty. */
+  int free_bulbs;
 
   struct research_invention {
     /* One of TECH_UNKNOWN, TECH_KNOWN or TECH_PREREQS_KNOWN. */
diff --git a/server/cityturn.c b/server/cityturn.c
index 94d5be36e5..3d75bf3cae 100644
--- a/server/cityturn.c
+++ b/server/cityturn.c
@@ -3347,7 +3347,7 @@ static void update_city_activity(struct city *pcity)
     pcity->did_sell = FALSE;
     pcity->did_buy = FALSE;
     pcity->airlift = city_airlift_max(pcity);
-    update_bulbs(pplayer, pcity->prod[O_SCIENCE], FALSE);
+    update_bulbs(pplayer, pcity->prod[O_SCIENCE], FALSE, FALSE);
 
     pplayer->economic.infra_points += get_city_bonus(pcity, EFT_INFRA_POINTS);
 
diff --git a/server/plrhand.c b/server/plrhand.c
index 514f3da58e..dff2154f0d 100644
--- a/server/plrhand.c
+++ b/server/plrhand.c
@@ -3458,5 +3458,5 @@ void update_national_activities(struct player *pplayer, int old_gold)
   research_get(pplayer)->researching_saved = A_UNKNOWN;
   /* Reduce the number of bulbs by the amount needed for tech upkeep and
    * check for finished research */
-  update_bulbs(pplayer, -player_tech_upkeep(pplayer), TRUE);
+  update_bulbs(pplayer, -player_tech_upkeep(pplayer), TRUE, FALSE);
 }
diff --git a/server/savegame/savegame2.c b/server/savegame/savegame2.c
index 769c538696..53f5202642 100644
--- a/server/savegame/savegame2.c
+++ b/server/savegame/savegame2.c
@@ -4921,6 +4921,7 @@ static void sg_load_researches(struct loaddata *loading)
   int number;
   const char *str;
   int i, j;
+  bool got_tech;
 
   /* Check status and return if not OK (sg_success FALSE). */
   sg_check_ret();
@@ -4964,9 +4965,12 @@ static void sg_load_researches(struct loaddata *loading)
     presearch->researching = technology_load(loading->file,
                                              "research.r%d.now", i);
     sg_failure_ret(secfile_lookup_bool(loading->file,
-                                       &presearch->got_tech,
+                                       &got_tech,
                                        "research.r%d.got_tech", i),
                    "%s", secfile_error());
+    if (got_tech) {
+      presearch->free_bulbs = presearch->bulbs_researched;
+    }
 
     str = secfile_lookup_str(loading->file, "research.r%d.done", i);
     sg_failure_ret(str != NULL, "%s", secfile_error());
diff --git a/server/savegame/savegame3.c b/server/savegame/savegame3.c
index f98d6f8647..4df6956c8b 100644
--- a/server/savegame/savegame3.c
+++ b/server/savegame/savegame3.c
@@ -7205,6 +7205,7 @@ static void sg_load_researches(struct loaddata *loading)
   const char *str;
   int i, j;
   int *vlist_research;
+  bool got_tech, got_tech_multi;
 
   vlist_research = NULL;
   /* Check status and return if not OK (sg_success FALSE). */
@@ -7248,13 +7249,22 @@ static void sg_load_researches(struct loaddata *loading)
                                                    "research.r%d.saved", i);
     presearch->researching = technology_load(loading->file,
                                              "research.r%d.now", i);
+    sg_failure_ret(secfile_lookup_int(loading->file,
+                                      &presearch->free_bulbs,
+                                      "research.r%d.free_bulbs", i),
+                       "%s", secfile_error());
+
+    /* 3.0...3.1 datafile format */
     sg_failure_ret(secfile_lookup_bool(loading->file,
-                                       &presearch->got_tech,
+                                       &got_tech,
                                        "research.r%d.got_tech", i),
                    "%s", secfile_error());
-    sg_failure_ret(secfile_lookup_bool(loading->file, &presearch->got_tech_multi,
+    sg_failure_ret(secfile_lookup_bool(loading->file, &got_tech_multi,
                                        "research.r%d.got_tech_multi", i),
                    "%s", secfile_error());
+    if (got_tech || got_tech_multi) {
+      presearch->free_bulbs = presearch->bulbs_researched;
+    }
 
     str = secfile_lookup_str(loading->file, "research.r%d.done", i);
     sg_failure_ret(str != NULL, "%s", secfile_error());
@@ -7346,10 +7356,8 @@ static void sg_save_researches(struct savedata *saving)
                          "research.r%d.bulbs", i);
       technology_save(saving->file, "research.r%d.now",
                       i, presearch->researching);
-      secfile_insert_bool(saving->file, presearch->got_tech,
-                          "research.r%d.got_tech", i);
-      secfile_insert_bool(saving->file, presearch->got_tech_multi,
-                          "research.r%d.got_tech_multi", i);
+      secfile_insert_int(saving->file, presearch->free_bulbs,
+                         "research.r%d.free_bulbs", i);
       /* Save technology lists as bytevector. Note that technology order is
        * saved in savefile.technology.order */
       advance_index_iterate(A_NONE, tech_id) {
diff --git a/server/scripting/api_server_edit.c b/server/scripting/api_server_edit.c
index 49c6a67eb6..623a72ce39 100644
--- a/server/scripting/api_server_edit.c
+++ b/server/scripting/api_server_edit.c
@@ -1094,7 +1094,7 @@ void api_edit_player_give_bulbs(lua_State *L, Player *pplayer, int amount)
   LUASCRIPT_CHECK_STATE(L);
   LUASCRIPT_CHECK_SELF(L, pplayer);
 
-  update_bulbs(pplayer, amount, TRUE);
+  update_bulbs(pplayer, amount, TRUE, TRUE);
 
   send_research_info(research_get(pplayer), NULL);
 }
diff --git a/server/scripting/api_server_game_methods.c b/server/scripting/api_server_game_methods.c
index cfc562de68..895b521687 100644
--- a/server/scripting/api_server_game_methods.c
+++ b/server/scripting/api_server_game_methods.c
@@ -191,9 +191,17 @@ int api_methods_player_tech_bulbs(lua_State *L, Player *pplayer,
     if (presearch->researching_saved == tn) {
       return
         presearch->bulbs_researching_saved - presearch->bulbs_researched;
-    } else if (!presearch->got_tech && tn != presearch->researching
+    } else if (tn != presearch->researching
                && presearch->bulbs_researched > 0) {
-      return -presearch->bulbs_researched * game.server.techpenalty / 100;
+      int bound_bulbs = presearch->bulbs_researched - presearch->free_bulbs;
+      int penalty;
+
+      if (bound_bulbs <= 0) {
+        return 0;
+      }
+      penalty = bound_bulbs * game.server.techpenalty / 100;
+
+      return -MIN(penalty, presearch->bulbs_researched);
     } else {
       return 0;
     }
@@ -201,10 +209,9 @@ int api_methods_player_tech_bulbs(lua_State *L, Player *pplayer,
 }
 
 /**********************************************************************//**
-  Returns whether pplayer is in "got tech" state and can invest their
-  remaining rsearched bulbs freely.
+  Returns bulbs that can be freely transferred to a new research target.
 **************************************************************************/
-bool api_methods_player_got_tech(lua_State *L, Player *pplayer)
+int api_methods_player_free_bulbs(lua_State *L, Player *pplayer)
 {
   const struct research *presearch;
 
@@ -213,9 +220,5 @@ bool api_methods_player_got_tech(lua_State *L, Player *pplayer)
   presearch = research_get(pplayer);
   LUASCRIPT_CHECK(L, presearch, "player's research not set", 0);
 
-  if (game.server.multiresearch) {
-    return presearch->got_tech_multi;
-  } else {
-    return presearch->got_tech;
-  }
+  return presearch->free_bulbs;
 }
diff --git a/server/scripting/api_server_game_methods.h b/server/scripting/api_server_game_methods.h
index c74a0bd61b..4a6e3d5148 100644
--- a/server/scripting/api_server_game_methods.h
+++ b/server/scripting/api_server_game_methods.h
@@ -39,6 +39,6 @@ int api_methods_nation_trait_default(lua_State *L, Nation_Type *pnation,
                                      const char *tname);
 int api_methods_player_tech_bulbs(lua_State *L, Player *pplayer,
                                   Tech_Type *tech);
-bool api_methods_player_got_tech(lua_State *L, Player *pplayer);
+int api_methods_player_free_bulbs(lua_State *L, Player *pplayer);
 
 #endif /* FC__API_SERVER_GAME_METHODS_H */
diff --git a/server/scripting/tolua_server.pkg b/server/scripting/tolua_server.pkg
index ac62a1a426..204c82aeff 100644
--- a/server/scripting/tolua_server.pkg
+++ b/server/scripting/tolua_server.pkg
@@ -498,8 +498,8 @@ $]
 /* Additions to common Player module. */
 module Player {
   module properties {
-    bool api_methods_player_got_tech
-      @ got_tech (lua_State *L, Player *pplayer);
+    int api_methods_player_free_bulbs
+      @ free_bulbs (lua_State *L, Player *pplayer);
   }
   int api_methods_player_trait
     @ trait (lua_State *L, Player *pplayer, const char *tname);
diff --git a/server/srv_main.c b/server/srv_main.c
index 3c18dca1e2..a741a838db 100644
--- a/server/srv_main.c
+++ b/server/srv_main.c
@@ -1418,7 +1418,7 @@ static void end_phase(void)
       }
       /* Add the researched bulbs to the pool; do *NOT* check for finished
        * research */
-      update_bulbs(pplayer, 0, FALSE);
+      update_bulbs(pplayer, 0, FALSE, FALSE);
     }
   } phase_players_iterate_end;
 
@@ -1440,8 +1440,7 @@ static void end_phase(void)
 
   /* Refresh cities */
   phase_players_iterate(pplayer) {
-    research_get(pplayer)->got_tech = FALSE;
-    research_get(pplayer)->got_tech_multi = FALSE;
+    research_get(pplayer)->free_bulbs = 0;
   } phase_players_iterate_end;
 
   alive_phase_players_iterate(pplayer) {
@@ -3384,7 +3383,7 @@ static void srv_ready(void)
 
     players_iterate(pplayer) {
       /* Check for finished research. */
-      update_bulbs(pplayer, 0, TRUE);
+      update_bulbs(pplayer, 0, TRUE, FALSE);
     } players_iterate_end;
   }
 
diff --git a/server/techtools.c b/server/techtools.c
index 3a145b2b42..cf0e5b059d 100644
--- a/server/techtools.c
+++ b/server/techtools.c
@@ -378,11 +378,10 @@ void found_new_tech(struct research *presearch, Tech_type_id tech_found,
 
   could_switch = create_current_governments_data(presearch);
 
-  /* got_tech allows us to change research without applying techpenalty
+  /* getting tech allows us to change research without applying techpenalty
    * (without losing bulbs) */
   if (tech_found == presearch->researching) {
-    presearch->got_tech = TRUE;
-    presearch->got_tech_multi = TRUE;
+    presearch->free_bulbs += presearch->bulbs_researched;
   }
   presearch->researching_saved = A_UNKNOWN;
   presearch->techs_researched++;
@@ -617,7 +616,8 @@ static bool lose_tech(struct research *research)
   This is called from each city every turn, from caravan revenue, at the
   end of the phase, and from lua API.
 ****************************************************************************/
-void update_bulbs(struct player *pplayer, int bulbs, bool check_tech)
+void update_bulbs(struct player *pplayer, int bulbs, bool check_tech,
+                  bool free_bulbs)
 {
   struct research *research = research_get(pplayer);
 
@@ -629,6 +629,13 @@ void update_bulbs(struct player *pplayer, int bulbs, bool check_tech)
   /* count our research contribution this turn */
   pplayer->server.bulbs_last_turn += bulbs;
   research->bulbs_researched += bulbs;
+  if (A_UNKNOWN != research->researching_saved) {
+    fc_assert(research->researching_saved != research->researching);
+    research->bulbs_researching_saved += bulbs;
+  }
+  if (free_bulbs) {
+    research->free_bulbs += bulbs;
+  }
   advance_index_iterate(A_FIRST, j) {
     if (j == research->researching) {
       research->inventions[j].bulbs_researched_saved = research->bulbs_researched;
@@ -640,7 +647,7 @@ void update_bulbs(struct player *pplayer, int bulbs, bool check_tech)
     /* If we have a negative number of bulbs we do try to:
      * - reduce the number of future techs;
      * - or lose one random tech.
-     * After that the number of bulbs available is incresed based on the
+     * After that the number of bulbs available is increased based on the
      * value of the lost tech. */
     if (lose_tech(research)) {
       Tech_type_id tech = (research->future_tech > 0
@@ -985,11 +992,8 @@ void choose_tech(struct research *research, Tech_type_id tech)
       }
     } advance_index_iterate_end;
     research->researching = tech;
-    if (!research->got_tech_multi) {
-      research->bulbs_researched = 0;
-    }
-    research->bulbs_researched = research->bulbs_researched + bulbs_res;
-    research->got_tech_multi = FALSE;
+    research->bulbs_researched = research->free_bulbs + bulbs_res;
+    research->free_bulbs = 0;
     if (research->bulbs_researched
         >= research_total_bulbs_required(research, tech, FALSE)) {
       tech_researched(research);
@@ -997,14 +1001,19 @@ void choose_tech(struct research *research, Tech_type_id tech)
     return;
   }
 
-  if (!research->got_tech && research->researching_saved == A_UNKNOWN) {
+  /* The first check implies we have NOT recently got a technology
+   * and are changing from one we were researching at tc. */
+  if (research->bulbs_researched - research->free_bulbs > 0
+      && research->researching_saved == A_UNKNOWN) {
     research->bulbs_researching_saved = research->bulbs_researched;
     research->researching_saved = research->researching;
     /* Subtract a penalty because we changed subject. */
     if (research->bulbs_researched > 0) {
       research->bulbs_researched
-        -= ((research->bulbs_researched * game.server.techpenalty) / 100);
-      fc_assert(research->bulbs_researched >= 0);
+        -= ((research->bulbs_researched - research->free_bulbs)
+            * game.server.techpenalty) / 100;
+      /* If free_bulbs were for some reason negative... */
+      research->bulbs_researched = MAX(research->bulbs_researched, 0);
     }
   } else if (tech == research->researching_saved) {
     research->bulbs_researched = research->bulbs_researching_saved;
diff --git a/server/techtools.h b/server/techtools.h
index 9fd4130cc5..2a6b385bd9 100644
--- a/server/techtools.h
+++ b/server/techtools.h
@@ -38,7 +38,8 @@ void script_tech_learned(struct research *presearch,
                          const char *reason);
 void found_new_tech(struct research *presearch, Tech_type_id tech_found,
                     bool was_discovery, bool saving_bulbs);
-void update_bulbs(struct player *pplayer, int bulbs, bool check_tech);
+void update_bulbs(struct player *pplayer, int bulbs, bool check_tech,
+                  bool free_bulbs);
 void init_tech(struct research *presearch, bool update);
 void choose_tech(struct research *presearch, Tech_type_id tech);
 void choose_random_tech(struct research *presearch);
diff --git a/server/unithand.c b/server/unithand.c
index 981fffb773..5a1f6cbb36 100644
--- a/server/unithand.c
+++ b/server/unithand.c
@@ -5970,7 +5970,7 @@ static bool do_unit_establish_trade(struct player *pplayer,
 
   if (bonus_type == TBONUS_SCIENCE || bonus_type == TBONUS_BOTH) {
     /* add bulbs and check for finished research */
-    update_bulbs(pplayer, revenue, TRUE);
+    update_bulbs(pplayer, revenue, TRUE, TRUE);
 
     /* Inform everyone about tech changes */
     send_research_info(research_get(pplayer), NULL);
-- 
2.34.1