From 38d631fb42e4e6703884d223058128b28c49b437 Mon Sep 17 00:00:00 2001
From: Marko Lindqvist <cazfi74@gmail.com>
Date: Sun, 4 Oct 2026 09:21:00 +0300
Subject: [PATCH 79/79] Add [Armistice|Ceasefire]_Length_Pct effect types

See RM #2263

Signed-off-by: Marko Lindqvist <cazfi74@gmail.com>
---
 ai/default/daieffects.c             |  4 ++-
 data/alien/effects.ruleset          |  8 +++++
 data/civ1/effects.ruleset           |  8 +++++
 data/civ2/effects.ruleset           |  8 +++++
 data/civ2civ3/effects.ruleset       |  8 +++++
 data/classic/effects.ruleset        |  8 +++++
 data/goldkeep/effects.ruleset       |  8 +++++
 data/granularity/effects.ruleset    |  8 +++++
 data/multiplayer/effects.ruleset    |  8 +++++
 data/sandbox/effects.ruleset        |  8 +++++
 data/stub/effects.ruleset           |  8 +++++
 doc/README.effects                  | 10 ++++++
 gen_headers/enums/effects_enums.def |  2 ++
 server/diplhand.c                   | 52 ++++++++++++++++++++++++-----
 server/ruleset/rscompat.c           |  4 +++
 server/ruleset/rssanity.c           |  3 +-
 16 files changed, 145 insertions(+), 10 deletions(-)

diff --git a/ai/default/daieffects.c b/ai/default/daieffects.c
index 2a1238c1d1..bfc1b17f5d 100644
--- a/ai/default/daieffects.c
+++ b/ai/default/daieffects.c
@@ -622,11 +622,13 @@ adv_want dai_effect_value(struct player *pplayer,
   case EFT_NUKE_BLAST_RADIUS_1_SQ:
   case EFT_HEAL_UNIT_PCT:
   case EFT_TILE_CLAIMABLE:
+  case EFT_SHIELD2GOLD_PCT:
+  case EFT_ARMISTICE_LENGTH_PCT:
+  case EFT_CEASEFIRE_LENGTH_PCT:
     break;
     /* This has no effect for AI */
   case EFT_VISIBLE_WALLS:
   case EFT_CITY_IMAGE:
-  case EFT_SHIELD2GOLD_PCT:
     break;
   case EFT_PERFORMANCE:
   case EFT_NATION_PERFORMANCE:
diff --git a/data/alien/effects.ruleset b/data/alien/effects.ruleset
index fbc618856b..996615c4db 100644
--- a/data/alien/effects.ruleset
+++ b/data/alien/effects.ruleset
@@ -738,6 +738,14 @@ value   = 100
 type    = "Tech_Leakage"
 value   = 1
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 a02a54f617..f2827c292e 100644
--- a/data/civ1/effects.ruleset
+++ b/data/civ1/effects.ruleset
@@ -1275,6 +1275,14 @@ reqs    =
 type    = "Tech_Leakage"
 value   = 1
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 7b45640cf5..8a1fac2fde 100644
--- a/data/civ2/effects.ruleset
+++ b/data/civ2/effects.ruleset
@@ -2279,6 +2279,14 @@ reqs    =
 type    = "Tech_Leakage"
 value   = 1
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 80f31d08fc..0b4d24ab95 100644
--- a/data/civ2civ3/effects.ruleset
+++ b/data/civ2civ3/effects.ruleset
@@ -3809,6 +3809,14 @@ value   = 300
 type    = "Tech_Leakage"
 value   = 1
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 36bcd27370..5c894b86d9 100644
--- a/data/classic/effects.ruleset
+++ b/data/classic/effects.ruleset
@@ -2312,6 +2312,14 @@ value   = 100
 type    = "Tech_Leakage"
 value   = 1
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 d991078750..cd4f8916a3 100644
--- a/data/goldkeep/effects.ruleset
+++ b/data/goldkeep/effects.ruleset
@@ -2524,6 +2524,14 @@ value   = 100
 type    = "Tech_Leakage"
 value   = 1
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 e790019d9d..b90ca49302 100644
--- a/data/granularity/effects.ruleset
+++ b/data/granularity/effects.ruleset
@@ -122,6 +122,14 @@ reqs    =
       "Tech", "Literature", "Player"
     }
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 b7501ad5bb..7c645261a3 100644
--- a/data/multiplayer/effects.ruleset
+++ b/data/multiplayer/effects.ruleset
@@ -2341,6 +2341,14 @@ value   = 100
 type    = "Tech_Leakage"
 value   = 1
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 59b166303c..e9564f1b35 100644
--- a/data/sandbox/effects.ruleset
+++ b/data/sandbox/effects.ruleset
@@ -4030,6 +4030,14 @@ value   = 300
 type    = "Tech_Leakage"
 value   = 1
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 1f5936f67f..31024581fb 100644
--- a/data/stub/effects.ruleset
+++ b/data/stub/effects.ruleset
@@ -68,6 +68,14 @@ value   = 1
 type    = "Tech_Cost_Pct"
 value   = 100
 
