Skip to main content

rpfm_extensions/startpos/world/
diplomacy.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//! Diplomatic relationships between factions (`FACTION/OLD_DIPLOMACY_MANAGER`).
12//!
13//! Each `start_pos_diplomacy` row gives both factions a relationship: its stance (vassal and client state rows give
14//! the overlord the `master` and `protector` stances), trade agreement, non-aggression pact and military access.
15//! Vassals join their master's wars, and lose the non-aggression pacts and military access of their rows with other
16//! factions (they keep trade agreements; client states keep everything). Factions know each other if they have a row,
17//! an inherited war, neighbouring regions or a `start_pos_past_events` row. Verified on Attila's main campaign:
18//! stances, flags and known factions match for every pair of factions (the vassal rule has a single case there).
19
20use getset::{CopyGetters, Getters};
21
22use std::collections::{HashMap, HashSet};
23
24use rpfm_lib::error::Result;
25
26use super::CampaignIndex;
27use crate::startpos::region_map::RegionMap;
28use crate::startpos::tables::StartposTables;
29
30#[cfg(test)] #[path = "diplomacy_test.rs"] mod diplomacy_test;
31
32/// Stance of factions without a relationship row.
33pub const NEUTRAL_STANCE: &str = "neutral";
34
35/// Stance of factions at war.
36const WAR_STANCE: &str = "war";
37
38/// Value a vassal pair stores on both sides (`#7` on the vassal's, `#8` on the master's). The game stores 20 for the
39/// Sassanid vassals and 40 for the Eastern Roman ones; the source isn't known.
40const VASSAL_VALUE: i32 = 20;
41
42//---------------------------------------------------------------------------//
43//                              Types
44//---------------------------------------------------------------------------//
45
46/// The relationship of a faction with another one.
47#[derive(Debug, Clone, PartialEq, Eq, Getters, CopyGetters)]
48pub struct Relationship {
49
50    /// Stance: `neutral`, `war`, `military_allies`, `defensive_allies`, `vassal`, `master`, `client_state` or
51    /// `protector`.
52    #[getset(get = "pub")]
53    stance: String,
54
55    /// They have a trade agreement.
56    #[getset(get_copy = "pub")]
57    trade: bool,
58
59    /// They have a non-aggression pact.
60    #[getset(get_copy = "pub")]
61    non_aggression: bool,
62
63    /// They grant each other military access.
64    #[getset(get_copy = "pub")]
65    military_access: bool,
66
67    /// Value of vassal pairs: on the vassal's side and on the master's side.
68    #[getset(get_copy = "pub")]
69    vassal_values: (i32, i32),
70
71    /// Enemies both factions are at war with, and those enemies' defensive allies (flagged `true`).
72    #[getset(get = "pub")]
73    allied_against: Vec<(String, bool)>,
74
75    /// The factions know each other.
76    #[getset(get_copy = "pub")]
77    known: bool,
78}
79
80/// The diplomatic relationships of the campaign's factions.
81#[derive(Debug, Clone, Default)]
82pub struct Diplomacy {
83
84    /// Relationships that aren't the default neutral one, by (faction, other faction) keys.
85    relationships: HashMap<(String, String), Relationship>,
86}
87
88//---------------------------------------------------------------------------//
89//                              Implementations
90//---------------------------------------------------------------------------//
91
92impl Default for Relationship {
93    fn default() -> Self {
94        Self {
95            stance: NEUTRAL_STANCE.to_owned(),
96            trade: false,
97            non_aggression: false,
98            military_access: false,
99            vassal_values: (0, 0),
100            allied_against: vec![],
101            known: false,
102        }
103    }
104}
105
106impl Diplomacy {
107
108    /// Builds the relationships of the campaign's factions.
109    ///
110    /// # Arguments
111    ///
112    /// * `tables` - Tables with `start_pos_diplomacy` and `start_pos_past_events`.
113    /// * `index` - The campaign's startpos rows.
114    /// * `map` - Region of each cell of the campaign map, to find the factions with neighbouring regions.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if a table or column is missing.
119    pub fn new(tables: &StartposTables, index: &CampaignIndex, map: &RegionMap) -> Result<Self> {
120        let mut rows = vec![];
121        for row in tables.table("start_pos_diplomacy")?.rows() {
122            let (Some(first), Some(second)) = (index.faction_by_id(&row.str("faction1")?), index.faction_by_id(&row.str("faction2")?)) else { continue };
123            rows.push((first.key().to_owned(), second.key().to_owned(), row.str("stance")?.into_owned(), row.bool("grants_trade_agreement")?, row.bool("non_aggression_pact")?, row.bool("grants_military_access")?));
124        }
125
126        let vassals = rows.iter().filter(|row| row.2 == "vassal").map(|row| row.0.to_owned()).collect::<HashSet<_>>();
127        let mut diplomacy = Self::default();
128        for (first, second, stance, trade, non_aggression, military_access) in &rows {
129            let of_vassal = stance != "vassal" && (vassals.contains(first) || vassals.contains(second));
130            let (first_stance, second_stance) = match stance.as_str() {
131                "vassal" => ("vassal", "master"),
132                "client_state" => ("client_state", "protector"),
133                other => (other, other),
134            };
135            let vassal = stance == "vassal";
136            for (faction, other, stance) in [(first, second, first_stance), (second, first, second_stance)] {
137                let relationship = diplomacy.entry(faction, other);
138                relationship.stance = stance.to_owned();
139                relationship.trade = *trade;
140                relationship.non_aggression = *non_aggression && !of_vassal;
141                relationship.military_access = *military_access && !of_vassal;
142                relationship.known = true;
143            }
144            if vassal {
145                diplomacy.entry(first, second).vassal_values = (VASSAL_VALUE, 0);
146                diplomacy.entry(second, first).vassal_values = (0, VASSAL_VALUE);
147            }
148        }
149
150        // Vassals join their master's wars. Wars here are the ones from rows, so they don't chain.
151        let wars = rows.iter().filter(|row| row.2 == WAR_STANCE).map(|row| (row.0.to_owned(), row.1.to_owned())).collect::<Vec<_>>();
152        let enemies_of = |faction: &str| wars.iter()
153            .filter_map(|(first, second)| if first == faction { Some(second.to_owned()) } else if second == faction { Some(first.to_owned()) } else { None })
154            .collect::<Vec<_>>();
155        for (subject, master, ..) in rows.iter().filter(|row| row.2 == "vassal") {
156            for enemy in enemies_of(master) {
157                if enemy != *subject && !diplomacy.relationships.contains_key(&(subject.to_owned(), enemy.to_owned())) {
158                    for (faction, other) in [(subject, &enemy), (&enemy, subject)] {
159                        let relationship = diplomacy.entry(faction, other);
160                        relationship.stance = WAR_STANCE.to_owned();
161                        relationship.known = true;
162                    }
163                }
164            }
165        }
166
167        // Allied factions at war with the same enemy fight it together, and against its defensive allies.
168        let defensive_allies_of = |faction: &str| rows.iter()
169            .filter(|row| row.2 == "defensive_allies")
170            .filter_map(|row| if row.0 == faction { Some(row.1.to_owned()) } else if row.1 == faction { Some(row.0.to_owned()) } else { None })
171            .collect::<Vec<_>>();
172        for (first, second, ..) in rows.iter().filter(|row| matches!(row.2.as_str(), "vassal" | "client_state" | "military_allies" | "defensive_allies")) {
173            let second_enemies = enemies_of(second).into_iter().collect::<HashSet<_>>();
174            let common = enemies_of(first).into_iter().filter(|enemy| second_enemies.contains(enemy)).collect::<Vec<_>>();
175            let mut against = vec![];
176            for enemy in common {
177                let allies = defensive_allies_of(&enemy);
178                against.push((enemy, false));
179                against.extend(allies.into_iter().map(|ally| (ally, true)));
180            }
181            if !against.is_empty() {
182                diplomacy.entry(first, second).allied_against = against.clone();
183                diplomacy.entry(second, first).allied_against = against;
184            }
185        }
186
187        // Factions with neighbouring regions know each other.
188        let owners = index.regions().iter().filter_map(|region| region.owner().as_ref().map(|owner| (region.key().as_str(), owner.as_str()))).collect::<HashMap<_, _>>();
189        for (region, other_region) in map.neighbour_pairs() {
190            if let (Some(owner), Some(other_owner)) = (owners.get(region), owners.get(other_region)) {
191                if owner != other_owner {
192                    diplomacy.entry(owner, other_owner).known = true;
193                    diplomacy.entry(other_owner, owner).known = true;
194                }
195            }
196        }
197
198        // Factions with past events between them know each other.
199        for row in tables.table("start_pos_past_events")?.rows() {
200            let (Some(source), Some(target)) = (index.faction_by_id(&row.str("source")?), index.faction_by_id(&row.str("target")?)) else { continue };
201            if source.key() != target.key() {
202                diplomacy.entry(source.key(), target.key()).known = true;
203                diplomacy.entry(target.key(), source.key()).known = true;
204            }
205        }
206
207        Ok(diplomacy)
208    }
209
210    /// Returns the relationship of a faction with another one: neutral if nothing sets it.
211    pub fn relationship(&self, faction: &str, other: &str) -> Relationship {
212        self.relationships.get(&(faction.to_owned(), other.to_owned())).cloned().unwrap_or_default()
213    }
214
215    /// Returns the relationship of a faction with another one, creating a neutral one if needed.
216    fn entry(&mut self, faction: &str, other: &str) -> &mut Relationship {
217        self.relationships.entry((faction.to_owned(), other.to_owned())).or_default()
218    }
219}