From 9033f1f4362e37846b82320332b4958a4654b117 Mon Sep 17 00:00:00 2001
From: Sweet Tea Dorminy <sweettea-kernel@dorminy.me>
Date: Sun, 1 Nov 2026 22:09:13 -0800
Subject: [PATCH 1/2] common: Add world and per-player knowledge generation
 counters

Pathfinding results can only be cached if it is known when their inputs
change. map_world_generation() changes whenever terrain, extras, tile or
extras owner, city centers or owners, diplomatic states, settings, the
set of players, or units that transport or are transported change, and
at turn and phase boundaries. map_knowledge_generation() changes when
a player's known tiles change. Nothing reads them yet.

See RM #2439

Signed-off-by: Sweet Tea Dorminy <sweettea-kernel@dorminy.me>
---
 common/game.c                      |  1 +
 common/map.c                       | 99 ++++++++++++++++++++++++++++++
 common/map.h                       |  7 +++
 common/tile.c                      | 15 +++++
 common/unit.c                      |  5 ++
 server/citytools.c                 |  1 +
 server/cityturn.c                  |  1 +
 server/diplhand.c                  |  1 +
 server/edithand.c                  |  4 ++
 server/maphand.c                   | 12 ++++
 server/plrhand.c                   |  8 +++
 server/scripting/api_server_edit.c |  1 +
 server/settings.c                  |  3 +
 server/srv_main.c                  |  6 ++
 server/unithand.c                  |  2 +
 server/unittools.c                 |  4 ++
 16 files changed, 170 insertions(+)

diff --git a/common/game.c b/common/game.c
index cb83133fef..3da35a1746 100644
--- a/common/game.c
+++ b/common/game.c
@@ -162,6 +162,7 @@ void game_remove_unit(struct world *gworld, struct unit *punit)
               punit->homecity);
   }
 
+  map_world_unit_changed(punit);
   unit_list_remove(unit_tile(punit)->units, punit);
 
   owner = unit_owner(punit);
diff --git a/common/map.c b/common/map.c
index f969a3854b..583db5794a 100644
--- a/common/map.c
+++ b/common/map.c
@@ -139,6 +139,105 @@ bv_extras get_tile_infrastructure_set(const struct tile *ptile,
   return pspresent;
 }
 