+[effect_armistice_len_pct]
+type    = "Armistice_Length_Pct"
+value   = 100
+
+[effect_ceasefire_len_pct]
+type    = "Ceasefire_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 aa32e16ad8..8ea568b460 100644
--- a/doc/README.effects
+++ b/doc/README.effects
@@ -238,6 +238,11 @@ Airlift
 Any_Government
     Allow changing to any form of government regardless of tech prerequisites.
 
+Armistice_Length_Pct
+    How long an armistice agreed by the two players last before turning
+    into a peace treaty. The effect is evaluated for both parties of
+    the treaty, and the longer of the times applies.
+
 Attack_Bonus
     Increases offensive bonuses of units. Unit requirements on this effect are
     the attacking unit itself. Can be limited to only apply to some actions
@@ -319,6 +324,11 @@ Casus_Belli_Success
       Disband Unit Recover, Establish Trade Route, Enter Marketplace,
       Pillage, Spy Escape, Surgical Strike Building, Surgical Strike Production
 
+Ceasefire_Length_Pct
+    How long a cease-fire agreed by the two players last before reverting
+    back to war. The effect is evaluated for both parties of the treaty,
+    and the shorter of the times applies.
+
 City_Build_Slots
     Increase the number of units with no population cost a city can build in
     a turn if there are enough shields.
diff --git a/gen_headers/enums/effects_enums.def b/gen_headers/enums/effects_enums.def
index 927bda1956..3bcce414b0 100644
--- a/gen_headers/enums/effects_enums.def
+++ b/gen_headers/enums/effects_enums.def
@@ -166,6 +166,8 @@ values
   IMPR_UPKEEP_REDUCTION             "Impr_Upkeep_Reduction"
   CULTURE_PCT                       "Culture_Pct"
   TILE_CLAIMABLE                    "Tile_Claimable"
+  ARMISTICE_LENGTH_PCT              "Armistice_Length_Pct"
+  CEASEFIRE_LENGTH_PCT              "Ceasefire_Length_Pct"
   USER_EFFECT_1                     "User_Effect_1"
   USER_EFFECT_2                     "User_Effect_2"
   USER_EFFECT_3                     "User_Effect_3"
