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