Skip to main content

rpfm_extensions/startpos/cai/
stances.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Wars and stance re-evaluation marks the AI starts with.
12//!
13//! While setting up the starting diplomacy, the game goes through each faction's relationships (factions and their
14//! relationships in startpos order) and fires the event of each `start_pos_diplomacy` row. The AI's diplomacy listener
15//! reacts to those events by marking, in some factions' strategic stance managers, other factions whose stance they
16//! must re-evaluate (`CAI_FACTION_STANCE_BLOCKING_AND_PROMOTION`):
17//!
18//! - War rows (on the declaring `faction1`'s turn): the target marks the declarer, and so do the target's relatives.
19//!   Then each established vassal of either side joins the war against the other side, with the same marks.
20//! - Vassal rows (on the vassal's turn): the vassal first declares war on its master's enemies; it and its military
21//!   access partners mark each other (the access is cancelled); it becomes established; then it marks its master,
22//!   its relatives mark the master and the master's relatives mark it. Client state rows: the same marks, no wars.
23//! - Alliance rows: no marks, but the processing faction adds the ally to its relatives.
24//!
25//! A faction's relatives are its vassals, overlord, client states, protector and allies (the alliance row's
26//! `faction2` holds `faction1` from the start). Verified on Attila's main campaign: 172/172 marks.
27
28use std::collections::{BTreeSet, HashMap, HashSet};
29
30use rpfm_lib::error::Result;
31
32use crate::startpos::tables::StartposTables;
33use crate::startpos::world::CampaignIndex;
34
35#[cfg(test)] #[path = "stances_test.rs"] mod stances_test;
36
37//---------------------------------------------------------------------------//
38//                              Types
39//---------------------------------------------------------------------------//
40
41/// Wars and stance re-evaluation marks after the starting diplomacy.
42#[derive(Debug, Clone, Default)]
43pub struct StartingStances {
44
45    /// Factions each faction marked, in marking order, by faction index.
46    marks: Vec<Vec<usize>>,
47
48    /// Pairs of faction indexes at war, smallest first.
49    wars: HashSet<(usize, usize)>,
50}
51
52/// State of the starting diplomacy while its events fire.
53struct Simulation {
54    stances: StartingStances,
55
56    /// Relatives of each faction (see the module docs), by faction index.
57    relatives: Vec<BTreeSet<usize>>,
58
59    /// Vassals each faction has once their row was processed.
60    established: Vec<BTreeSet<usize>>,
61}
62
63//---------------------------------------------------------------------------//
64//                              Implementations
65//---------------------------------------------------------------------------//
66
67impl StartingStances {
68
69    /// Simulates the starting diplomacy.
70    ///
71    /// # Arguments
72    ///
73    /// * `tables` - Tables with `start_pos_diplomacy`.
74    /// * `index` - The campaign's startpos rows.
75    /// * `factions` - Keys of the world's factions, in startpos order.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the table or one of its columns is missing.
80    pub fn new(tables: &StartposTables, index: &CampaignIndex, factions: &[&str]) -> Result<Self> {
81        let position = factions.iter().enumerate().map(|(position, key)| (*key, position)).collect::<HashMap<_, _>>();
82        let mut stances = HashMap::new();
83        let mut access = vec![BTreeSet::new(); factions.len()];
84        for row in tables.table("start_pos_diplomacy")?.rows() {
85            let (Some(first), Some(second)) = (index.faction_by_id(&row.str("faction1")?), index.faction_by_id(&row.str("faction2")?)) else { continue };
86            let (Some(&first), Some(&second)) = (position.get(first.key().as_str()), position.get(second.key().as_str())) else { continue };
87            stances.insert((first, second), row.str("stance")?.into_owned());
88            if row.bool("grants_military_access")? {
89                access[first].insert(second);
90                access[second].insert(first);
91            }
92        }
93
94        let mut simulation = Simulation {
95            stances: Self { marks: vec![vec![]; factions.len()], wars: HashSet::new() },
96            relatives: vec![BTreeSet::new(); factions.len()],
97            established: vec![BTreeSet::new(); factions.len()],
98        };
99        for ((first, second), stance) in &stances {
100            if matches!(stance.as_str(), "vassal" | "client_state" | "military_allies" | "defensive_allies") {
101                simulation.relatives[*second].insert(*first);
102            }
103            if matches!(stance.as_str(), "vassal" | "client_state") {
104                simulation.relatives[*first].insert(*second);
105            }
106        }
107
108        for (faction, partners) in access.iter().enumerate() {
109            for other in (0..factions.len()).filter(|other| *other != faction) {
110                match stances.get(&(faction, other)).map(String::as_str) {
111                    Some("war") => if !simulation.stances.at_war(faction, other) {
112                        simulation.declare_war(faction, other);
113                    },
114                    Some("vassal") => {
115                        for enemy in 0..factions.len() {
116                            if enemy != faction && enemy != other && simulation.stances.at_war(other, enemy) && !simulation.stances.at_war(faction, enemy) {
117                                simulation.declare_war(faction, enemy);
118                            }
119                        }
120                        for &partner in partners.iter().filter(|partner| **partner != other) {
121                            simulation.stances.mark(faction, partner);
122                            simulation.stances.mark(partner, faction);
123                        }
124                        simulation.established[other].insert(faction);
125                        simulation.mark_subject(faction, other);
126                    },
127                    Some("client_state") => simulation.mark_subject(faction, other),
128                    Some("military_allies" | "defensive_allies") => { simulation.relatives[faction].insert(other); },
129                    _ => if matches!(stances.get(&(other, faction)).map(String::as_str), Some("military_allies" | "defensive_allies")) {
130                        simulation.relatives[faction].insert(other);
131                    },
132                }
133            }
134        }
135        Ok(simulation.stances)
136    }
137
138    /// Returns the factions a faction marked for re-evaluation, in marking order.
139    ///
140    /// # Arguments
141    ///
142    /// * `faction` - Index of the faction in startpos order.
143    pub fn marks(&self, faction: usize) -> &[usize] {
144        self.marks.get(faction).map(Vec::as_slice).unwrap_or_default()
145    }
146
147    /// Returns if two factions are at war.
148    ///
149    /// # Arguments
150    ///
151    /// * `faction` - Index of a faction in startpos order.
152    /// * `other` - Index of the other faction.
153    pub fn at_war(&self, faction: usize, other: usize) -> bool {
154        self.wars.contains(&(faction.min(other), faction.max(other)))
155    }
156
157    /// Marks a faction in another one's stance manager, once.
158    fn mark(&mut self, faction: usize, target: usize) {
159        if !self.marks[faction].contains(&target) {
160            self.marks[faction].push(target);
161        }
162    }
163}
164
165impl Simulation {
166
167    /// Declares a war: the target and its relatives mark the declarer, then established vassals join in.
168    fn declare_war(&mut self, declarer: usize, target: usize) {
169        self.stances.wars.insert((declarer.min(target), declarer.max(target)));
170        self.stances.mark(target, declarer);
171        self.notify_relatives(target, declarer);
172
173        let joining = self.established[declarer].iter().map(|vassal| (*vassal, target))
174            .chain(self.established[target].iter().map(|vassal| (*vassal, declarer)))
175            .collect::<Vec<_>>();
176        for (vassal, enemy) in joining {
177            if vassal != enemy && !self.stances.at_war(vassal, enemy) {
178                self.declare_war(vassal, enemy);
179            }
180        }
181    }
182
183    /// Makes the marks of a vassal or client state row.
184    fn mark_subject(&mut self, subject: usize, overlord: usize) {
185        self.stances.mark(subject, overlord);
186        self.notify_relatives(subject, overlord);
187        self.notify_relatives(overlord, subject);
188    }
189
190    /// Makes every relative of a faction (but the target) mark the target.
191    fn notify_relatives(&mut self, faction: usize, target: usize) {
192        let relatives = self.relatives[faction].iter().copied().filter(|relative| *relative != target).collect::<Vec<_>>();
193        for relative in relatives {
194            self.stances.mark(relative, target);
195        }
196    }
197}