From f7c1233789dfb1ca50bbcf39d5b51f7a602ec16b Mon Sep 17 00:00:00 2001
From: Sweet Tea Dorminy <sweettea-kernel@dorminy.me>
Date: Sat, 7 Nov 2026 16:30:56 -0800
Subject: [PATCH] ai/default: Set diplomat_threat only if action possible.

city_data->diplomat_threat is documented as "Enemy diplomat or spy is
near the city" (daicity.h) and is the sole trigger for
dai_choose_diplomat_defensive()'s 16000 want, which outranks every
military build. assess_danger() set it for any unit for which
utype_acts_hostile() holds within two moves, and ACTION_HOSTILE is true
of attackers, pillagers, bombarders and city conquerors as well as of
the spy actions, so an approaching Warriors made the city buy a
Diplomat before a defender. Measured on 40 autogames: of the cities
with such a unit within two tiles, only 21-29% had an enemy diplomat
in the same radius.

Ask the enablers instead: the threat is raised when the unit's type can
perform a hostile diplomat action against a city (incite, poison,
plague, sabotage, steal, suitcase nuke) and some enabler of that action
has its actor and target requirements met for this unit and this city,
so the ruleset, the governments and the buildings involved all count.
Range and movement are deliberately not evaluated, since the unit is
still up to two moves away. A Diplomat that could do nothing to this
city under this government is no longer a threat to it.

See RM #2452

Signed-off-by: Sweet Tea Dorminy <sweettea-kernel@dorminy.me>
---
 ai/default/daimilitary.c | 66 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 65 insertions(+), 1 deletion(-)

diff --git a/ai/default/daimilitary.c b/ai/default/daimilitary.c
index fd456a347b..6b7c4a313b 100644
--- a/ai/default/daimilitary.c
+++ b/ai/default/daimilitary.c
@@ -68,6 +68,9 @@
 
 #include "daimilitary.h"
 
+static bool unit_is_diplomat_threat_to(const struct unit *punit,
+                                       const struct city *pcity);
+
 /* Size 1 city gets destroyed when conquered. It's still a good thing
  * stop enemy from having it. */
 #define CITY_CONQUEST_WORTH(_city_, _data_) \
@@ -902,7 +905,7 @@ static unsigned int assess_danger(struct ai_type *ait,
       (void) dai_wants_defender_against(ait, nmap, pplayer, pcity, utype,
                                         vulnerability / MAX(move_time, 1));
 
-      if (utype_acts_hostile(unit_type_get(punit)) && 2 >= move_time) {
+      if (2 >= move_time && unit_is_diplomat_threat_to(punit, pcity)) {
         city_data->diplomat_threat = TRUE;
       }
 
@@ -2072,3 +2075,64 @@ struct adv_choice *military_advisor_choose_build(struct ai_type *ait,
 
   return choice;
 }
+
+/**********************************************************************//**
+  Could this unit, once beside the city, perform a hostile diplomat
+  action against it under the current ruleset, governments and
+  buildings? The actor and target requirements of the enablers answer
+  that. Range and movement are left out on purpose: this is asked of a
+  unit that is still up to two moves away. Attacks, conquest and
+  bombardment are hostile too, but they have a military answer, not a
+  defensive Diplomat.
+**************************************************************************/
+static bool unit_is_diplomat_threat_to(const struct unit *punit,
+                                       const struct city *pcity)
+{
+  const struct unit_type *ut = unit_type_get(punit);
+  const struct req_context actor_ctx = {
+    .player = unit_owner(punit),
+    .tile = unit_tile(punit),
+    .unit = punit,
+    .unittype = ut,
+  };
+  const struct req_context target_ctx = {
+    .player = city_owner(pcity),
+    .city = pcity,
+    .tile = city_tile(pcity),
+  };
+
+  action_iterate(act_id) {
+    const struct action *paction = action_by_number(act_id);
+
+    if (action_get_target_kind(paction) != ATK_CITY
+        || !utype_can_do_action(ut, act_id)) {
+      continue;
+    }
+    switch (paction->result) {
+    case ACTRES_SPY_INCITE_CITY:
+    case ACTRES_SPY_POISON:
+    case ACTRES_SPY_SPREAD_PLAGUE:
+    case ACTRES_SPY_SABOTAGE_CITY:
+    case ACTRES_SPY_TARGETED_SABOTAGE_CITY:
+    case ACTRES_SPY_SABOTAGE_CITY_PRODUCTION:
+    case ACTRES_SPY_STEAL_TECH:
+    case ACTRES_SPY_TARGETED_STEAL_TECH:
+    case ACTRES_SPY_STEAL_GOLD:
+    case ACTRES_SPY_NUKE:
+      break;
+    default:
+      continue;
+    }
+    action_enabler_list_iterate(action_enablers_for_action(act_id),
+                                enabler) {
+      if (are_reqs_active(&actor_ctx, &target_ctx, &enabler->actor_reqs,
+                          RPT_CERTAIN)
+          && are_reqs_active(&target_ctx, &actor_ctx,
+                             &enabler->target_reqs, RPT_CERTAIN)) {
+        return TRUE;
+      }
+    } action_enabler_list_iterate_end;
+  } action_iterate_end;
+
+  return FALSE;
+}
-- 
2.55.0