+/* The world generation. It changes whenever something a pathfinder reads
+ * may have changed: terrain, extras and their owner, tile owner, city
+ * centers and their owner, diplomatic states, settings, the set of
+ * players, the units that can transport or are transported, and turn
+ * and phase boundaries. Tile knowledge has its own per-player counter
+ * (map_knowledge_changed()). Caches of pathfinding results compare it
+ * on every lookup; a change that turns out to be irrelevant only costs
+ * a recomputation. */
+static unsigned int world_generation = 0;
+
+/* Per player slot: changes whenever the set of tiles the player knows
+ * changes. Pathfinding reads tile knowledge only when the parameter is
+ * not omniscient, so caches of such results compare this as well as
+ * the world generation. */
+static unsigned int knowledge_generation[MAX_NUM_PLAYER_SLOTS];
+
+/*******************************************************************//**
+  Return whether ptile is a tile of the current map, and not a virtual
+  tile (tile_virtual_new()), which no pathfinder reads.
+***********************************************************************/
+static bool map_world_tile_is_real(const struct tile *ptile)
+{
+  return (wld.map.tiles != nullptr
+          && ptile >= wld.map.tiles
+          && ptile < wld.map.tiles + MAP_INDEX_SIZE);
+}
+
+/*******************************************************************//**
+  Something pathfinding may read has changed, or may have. Issues a new
+  world generation.
+***********************************************************************/
+void map_world_changed(void)
+{
+  world_generation++;
+}
+
+/*******************************************************************//**
+  As map_world_changed(), but only when ptile is a real map tile: the
+  change of a virtual tile changes nothing any pathfinder reads.
+
+  @param ptile Tile that changed
+***********************************************************************/
+void map_world_tile_changed(const struct tile *ptile)
+{
+  if (map_world_tile_is_real(ptile)) {
+    map_world_changed();
+  }
+}
+
+/*******************************************************************//**
+  The tile, orders or presence of punit is about to change.
+
+  Pathfinding (pf_get_move_scope(), pf_transport_check()) reads the
+  units on a tile only as possible transports: their owner, type,
+  orders, transport depth and the transports carrying them. Pathfinders
+  that read other units (get_zoc, get_TB or is_action_possible
+  callbacks) do not cache their results. So only a unit that can
+  transport, or that is transported, changes the world generation.
+
+  @param punit Unit about to change
+***********************************************************************/
+void map_world_unit_changed(const struct unit *punit)
+{
+  if (unit_type_get(punit)->transport_capacity > 0
+      || unit_transport_get(punit) != nullptr) {
+    map_world_changed();
+  }
+}
+
+/*******************************************************************//**
+  Return the current world generation; see map_world_changed().
+***********************************************************************/
+unsigned int map_world_generation(void)
+{
+  return world_generation;
+}
+
+/*******************************************************************//**
+  The set of tiles pplayer knows has changed, or may have.
+
+  @param pplayer Player whose knowledge changed
+***********************************************************************/
+void map_knowledge_changed(const struct player *pplayer)
+{
+  knowledge_generation[player_index(pplayer)]++;
+}
+
+/*******************************************************************//**
+  Return the current knowledge generation of pplayer; see
+  map_knowledge_changed().
+
+  @param pplayer Player whose knowledge generation to return
+  @return        Knowledge generation
+***********************************************************************/
+unsigned int map_knowledge_generation(const struct player *pplayer)
+{
+  return knowledge_generation[player_index(pplayer)];
+}
+
 /*******************************************************************//**
   Returns TRUE if we are at a stage of the game where the map
   has not yet been generated/loaded.
diff --git a/common/map.h b/common/map.h
index b124ea0ae5..6de186647e 100644
--- a/common/map.h
+++ b/common/map.h
@@ -805,6 +805,13 @@ static inline bool is_border_tile(const struct tile *ptile, int dist)
 enum direction8 rand_direction(void);
 enum direction8 opposite_direction(enum direction8 dir);
 
+void map_world_changed(void);
+void map_world_tile_changed(const struct tile *ptile);
+void map_world_unit_changed(const struct unit *punit);
+unsigned int map_world_generation(void);
+void map_knowledge_changed(const struct player *pplayer);
+unsigned int map_knowledge_generation(const struct player *pplayer);
+
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */
diff --git a/common/tile.c b/common/tile.c
index b784fb7059..7d98332b4e 100644
--- a/common/tile.c
+++ b/common/tile.c
@@ -72,6 +72,9 @@ void tile_set_owner(struct tile *ptile, struct player *pplayer,
   if (BORDERS_DISABLED != game.info.borders
       /* City tiles are always owned by the city owner. */
       || (tile_city(ptile) != nullptr || ptile->owner != nullptr)) {
+    if (ptile->owner != pplayer) {
+      map_world_tile_changed(ptile);
+    }
     ptile->owner = pplayer;
     ptile->claimer = claimer;
   }
@@ -106,6 +109,15 @@ struct city *tile_worked(const struct tile *ptile)
 ****************************************************************************/
 void tile_set_worked(struct tile *ptile, struct city *pcity)
 {
+  struct city *new_center = (pcity != nullptr
+                             && is_city_center(pcity, ptile)
+                             ? pcity : nullptr);
+
+  if (tile_city(ptile) != new_center) {
+    /* A city center appears or goes. Which city works a tile is not
+     * read by pathfinding. */
+    map_world_tile_changed(ptile);
+  }
   ptile->worked = pcity;
 }
 
@@ -150,6 +162,7 @@ void tile_set_terrain(struct tile *ptile, struct terrain *pterrain)
                 tile_city(ptile)->id);
 #endif /* 0 */
 
