Skip to main content

rpfm_extensions/startpos/world/writer/
factions.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//! Writer of the `FACTION` records (`WORLD/FACTION_ARRAY`).
12
13use std::collections::HashMap;
14
15use rpfm_lib::error::Result;
16use rpfm_lib::files::esf::RecordNode;
17
18use super::armies::ArmyWriter;
19use super::characters::CharacterWriter;
20use super::government::{write_character_posts, PostPlan};
21use super::provinces::write_mercenary_pool;
22use super::sight::{write_shroud, SightPlan};
23use super::{child, next_id, start_pos_faction_row, WorldInputs, WorldPlan, WORLD_PATH};
24use crate::startpos::cai::technologies::faction_technologies;
25use crate::startpos::layout::{child_path, StartposLayout};
26use crate::startpos::vision::Vision;
27use crate::startpos::world::agents::recruitment_pool_agents;
28use crate::startpos::world::diplomacy::Diplomacy;
29use crate::startpos::world::government::political_parties;
30use crate::startpos::world::pools::{faction_mercenary_pool, levy_pools};
31use crate::startpos::world::FactionEntry;
32use crate::startpos::world::factions::{faction_flags, faction_summary, FactionFlag};
33
34/// Path of a faction.
35const FACTION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION";
36
37/// Localisation key prefix of the faction names.
38const SCREEN_NAME_PREFIX: &str = "factions_screen_name_";
39
40/// Lowest `start_pos_factions.honour` of the factions that start with an empty diplomatic memory (`+21` field).
41const EMPTY_MEMORY_HONOUR: i64 = 129;
42
43/// Length of the per-type event counters of a faction (`FACTION_FLAG_AND_COLOURS@2+19`). The game fills some of them
44/// for playable factions during the build (source unknown); zeros are compatible.
45const EVENT_COUNTERS: usize = 151;
46
47/// Culture whose factions have `FACTION_ACTION_LOG_BLOCK+12` unset (meaning unknown).
48const NOMADIC_CULTURE: &str = "att_cult_nomadic";
49
50/// Diplomatic memory of most factions (`FACTION_FLAG_AND_COLOURS@2+21`): only its fourth value is set.
51const DEFAULT_MEMORY: [u8; 22] = [0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
52
53//---------------------------------------------------------------------------//
54//                              Types
55//---------------------------------------------------------------------------//
56
57/// What the faction writer takes from the other writers and plans.
58pub struct FactionContext<'c, 'a, 'b> {
59
60    /// Writer of the forces.
61    pub armies: &'c ArmyWriter<'a, 'b>,
62
63    /// Writer of the characters.
64    pub characters: &'c CharacterWriter<'a, 'b>,
65
66    /// Government posts of the factions.
67    pub posts: &'c PostPlan,
68
69    /// Levy pools of the overlords.
70    pub levy_pools: &'c [PlannedLevyPool],
71
72    /// Diplomatic relationships between the factions.
73    pub diplomacy: &'c Diplomacy,
74
75    /// Vision of the world.
76    pub sight: &'c SightPlan,
77}
78
79/// A levy pool of an overlord, with its runtime ids.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct PlannedLevyPool {
82
83    /// Mercenary pool id.
84    id: u32,
85
86    /// Overlord faction key.
87    overlord: String,
88
89    /// Runtime id of the subject faction.
90    subject: u32,
91
92    /// Runtime id of the province.
93    province: u32,
94}
95
96//---------------------------------------------------------------------------//
97//                              Implementations
98//---------------------------------------------------------------------------//
99
100/// Writes the rebel faction (`REBEL_FACTION`): the layout's, with the number of regions without an owner.
101///
102/// # Errors
103///
104/// Returns an error if the layout lacks the records.
105pub fn write_rebel_faction(layout: &StartposLayout, plan: &WorldPlan) -> Result<RecordNode> {
106    let path = child_path(WORLD_PATH, "REBEL_FACTION");
107    let region_count = plan.regions().iter().filter(|region| region.owner().is_none()).count();
108    let mut faction = layout.builder(&child_path(&path, "FACTION"))?;
109    faction.group()?.set_u32("FACTION_FLAG_AND_COLOURS@2+14", u32::try_from(region_count).unwrap_or(u32::MAX))?;
110
111    let mut rebels = layout.builder(&path)?;
112    rebels.group()?.set_record("FACTION", Some(faction.build()?))?;
113    rebels.build()
114}
115
116/// Writes the faction array.
117///
118/// # Errors
119///
120/// Returns an error if a table, column or layout is missing.
121pub fn write_faction_array(inputs: &WorldInputs, plan: &WorldPlan, context: &FactionContext) -> Result<RecordNode> {
122    let mut array = child(inputs.layout, WORLD_PATH, "FACTION_ARRAY")?;
123    for faction in plan.factions() {
124        array.push_group()?.set_record("FACTION", Some(write_faction(inputs, plan, context, faction)?))?;
125    }
126    array.build()
127}
128
129/// Plans the levy pools of the overlords: their ids continue the province pools, in order of the subject's and the
130/// province's runtime ids.
131///
132/// # Arguments
133///
134/// * `inputs` - Tables, startpos rows and layouts.
135/// * `plan` - Order and runtime ids of the world's objects.
136/// * `province_ids` - Runtime ids of the provinces, by key.
137/// * `first_pool` - Id of the first levy pool.
138///
139/// # Errors
140///
141/// Returns an error if a table or column is missing.
142pub fn plan_levy_pools(inputs: &WorldInputs, plan: &WorldPlan, province_ids: &HashMap<String, u32>, first_pool: u32) -> Result<Vec<PlannedLevyPool>> {
143    let mut pools = levy_pools(inputs.tables, inputs.index)?.into_iter()
144        .map(|pool| PlannedLevyPool {
145            id: 0,
146            subject: plan.faction_id(pool.subject()).unwrap_or_default(),
147            province: province_ids.get(pool.province()).copied().unwrap_or_default(),
148            overlord: pool.overlord().to_owned(),
149        })
150        .collect::<Vec<_>>();
151    pools.sort_by_key(|pool| (pool.subject, pool.province));
152
153    let mut next_pool = first_pool;
154    for pool in &mut pools {
155        pool.id = next_id(&mut next_pool);
156    }
157    Ok(pools)
158}
159
160/// Writes a faction.
161///
162/// # Errors
163///
164/// Returns an error if a table, column or layout is missing.
165pub fn write_faction(inputs: &WorldInputs, plan: &WorldPlan, context: &FactionContext, faction: &FactionEntry) -> Result<RecordNode> {
166    let layout = inputs.layout;
167    let key = faction.key();
168    let row = start_pos_faction_row(inputs.tables, faction)?;
169    let summary = faction_summary(inputs.tables, inputs.index, faction)?;
170    let faction_id = |key: &Option<String>| key.as_deref().and_then(|key| plan.faction_id(key)).unwrap_or_default();
171
172    let mut setup = child(layout, FACTION_PATH, "CAMPAIGN_PLAYER_SETUP")?;
173    setup.group()?
174        .set_str("CAMPAIGN_PLAYER_SETUP_INGAME_MODIFIABLES+1", key)?
175        .set_bool("CAMPAIGN_PLAYER_SETUP_INGAME_MODIFIABLES+3", faction.playable())?;
176
177    let mut economics = child(layout, FACTION_PATH, "FACTION_ECONOMICS")?;
178    economics.group()?.set_i32("history+1", i32::try_from(row.int("treasury")?).unwrap_or(i32::MAX))?;
179
180    let posts = write_character_posts(inputs, context.posts, key, context.characters.leader_id(faction))?;
181    let mut government = child(layout, FACTION_PATH, "GOVERNMENT")?;
182    government.group()?
183        .set_str("^+2", &row.str("government_type")?)?
184        .set_record("CHARACTER_POSTS", Some(posts))?;
185
186    let mut religion = child(layout, FACTION_PATH, "FACTION_RELIGION_MANAGER")?;
187    religion.group()?.set_str("^+1", &row.str("state_religion")?)?;
188
189    let mut localisation = child(layout, FACTION_PATH, "CAMPAIGN_LOCALISATION")?;
190    localisation.group()?.set_str("^+1", &format!("{SCREEN_NAME_PREFIX}{key}"))?.unset("^+2")?;
191
192    let [flag, republican_flag] = faction_flags(inputs.tables, key)?;
193    let honour = row.int("honour")?;
194    let memory = if honour >= EMPTY_MEMORY_HONOUR { vec![0; DEFAULT_MEMORY.len()] } else { DEFAULT_MEMORY.to_vec() };
195
196    let mut horde = None;
197    if let Some(is_horde) = summary.horde() {
198        let mut manager = child(layout, FACTION_PATH, "FACTION_HORDE_MANAGER")?;
199        manager.group()?.set_i32("^+1", i32::from(is_horde))?;
200        horde = Some(manager.build()?);
201    }
202
203    let mission_manager = faction.playable().then(|| layout.default_record(&format!("{FACTION_PATH}/CAMPAIGN_MISSION_MANAGER"))).transpose()?;
204    let shroud = if faction.playable() { Some(write_shroud(layout, FACTION_PATH, context.sight, &faction_vision(inputs, context, faction)?)?) } else { None };
205
206    let mut record = layout.builder(FACTION_PATH)?;
207    record.group()?
208        .set_u32("^+1", plan.faction_id(key).unwrap_or_default())?
209        .set_str("^+2", key)?
210        .set_record("CAMPAIGN_PLAYER_SETUP", Some(setup.build()?))?
211        .set_record("FACTION_ECONOMICS", Some(economics.build()?))?
212        .set_record("GOVERNMENT", Some(government.build()?))?
213        .set_record("FACTION_HORDE_MANAGER", horde)?
214        .set_bool("GOVERNMENT+3", row.bool("is_major")?)?
215        .set_record("FACTION_RELIGION_MANAGER", Some(religion.build()?))?
216        .set_record("CHARACTER_ARRAY", Some(context.characters.write_character_array(faction)?))?
217        .set_record("FACTION_TECHNOLOGY_MANAGER", Some(write_technologies(inputs, faction)?))?
218        .set_record("ARMY_ARRAY", Some(context.armies.write_army_array(faction)?))?
219        .set_bool("MORGUE+1", faction.playable())?
220        .set_strings("MORGUE+2", summary.victory_regions().to_vec())?
221        .set_u32("MORGUE+3", summary.capital().as_deref().and_then(|capital| plan.region_id(capital)).unwrap_or_default())?
222        .set_u32("MORGUE+4", summary.capital().as_deref().and_then(|capital| plan.region_id(capital)).unwrap_or_default())?
223        .set_bool("PRESTIGE+1", summary.without_characters())?
224        .set_str("PRESTIGE+6", &row.str("cdir_military_generator_config")?)?
225        .set_record("FACTION_FLAG_AND_COLOURS", Some(write_flag(inputs, FACTION_PATH, &flag)?))?
226        .set_record("FACTION_FLAG_AND_COLOURS@2", Some(write_flag(inputs, FACTION_PATH, &republican_flag)?))?
227        .set_u32("FACTION_FLAG_AND_COLOURS@2+1", faction_id(summary.client_state_of()))?
228        .set_u32("FACTION_FLAG_AND_COLOURS@2+2", faction_id(summary.vassal_of()))?
229        .set_bool("FACTION_FLAG_AND_COLOURS@2+5", summary.has_war_or_vassal())?
230        .set_u32("FACTION_FLAG_AND_COLOURS@2+14", u32::try_from(summary.region_count()).unwrap_or(u32::MAX))?
231        .set_bytes("FACTION_FLAG_AND_COLOURS@2+21", memory)?
232        .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
233        .set_strings("FACTION_ACTION_LOG_BLOCK+2", summary.subjects().to_vec())?
234        .set_record("FACTION_MERCENARY_POOL", Some(write_faction_mercenary_pool(inputs, plan, faction)?))?
235        .set_record("LEVY_MERCENARY_MANAGER", Some(write_levy_pools(inputs, context.levy_pools, faction)?))?
236        .set_record("CHARACTER_RECRUITMENT_POOL_MANAGER", Some(write_recruitment_pools(inputs, faction)?))?
237        .set_record("CAMPAIGN_POLITICS", Some(write_politics(inputs, faction)?))?
238        .set_record("OLD_DIPLOMACY_MANAGER", Some(write_diplomacy(inputs, plan, context.diplomacy, faction)?))?
239        .set_record("MILITARY_FORCE_LEGACY_NAMES_AND_EMBLEMS", Some(context.armies.write_used_legacies(faction)?))?
240        .set_record("CAMPAIGN_MISSION_MANAGER", mission_manager)?
241        .set_record("CAMPAIGN_SHROUD", shroud)?
242        .set_u32s("FACTION_FLAG_AND_COLOURS@2+19", vec![0; EVENT_COUNTERS])?
243        .set_u32("FACTION_ACTION_LOG_BLOCK+11", context.characters.progenitor_family_id(faction))?
244        .set_bool("FACTION_ACTION_LOG_BLOCK+12", *faction.culture() != NOMADIC_CULTURE)?;
245
246    record.build()
247}
248
249/// Returns what a faction sees: its regions, its settlements and its forces' commanders.
250fn faction_vision(inputs: &WorldInputs, context: &FactionContext, faction: &FactionEntry) -> Result<Vision> {
251    let mut vision = Vision::default();
252    for region in inputs.index.regions_of(faction.key()) {
253        for area in [context.sight.region(region.key()), context.sight.settlement(region.key())].into_iter().flatten() {
254            vision.extend(area);
255        }
256    }
257    for character in context.characters.characters_of(faction) {
258        if let Some(area) = context.characters.vision(character)? {
259            vision.extend(&area);
260        }
261    }
262    Ok(vision)
263}
264
265/// Writes the technologies a faction starts with (`start_pos_technologies`), in table order (the game uses a hash
266/// order). The game drops the ones its technology tree lacks, like generic technologies whose slot holds one of the
267/// faction's own.
268fn write_technologies(inputs: &WorldInputs, faction: &FactionEntry) -> Result<RecordNode> {
269    let path = format!("{FACTION_PATH}/FACTION_TECHNOLOGY_MANAGER");
270    let tree = faction_technologies(inputs.tables, inputs.index.campaign(), faction)?;
271    let mut researched = child(inputs.layout, &path, "RESEARCHED_TECHS")?;
272    for row in inputs.tables.table("start_pos_technologies")?.rows() {
273        let technology = row.str("technology")?;
274        if row.str("faction")? == *faction.id() && tree.iter().any(|known| *known == technology) {
275            researched.push_group()?
276                .set_str("^+1", &technology)?
277                .set_bool("^+2", true)?;
278        }
279    }
280
281    let mut record = inputs.layout.builder(&path)?;
282    record.group()?
283        .set_str("^+1", faction.key())?
284        .set_record("RESEARCHED_TECHS", Some(researched.build()?))?;
285    record.build()
286}
287
288/// Writes the mercenary pool of a faction. Its id is the faction's runtime id.
289fn write_faction_mercenary_pool(inputs: &WorldInputs, plan: &WorldPlan, faction: &FactionEntry) -> Result<RecordNode> {
290    let path = format!("{FACTION_PATH}/FACTION_MERCENARY_POOL");
291    let units = faction_mercenary_pool(inputs.tables, faction.key())?;
292    let mut record = inputs.layout.builder(&path)?;
293    record.group()?
294        .set_u32("^+1", plan.faction_id(faction.key()).unwrap_or_default())?
295        .set_record("MERCENARY_POOL", Some(write_mercenary_pool(inputs, &path, &units)?))?;
296    record.build()
297}
298
299/// Writes the levy pools of an overlord: an empty mercenary pool per subject and province.
300fn write_levy_pools(inputs: &WorldInputs, pools: &[PlannedLevyPool], faction: &FactionEntry) -> Result<RecordNode> {
301    let path = format!("{FACTION_PATH}/LEVY_MERCENARY_MANAGER");
302    let pool_path = format!("{path}/LEVY_POOLS_BLOCK/LEVY_MERCENARY_POOL");
303    let mut block = child(inputs.layout, &path, "LEVY_POOLS_BLOCK")?;
304    for pool in pools.iter().filter(|pool| pool.overlord == *faction.key()) {
305        let mut record = inputs.layout.builder(&pool_path)?;
306        record.group()?
307            .set_u32("^+1", pool.id)?
308            .set_record("MERCENARY_POOL", Some(write_mercenary_pool(inputs, &pool_path, &[])?))?
309            .set_u32("MERCENARY_POOL+1", pool.subject)?
310            .set_u32("MERCENARY_POOL+2", pool.province)?;
311        block.push_group()?.set_record("LEVY_MERCENARY_POOL", Some(record.build()?))?;
312    }
313
314    let mut manager = inputs.layout.builder(&path)?;
315    manager.group()?.set_record("LEVY_POOLS_BLOCK", Some(block.build()?))?;
316    manager.build()
317}
318
319/// Writes the character recruitment pools of a faction: an empty pool per agent type it can recruit.
320fn write_recruitment_pools(inputs: &WorldInputs, faction: &FactionEntry) -> Result<RecordNode> {
321    let path = format!("{FACTION_PATH}/CHARACTER_RECRUITMENT_POOL_MANAGER");
322    let mut block = child(inputs.layout, &path, "CHARACTER_RECRUITMENT_POOL_BLOCK")?;
323    for agent in recruitment_pool_agents(inputs.tables, faction)? {
324        block.push_group()?.set_str("^+1", &agent)?;
325    }
326
327    let mut manager = inputs.layout.builder(&path)?;
328    manager.group()?.set_record("CHARACTER_RECRUITMENT_POOL_BLOCK", Some(block.build()?))?;
329    manager.build()
330}
331
332/// Writes the diplomatic relationships of a faction with every other faction, in startpos order (the game uses a
333/// hash order).
334fn write_diplomacy(inputs: &WorldInputs, plan: &WorldPlan, diplomacy: &Diplomacy, faction: &FactionEntry) -> Result<RecordNode> {
335    let path = format!("{FACTION_PATH}/OLD_DIPLOMACY_MANAGER");
336    let relationship_path = format!("{path}/DIPLOMACY_RELATIONSHIPS_ARRAY/OLD_DIPLOMACY_RELATIONSHIP");
337    let mut array = child(inputs.layout, &path, "DIPLOMACY_RELATIONSHIPS_ARRAY")?;
338    for other in plan.factions().iter().filter(|other| other.key() != faction.key()) {
339        let relationship = diplomacy.relationship(faction.key(), other.key());
340        let other_id = plan.faction_id(other.key()).unwrap_or_default();
341
342        let mut allied = child(inputs.layout, &relationship_path, "ALLIED_IN_WAR_AGAINST")?;
343        for (enemy, defensive_ally) in relationship.allied_against() {
344            allied.push_group()?
345                .set_u32("^+1", plan.faction_id(enemy).unwrap_or_default())?
346                .set_i32("^+2", if *defensive_ally { -1 } else { 0 })?;
347        }
348
349        let (vassal_value, master_value) = relationship.vassal_values();
350        let mut record = inputs.layout.builder(&relationship_path)?;
351        record.group()?
352            .set_u32("^+1", other_id)?
353            .set_bool("^+2", relationship.trade())?
354            .set_i32("^+3", if relationship.allied_against().is_empty() { 0 } else { -1 })?
355            .set_str("^+4", relationship.stance())?
356            .set_i32("^+8", vassal_value)?
357            .set_i32("^+9", master_value)?
358            .set_record("ALLIED_IN_WAR_AGAINST", Some(allied.build()?))?
359            .set_bool("HOSTAGES+2", relationship.known())?
360            .set_bool("HOSTAGES+3", relationship.non_aggression())?
361            .set_i32("HOSTAGES+4", if relationship.military_access() { -1 } else { 0 })?;
362        array.push_group()?
363            .set_u32("^+1", other_id)?
364            .set_record("OLD_DIPLOMACY_RELATIONSHIP", Some(record.build()?))?;
365    }
366
367    let mut manager = inputs.layout.builder(&path)?;
368    manager.group()?.set_record("DIPLOMACY_RELATIONSHIPS_ARRAY", Some(array.build()?))?;
369    manager.build()
370}
371
372/// Writes the politics of a faction: its political parties with their starting power.
373fn write_politics(inputs: &WorldInputs, faction: &FactionEntry) -> Result<RecordNode> {
374    let path = format!("{FACTION_PATH}/CAMPAIGN_POLITICS");
375    let parties_path = format!("{path}/POLITICAL_PARTIES");
376    let parties = political_parties(inputs.tables, faction.key())?;
377    let mut array = child(inputs.layout, &path, "POLITICAL_PARTIES")?;
378    for party in &parties {
379        let mut record = child(inputs.layout, &parties_path, "CAMPAIGN_POLITICAL_PARTY")?;
380        record.group()?
381            .set_str("^+1", party.key())?
382            .set_u32("^+2", party.power())?;
383        array.push_group()?
384            .set_str("^+1", party.key())?
385            .set_record("CAMPAIGN_POLITICAL_PARTY", Some(record.build()?))?;
386    }
387
388    let mut politics = inputs.layout.builder(&path)?;
389    politics.group()?
390        .set_record("POLITICAL_PARTIES", Some(array.build()?))?
391        .set_bool("POLITICAL_PARTIES+1", !parties.is_empty())?;
392    politics.build()
393}
394
395/// Writes the flag record (`FACTION_FLAG_AND_COLOURS`) of a parent record: the flag path, then each colour's red,
396/// green and blue.
397pub(super) fn write_flag(inputs: &WorldInputs, parent: &str, flag: &FactionFlag) -> Result<RecordNode> {
398    let mut record = child(inputs.layout, parent, "FACTION_FLAG_AND_COLOURS")?;
399    let group = record.group()?;
400    group.set_str("^+1", flag.path())?;
401    for (index, value) in flag.colours().iter().flatten().enumerate() {
402        group.set_u32(&format!("^+{}", index + 2), u32::from(*value))?;
403    }
404    record.build()
405}