From 3151141ce7a43632bdf521127c88f9f8d5b27ac2 Mon Sep 17 00:00:00 2001
From: Marko Lindqvist <cazfi74@gmail.com>
Date: Sun, 25 Oct 2026 05:47:53 +0200
Subject: [PATCH 91/91] Add Contact_Length_Pct effect type

See RM #2313

Signed-off-by: Marko Lindqvist <cazfi74@gmail.com>
---
 ai/default/daieffects.c             |  1 +
 data/alien/effects.ruleset          |  4 ++++
 data/civ1/effects.ruleset           |  4 ++++
 data/civ2/effects.ruleset           |  4 ++++
 data/civ2civ3/effects.ruleset       |  4 ++++
 data/classic/effects.ruleset        |  4 ++++
 data/goldkeep/effects.ruleset       |  4 ++++
 data/granularity/effects.ruleset    |  4 ++++
 data/multiplayer/effects.ruleset    |  4 ++++
 data/sandbox/effects.ruleset        |  6 +++++-
 data/stub/effects.ruleset           |  4 ++++
 doc/README.effects                  |  5 +++++
 gen_headers/enums/effects_enums.def |  1 +
 server/plrhand.c                    | 29 +++++++++++++++++++++++++++--
 server/ruleset/rscompat.c           |  1 +
 server/ruleset/rssanity.c           |  1 +
 server/settings.c                   |  3 ++-
 17 files changed, 79 insertions(+), 4 deletions(-)

