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}