+  map_world_tile_changed(ptile);
   ptile->terrain = pterrain;
   if (ptile->resource != nullptr) {
     if (pterrain != nullptr
@@ -956,6 +969,7 @@ bool tile_has_cause_extra(const struct tile *ptile, enum extra_cause cause)
 void tile_add_extra(struct tile *ptile, const struct extra_type *pextra)
 {
   if (pextra != nullptr) {
+    map_world_tile_changed(ptile);
     BV_SET(ptile->extras, extra_index(pextra));
   }
 }
@@ -966,6 +980,7 @@ void tile_add_extra(struct tile *ptile, const struct extra_type *pextra)
 void tile_remove_extra(struct tile *ptile, const struct extra_type *pextra)
 {
   if (pextra != nullptr) {
+    map_world_tile_changed(ptile);
     BV_CLR(ptile->extras, extra_index(pextra));
     if (ptile->resource == pextra) {
       ptile->resource = nullptr;
diff --git a/common/unit.c b/common/unit.c
index 1b484d1613..81374514ce 100644
--- a/common/unit.c
+++ b/common/unit.c
@@ -1827,6 +1827,9 @@ void unit_virtual_destroy(struct unit *punit)
 void free_unit_orders(struct unit *punit)
 {
   if (punit->has_orders) {
+    /* unit_has_orders() decides whether a pathfinder may disembark
+     * from this unit as a transport. */
+    map_world_unit_changed(punit);
     punit->goto_tile = nullptr;
     free(punit->orders.list);
     punit->orders.list = nullptr;
@@ -2464,6 +2467,7 @@ bool unit_transport_load(struct unit *pcargo, struct unit *ptrans, bool force)
   fc_assert_ret_val(!unit_list_search(ptrans->transporting, pcargo), FALSE);
 
   if (force || can_unit_load(pcargo, ptrans)) {
+    map_world_changed();
     pcargo->transporter = ptrans;
     unit_list_append(ptrans->transporting, pcargo);
 
@@ -2501,6 +2505,7 @@ bool unit_transport_unload(struct unit *pcargo)
   }
 
   /* For the server (also safe for the client). */
+  map_world_changed();
   pcargo->transporter = nullptr;
 
   return TRUE;
diff --git a/server/citytools.c b/server/citytools.c
index 9f3958057b..dc7286b835 100644
--- a/server/citytools.c
+++ b/server/citytools.c
@@ -1202,6 +1202,7 @@ bool transfer_city(struct player *ptaker, struct city *pcity,
   map_clear_border(pcenter);
   /* city_thaw_workers_queue() later */
 
+  map_world_changed();
   pcity->owner = ptaker;
   pcity->capital = CAPITAL_NOT;
   pcity->acquire_t = CACQ_CONQUEST;
diff --git a/server/cityturn.c b/server/cityturn.c
index 2762b4b4f8..854f6e90ba 100644
--- a/server/cityturn.c
+++ b/server/cityturn.c
@@ -2979,6 +2979,7 @@ static struct unit *city_create_unit(struct city *pcity,
 
   /* If city has a rally point set, give the unit a move order. */
   if (pcity->rally_point.length) {
+    map_world_unit_changed(punit);
     punit->has_orders = TRUE;
     punit->orders.length = pcity->rally_point.length;
     punit->orders.vigilant = pcity->rally_point.vigilant;
diff --git a/server/diplhand.c b/server/diplhand.c
index 4f6c30352a..945a65527f 100644
--- a/server/diplhand.c
+++ b/server/diplhand.c
@@ -129,6 +129,7 @@ void set_diplstate_type(struct player_diplstate *state1,
 
   max = dst_closest(max, state1->max_state);
 
+  map_world_changed();
   state1->type = type;
   state2->type = type;
   state1->max_state = max;
diff --git a/server/edithand.c b/server/edithand.c
index af75014a3c..d72ea934e8 100644
--- a/server/edithand.c
+++ b/server/edithand.c
@@ -368,6 +368,8 @@ void handle_edit_tile_extra(struct connection *pc, int tile,
 
   conn_list_do_buffer(game.est_connections);
   square_iterate(&(wld.map), ptile_center, size - 1, ptile) {
+    /* Pathfinding reads the extras owner. */
+    map_world_changed();
     ptile->extras_owner = plr_eowner;
     edit_tile_extra_handling(ptile, extra_by_number(id), removal, TRUE);
   } square_iterate_end;
@@ -410,6 +412,8 @@ void handle_edit_tile(struct connection *pc,
   }
 
   if (ptile->extras_owner != eowner) {
+    /* Pathfinding reads the extras owner. */
+    map_world_changed();
     ptile->extras_owner = eowner;
     changed = TRUE;
   }
diff --git a/server/maphand.c b/server/maphand.c
index 9f9ec53092..cc2851d402 100644
--- a/server/maphand.c
+++ b/server/maphand.c
@@ -1194,6 +1194,10 @@ void change_playertile_site(struct player_tile *ptile,
 **************************************************************************/
 void map_set_known(struct tile *ptile, struct player *pplayer)
 {
+  if (!dbv_isset(&pplayer->tile_known, tile_index(ptile))) {
+    /* Pathfinding reads tile knowledge. */
+    map_knowledge_changed(pplayer);
+  }
   dbv_set(&pplayer->tile_known, tile_index(ptile));
 }
 
@@ -1202,6 +1206,10 @@ void map_set_known(struct tile *ptile, struct player *pplayer)
 **************************************************************************/
 void map_clear_known(struct tile *ptile, struct player *pplayer)
 {
+  if (dbv_isset(&pplayer->tile_known, tile_index(ptile))) {
+    /* Pathfinding reads tile knowledge. */
+    map_knowledge_changed(pplayer);
+  }
   dbv_clr(&pplayer->tile_known, tile_index(ptile));
 }
 
@@ -1246,6 +1254,7 @@ void player_map_init(struct player *pplayer)
   } whole_map_iterate_end;
 
   dbv_init(&pplayer->tile_known, MAP_INDEX_SIZE);
+  map_knowledge_changed(pplayer);
 }
 
 /**********************************************************************//**
@@ -1326,6 +1335,7 @@ void remove_player_from_maps(struct player *pplayer)
       reality_changed = TRUE;
     }
     if (extra_owner(ptile) == pplayer) {
+      map_world_changed();
       ptile->extras_owner = nullptr;
       reality_changed = TRUE;
     }
@@ -2201,6 +2211,7 @@ void tile_claim_bases(struct tile *ptile, struct player *powner)
   /* This MUST be before potentially recursive call to map_claim_base(),
    * so that the recursive call will get new owner == base_loser and
    * abort recursion. */
+  map_world_changed();
   ptile->extras_owner = powner;
 
   extra_type_by_cause_iterate(EC_BASE, pextra) {
@@ -2569,6 +2580,7 @@ void create_extra(struct tile *ptile, struct extra_type *pextra,
           }
         } extra_type_by_cause_iterate_end;
 
+        map_world_changed();
         ptile->extras_owner = pplayer;
       }
     } else {
diff --git a/server/plrhand.c b/server/plrhand.c
index 759281b4f3..44d862d059 100644
--- a/server/plrhand.c
+++ b/server/plrhand.c
@@ -251,6 +251,7 @@ void kill_player(struct player *pplayer)
       map_claim_ownership(ptile, nullptr, nullptr, FALSE);
     }
     if (extra_owner(ptile) == pplayer) {
+      map_world_changed();
       ptile->extras_owner = nullptr;
     }
   } whole_map_iterate_end;
@@ -838,6 +839,7 @@ static void maybe_claim_base(struct tile *ptile, struct player *new_owner,
       map_claim_base(ptile, pextra, new_owner, old_owner);
     } extra_type_by_cause_iterate_end;
 
+    map_world_changed();
     ptile->extras_owner = new_owner;
   }
 }
@@ -969,6 +971,8 @@ void handle_diplomacy_cancel_pact(struct player *pplayer,
   }
 
   /* Do the change */
+  /* Pathfinding reads diplomatic states. */
+  map_world_changed();
   ds_plrplr2->type = ds_plr2plr->type = new_type;
   ds_plrplr2->turns_left = ds_plr2plr->turns_left = 16;
 
@@ -1907,6 +1911,8 @@ struct player *server_create_player(int player_id, const char *ai_tname,
   if (nullptr == pplayer) {
     return nullptr;
   }
+  /* Pathfinding results are keyed by player. */
+  map_world_changed();
 
   if (allow_ai_type_fallbacking) {
     pplayer->savegame_ai_type_name = fc_strdup(ai_tname);
@@ -1950,6 +1956,8 @@ void server_remove_player(struct player *pplayer)
 
   /* Save player slot */
   pslot = pplayer->slot;
+  /* Pathfinding results are keyed by player. */
+  map_world_changed();
 
   log_normal(_("Removing player %s."), player_name(pplayer));
 
diff --git a/server/scripting/api_server_edit.c b/server/scripting/api_server_edit.c
index 48f90710cd..e3cf9c11ea 100644
--- a/server/scripting/api_server_edit.c
+++ b/server/scripting/api_server_edit.c
@@ -1175,6 +1175,7 @@ bool api_edit_tile_hide(lua_State *L, Tile *ptile, Player *pplayer)
     } trade_partners_iterate_end;
   }
 
+  map_knowledge_changed(pplayer);
   dbv_clr(&pplayer->tile_known, tile_index(ptile));
 
   send_tile_info(pplayer->connections, ptile, TRUE);
diff --git a/server/settings.c b/server/settings.c
index eab7c1398f..4443565860 100644
--- a/server/settings.c
+++ b/server/settings.c
@@ -5657,6 +5657,9 @@ static void settings_list_free(void)
 ****************************************************************************/
 void setting_changed(struct setting *pset)
 {
+  /* Settings (borders, restrictinfra, citymindist, ...) are read by
+   * pathfinding. */
+  map_world_changed();
   pset->setdef = SETDEF_CHANGED;
 }
 
diff --git a/server/srv_main.c b/server/srv_main.c
index 15cf72eb87..fd2cd879b2 100644
--- a/server/srv_main.c
+++ b/server/srv_main.c
@@ -973,6 +973,7 @@ static void update_diplomatics(void)
             && state->auto_cancel_turn != game.info.turn) {
           state->turns_left--;
           if (state->turns_left <= 0) {
+            map_world_changed();
             state->type = DS_PEACE;
             state2->type = DS_PEACE;
             state->turns_left = 0;
@@ -1006,6 +1007,7 @@ static void update_diplomatics(void)
                             "You are now at war with the %s."),
                           player_name(plr1),
                           nation_plural_for_player(plr1));
+            map_world_changed();
             state->type = DS_WAR;
             state2->type = DS_WAR;
             state->turns_left = 0;
@@ -1157,6 +1159,7 @@ static void ai_start_phase(void)
 static void begin_turn(bool is_new_turn)
 {
   log_debug("Begin turn");
+  map_world_changed();
 
   event_cache_remove_old();
 
@@ -1264,6 +1267,7 @@ static void begin_turn(bool is_new_turn)
 static void begin_phase(bool is_new_phase)
 {
   log_debug("Begin phase");
+  map_world_changed();
 
   conn_list_do_buffer(game.est_connections);
 
@@ -1443,6 +1447,7 @@ static void begin_phase(bool is_new_phase)
 static void end_phase(void)
 {
   log_debug("Endphase");
+  map_world_changed();
 
   /*
    * This empties the client Messages window; put this before
@@ -1609,6 +1614,7 @@ static void end_phase(void)
 static void end_turn(void)
 {
   log_debug("Endturn");
+  map_world_changed();
 
   /* Hack: Because observer players never get an end-phase packet we send
    * one here. */
diff --git a/server/unithand.c b/server/unithand.c
index f87ee0539b..d0382430c4 100644
--- a/server/unithand.c
+++ b/server/unithand.c
@@ -4140,6 +4140,7 @@ void unit_change_homecity_handling(struct unit *punit, struct city *new_pcity,
 
     unit_list_remove(old_owner->units, punit);
     unit_list_prepend(new_owner->units, punit);
+    map_world_changed();
     punit->owner = new_owner;
 
     /* Activate AI control of the new owner. */
@@ -6968,6 +6969,7 @@ void handle_unit_orders(struct player *pplayer,
     return;
   }
 
+  map_world_unit_changed(punit);
   punit->has_orders = TRUE;
   punit->orders.length = length;
   punit->orders.index = 0;
diff --git a/server/unittools.c b/server/unittools.c
index 74d73bbed4..17f5290d6a 100644
--- a/server/unittools.c
+++ b/server/unittools.c
@@ -1572,6 +1572,8 @@ void transform_unit(struct unit *punit, const struct unit_type *to_unit,
   int old_hp = unit_type_get(punit)->hp;
   int lvls;
 
+  /* A transport's type is read by pathfinding. */
+  map_world_changed();
   punit->utype = to_unit;
 
   /* New type may not have the same veteran system, and we may want to
@@ -1747,6 +1749,7 @@ bool place_unit(struct unit *punit, struct player *pplayer,
                     FALSE);
 
   unit_list_prepend(pplayer->units, punit);
+  map_world_unit_changed(punit);
   unit_list_prepend(ptile->units, punit);
   unit_make_contact(punit, ptile, nullptr);
   if (pcity && !unit_has_type_flag(punit, UTYF_NOHOME)) {
@@ -3957,6 +3960,7 @@ static void unit_move_by_data(struct unit_move_data *pdata,
   /* Remove unit from the source tile. */
   fc_assert(unit_tile(punit) == psrctile);
 
+  map_world_unit_changed(punit);
 #ifndef FREECIV_NDEBUG
   bool success =
 #endif
-- 
2.55.0