diff --git a/server/diplhand.c b/server/diplhand.c
index 4f6c30352a..5179d0f035 100644
--- a/server/diplhand.c
+++ b/server/diplhand.c
@@ -442,6 +442,8 @@ void handle_diplomacy_accept_treaty_req(struct player *pplayer,
       enum diplstate_type old_diplstate = ds_giverdest->type;
       struct unit_list *pgiver_seen_units BAD_HEURISTIC_INIT(NULL);
       struct unit_list *pdest_seen_units BAD_HEURISTIC_INIT(NULL);
+      int armistice_length, arm_1, arm_2;
+      int ceasefire_length, cf_1, cf_2;
 
       switch (pclause->type) {
       case CLAUSE_EMBASSY:
@@ -563,8 +565,25 @@ void handle_diplomacy_accept_treaty_req(struct player *pplayer,
           pdest_seen_units = get_units_seen_via_ally(pdest, pgiver);
         }
         set_diplstate_type(ds_giverdest, ds_destgiver, DS_CEASEFIRE);
-        ds_giverdest->turns_left = game.info.ceasefire_length;
-        ds_destgiver->turns_left = game.info.ceasefire_length;
+        cf_1 = get_target_bonus_effects(nullptr,
+                                        &(const struct req_context) {
+                                          .player = pgiver,
+                                        },
+                                        &(const struct req_context) {
+                                          .player = pdest,
+                                        }, EFT_CEASEFIRE_LENGTH_PCT);
+        cf_2 = get_target_bonus_effects(nullptr,
+                                        &(const struct req_context) {
+                                          .player = pdest,
+                                        },
+                                        &(const struct req_context) {
+                                          .player = pgiver,
+                                        }, EFT_CEASEFIRE_LENGTH_PCT);
+        ceasefire_length
+          = game.info.ceasefire_length * MIN(cf_1, cf_2) / 100;
+        ceasefire_length = MAX(1, ceasefire_length);
+        ds_giverdest->turns_left = ceasefire_length;
+        ds_destgiver->turns_left = ceasefire_length;
         notify_player(pgiver, NULL, E_TREATY_CEASEFIRE, ftc_server,
                       _("You agree on a cease-fire with %s."),
                       player_name(pdest));
@@ -587,8 +606,25 @@ void handle_diplomacy_accept_treaty_req(struct player *pplayer,
           pdest_seen_units = get_units_seen_via_ally(pdest, pgiver);
         }
         set_diplstate_type(ds_giverdest, ds_destgiver, DS_ARMISTICE);
-        ds_giverdest->turns_left = game.info.armistice_length;
-        ds_destgiver->turns_left = game.info.armistice_length;
+        arm_1 = get_target_bonus_effects(nullptr,
+                                         &(const struct req_context) {
+                                           .player = pgiver,
+                                         },
+                                         &(const struct req_context) {
+                                           .player = pdest,
+                                         }, EFT_ARMISTICE_LENGTH_PCT);
+        arm_2 = get_target_bonus_effects(nullptr,
+                                         &(const struct req_context) {
+                                           .player = pdest,
+                                         },
+                                         &(const struct req_context) {
+                                           .player = pgiver,
+                                         }, EFT_ARMISTICE_LENGTH_PCT);
+        armistice_length
+          = game.info.armistice_length * MAX(arm_1, arm_2) / 100;
+        armistice_length = MAX(1, armistice_length);
+        ds_giverdest->turns_left = armistice_length;
+        ds_destgiver->turns_left = armistice_length;
         notify_player(pgiver, NULL, E_TREATY_PEACE, ftc_server,
                       /* TRANS: ... the Poles ... Polish territory */
                       PL_("You agree on an armistice with the %s. In %d turn, "
@@ -599,9 +635,9 @@ void handle_diplomacy_accept_treaty_req(struct player *pplayer,
                           "it will become a peace treaty. Move any "
                           "military units out of %s territory to avoid them "
                           "being disbanded.",
-                          game.info.armistice_length),
+                          armistice_length),
                       nation_plural_for_player(pdest),
-                      game.info.armistice_length,
+                      armistice_length,
                       nation_adjective_for_player(pdest));
         notify_player(pdest, NULL, E_TREATY_PEACE, ftc_server,
                       /* TRANS: ... the Poles ... Polish territory */
@@ -613,9 +649,9 @@ void handle_diplomacy_accept_treaty_req(struct player *pplayer,
                           "it will become a peace treaty. Move any "
                           "military units out of %s territory to avoid them "
                           "being disbanded.",
-                          game.info.armistice_length),
+                          armistice_length),
                       nation_plural_for_player(pgiver),
-                      game.info.armistice_length,
+                      armistice_length,
                       nation_adjective_for_player(pgiver));
         if (old_diplstate == DS_ALLIANCE) {
           update_players_after_alliance_breakup(pgiver, pdest,
diff --git a/server/ruleset/rscompat.c b/server/ruleset/rscompat.c
index 8d3853b9ce..86f71434ec 100644
--- a/server/ruleset/rscompat.c
+++ b/server/ruleset/rscompat.c
@@ -515,6 +515,10 @@ void rscompat_postprocess(struct rscompat_info *info)
    * the new effects from being upgraded by accident. */
   iterate_effect_cache(effect_list_compat_cb, 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);
+
   /* Make sure that all action enablers added or modified by the
    * compatibility post processing fulfills all hard action requirements. */
   rscompat_enablers_add_obligatory_hard_reqs();
diff --git a/server/ruleset/rssanity.c b/server/ruleset/rssanity.c
index c6f72adde5..98cc0429b8 100644
--- a/server/ruleset/rssanity.c
+++ b/server/ruleset/rssanity.c
@@ -52,7 +52,8 @@ enum effect_type req_base_effects[] =
 /* These have been made mandatory in freeciv-3.4 */
 enum effect_type req_base_effects_3_4[] =
   {
-    /* None yet */
+    EFT_ARMISTICE_LENGTH_PCT,
+    EFT_CEASEFIRE_LENGTH_PCT,
     EFT_COUNT
   };
 
-- 
2.53.0