diff --git a/ai/default/daieffects.c b/ai/default/daieffects.c
index bfc1b17f5d..8288b99dfe 100644
--- a/ai/default/daieffects.c
+++ b/ai/default/daieffects.c
@@ -625,6 +625,7 @@ adv_want dai_effect_value(struct player *pplayer,
   case EFT_SHIELD2GOLD_PCT:
   case EFT_ARMISTICE_LENGTH_PCT:
   case EFT_CEASEFIRE_LENGTH_PCT:
+  case EFT_CONTACT_LENGTH_PCT:
     break;
     /* This has no effect for AI */
   case EFT_VISIBLE_WALLS:
diff --git a/data/alien/effects.ruleset b/data/alien/effects.ruleset
index 996615c4db..4bdfcb037f 100644
--- a/data/alien/effects.ruleset
+++ b/data/alien/effects.ruleset
@@ -746,6 +746,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 [effect_mood_center]
 type    = "Make_Content"
 value	= 3
diff --git a/data/civ1/effects.ruleset b/data/civ1/effects.ruleset
index f2827c292e..872cb2cd71 100644
--- a/data/civ1/effects.ruleset
+++ b/data/civ1/effects.ruleset
@@ -1283,6 +1283,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 ; Cities can always work tiles
 [effect_tile_workable]
 type    = "Tile_Workable"
diff --git a/data/civ2/effects.ruleset b/data/civ2/effects.ruleset
index 8a1fac2fde..727cce1411 100644
--- a/data/civ2/effects.ruleset
+++ b/data/civ2/effects.ruleset
@@ -2287,6 +2287,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 ; Cities can always work tiles
 [effect_tile_workable]
 type    = "Tile_Workable"
diff --git a/data/civ2civ3/effects.ruleset b/data/civ2civ3/effects.ruleset
index 0b4d24ab95..da414ef269 100644
--- a/data/civ2civ3/effects.ruleset
+++ b/data/civ2civ3/effects.ruleset
@@ -3817,6 +3817,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 ; Cities can always work tiles
 [effect_tile_workable]
 type    = "Tile_Workable"
diff --git a/data/classic/effects.ruleset b/data/classic/effects.ruleset
index 5c894b86d9..875d9be207 100644
--- a/data/classic/effects.ruleset
+++ b/data/classic/effects.ruleset
@@ -2320,6 +2320,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 ; Cities can always work tiles
 [effect_tile_workable]
 type    = "Tile_Workable"
diff --git a/data/goldkeep/effects.ruleset b/data/goldkeep/effects.ruleset
index cd4f8916a3..fea6529209 100644
--- a/data/goldkeep/effects.ruleset
+++ b/data/goldkeep/effects.ruleset
@@ -2532,6 +2532,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 ; Cities can always work tiles
 [effect_tile_workable]
 type    = "Tile_Workable"
diff --git a/data/granularity/effects.ruleset b/data/granularity/effects.ruleset
index e25156c1e0..f073624d38 100644
--- a/data/granularity/effects.ruleset
+++ b/data/granularity/effects.ruleset
@@ -130,6 +130,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 [effect_tile_land_workable]
 type    = "Tile_Workable"
 value   = 1
diff --git a/data/multiplayer/effects.ruleset b/data/multiplayer/effects.ruleset
index 7c645261a3..999a93da1f 100644
--- a/data/multiplayer/effects.ruleset
+++ b/data/multiplayer/effects.ruleset
@@ -2349,6 +2349,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 ; Cities can always work tiles
 [effect_tile_workable]
 type    = "Tile_Workable"
diff --git a/data/sandbox/effects.ruleset b/data/sandbox/effects.ruleset
index e9564f1b35..5b1bcaf192 100644
--- a/data/sandbox/effects.ruleset
+++ b/data/sandbox/effects.ruleset
@@ -1,4 +1,4 @@
-; Modifying this file:
+d; Modifying this file:
 ; You should not modify this file except to make bugfixes or
 ; for other "maintenance". If you want to make custom changes,
 ; you should create a new datadir subdirectory and copy this file
@@ -4038,6 +4038,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 ; Cities can always work tiles
 [effect_tile_workable]
 type    = "Tile_Workable"
diff --git a/data/stub/effects.ruleset b/data/stub/effects.ruleset
index 31024581fb..687e2f7222 100644
--- a/data/stub/effects.ruleset
+++ b/data/stub/effects.ruleset
@@ -76,6 +76,10 @@ value   = 100
 type    = "Ceasefire_Length_Pct"
 value   = 100
 
+[effect_contact_len_pct]
+type    = "Contact_Length_Pct"
+value   = 100
+
 ; Cities can always work tiles
 [effect_tile_workable]
 type    = "Tile_Workable"
diff --git a/doc/README.effects b/doc/README.effects
index 8ea568b460..9e78d1858c 100644
--- a/doc/README.effects
+++ b/doc/README.effects
@@ -367,6 +367,11 @@ Conquest_Tech_Pct
     Percent chance that a player conquering a city learns a tech from the
     former owner.
 
+Contact_Length_Pct
+    How long a contact remains active after player's units last met.
+    The effect is evaluated for both sides, and the shorter of
+    the times applies.
+
 Culture_Pct
     Percent change to culture, both to current performance and to
     the accumulating history.
diff --git a/gen_headers/enums/effects_enums.def b/gen_headers/enums/effects_enums.def
index 3bcce414b0..67fa4539d2 100644
--- a/gen_headers/enums/effects_enums.def
+++ b/gen_headers/enums/effects_enums.def
@@ -168,6 +168,7 @@ values
   TILE_CLAIMABLE                    "Tile_Claimable"
   ARMISTICE_LENGTH_PCT              "Armistice_Length_Pct"
   CEASEFIRE_LENGTH_PCT              "Ceasefire_Length_Pct"
+  CONTACT_LENGTH_PCT                "Contact_Length_Pct"
   USER_EFFECT_1                     "User_Effect_1"
   USER_EFFECT_2                     "User_Effect_2"
   USER_EFFECT_3                     "User_Effect_3"
diff --git a/server/plrhand.c b/server/plrhand.c
index 759281b4f3..e60ad95295 100644
--- a/server/plrhand.c
+++ b/server/plrhand.c
@@ -2318,8 +2318,33 @@ void make_contact(struct player *pplayer1, struct player *pplayer2,
 
   if (get_player_bonus(pplayer1, EFT_NO_DIPLOMACY) <= 0
       && get_player_bonus(pplayer2, EFT_NO_DIPLOMACY) <= 0) {
-    ds_plr1plr2->contact_turns_left = game.server.contactturns;
-    ds_plr2plr1->contact_turns_left = game.server.contactturns;
+    int ct_1, ct_2;
+    int contact_turns;
+
+    if (game.server.contactturns == 0) {
+      contact_turns = 0;
+    } else {
+      ct_1 = get_target_bonus_effects(nullptr,
+                                      &(const struct req_context) {
+                                        .player = pplayer1,
+                                      },
+                                      &(const struct req_context) {
+                                        .player = pplayer2,
+                                      }, EFT_CONTACT_LENGTH_PCT);
+      ct_2 = get_target_bonus_effects(nullptr,
+                                      &(const struct req_context) {
+                                        .player = pplayer2,
+                                      },
+                                      &(const struct req_context) {
+                                        .player = pplayer1,
+                                      }, EFT_CONTACT_LENGTH_PCT);
+      contact_turns
+        = game.server.contactturns * MIN(ct_1, ct_2) / 100;
+      contact_turns = MAX(1, contact_turns);
+    }
+
+    ds_plr1plr2->contact_turns_left = contact_turns;
+    ds_plr2plr1->contact_turns_left = contact_turns;
   }
   if (ds_plr1plr2->type == DS_NO_CONTACT) {
     enum diplstate_type new_state = get_default_diplstate(pplayer1,
diff --git a/server/ruleset/rscompat.c b/server/ruleset/rscompat.c
index 86f71434ec..453c705c30 100644
--- a/server/ruleset/rscompat.c
+++ b/server/ruleset/rscompat.c
@@ -518,6 +518,7 @@ void rscompat_postprocess(struct rscompat_info *info)
   /* Old behavior: Armistice and Ceasefire took full (!00%) time. */
   effect_new(EFT_ARMISTICE_LENGTH_PCT, 100, nullptr);
   effect_new(EFT_CEASEFIRE_LENGTH_PCT, 100, nullptr);
+  effect_new(EFT_CONTACT_LENGTH_PCT, 100, nullptr);
 
   /* Make sure that all action enablers added or modified by the
    * compatibility post processing fulfills all hard action requirements. */
diff --git a/server/ruleset/rssanity.c b/server/ruleset/rssanity.c
index 98cc0429b8..a35307d3ec 100644
--- a/server/ruleset/rssanity.c
+++ b/server/ruleset/rssanity.c
@@ -54,6 +54,7 @@ enum effect_type req_base_effects_3_4[] =
   {
     EFT_ARMISTICE_LENGTH_PCT,
     EFT_CEASEFIRE_LENGTH_PCT,
+    EFT_CONTACT_LENGTH_PCT,
     EFT_COUNT
   };
 
diff --git a/server/settings.c b/server/settings.c
index eab7c1398f..84f8ae4e22 100644
--- a/server/settings.c
+++ b/server/settings.c
@@ -2771,7 +2771,8 @@ static struct setting settings[] = {
           N_("Turns until player contact is lost"),
           N_("Players may meet for diplomacy this number of turns "
              "after their units have last met, even when they do not have "
-             "an embassy. If set to zero, then players cannot meet unless "
+             "an embassy. This is a base value, which ruleset may adjust. "
+             "If set to zero, then players cannot meet unless "
              "they have an embassy."),
           nullptr, nullptr, nullptr,
           GAME_MIN_CONTACTTURNS, GAME_MAX_CONTACTTURNS,
-- 
2.53.0

