Skip to main content

rpfm_extensions/startpos/cai/
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 AI's factions (`CAI_WORLD_FACTIONS`), technology trees (`CAI_WORLD_TECHNOLOGY_TREES`) and
12//! technologies (`CAI_WORLD_TECHNOLOGIES`).
13
14use std::collections::HashMap;
15
16use rpfm_lib::error::Result;
17use rpfm_lib::files::esf::RecordNode;
18
19use super::map::MapRegion;
20use super::plan::CaiPlan;
21use super::world_view::WorldView;
22use super::{component, CAI_WORLD_PATH};
23use crate::startpos::layout::{child_path, StartposLayout};
24use crate::startpos::tables::StartposTables;
25use crate::startpos::world::CampaignIndex;
26
27#[cfg(test)] #[path = "factions_test.rs"] mod factions_test;
28
29/// World id of the rebels.
30pub(crate) const REBELS_WORLD_ID: u32 = 1;
31
32/// Personality group of the rebels.
33const REBELS_PERSONALITY: &str = "default";
34
35//---------------------------------------------------------------------------//
36//                              Types
37//---------------------------------------------------------------------------//
38
39/// What an AI faction refers to.
40#[derive(Debug, Clone, Default)]
41pub struct FactionLinks {
42
43    /// World id of the faction.
44    world_id: u32,
45
46    /// Components of the regions it owns, in map order.
47    regions: Vec<u32>,
48
49    /// Connectivity groups it has a presence in.
50    groups: Vec<u32>,
51
52    /// Components of its mobiles, in creation order.
53    mobiles: Vec<u32>,
54
55    /// Components of its characters, in creation order.
56    characters: Vec<u32>,
57
58    /// Component of its capital region (0 for none).
59    capital: u32,
60
61    /// Component of its leader (0 for none).
62    leader: u32,
63
64    /// Components of its government posts.
65    posts: Vec<u32>,
66
67    /// Components of its parts of provinces.
68    provinces: Vec<u32>,
69
70    /// Components of its views of the seas.
71    seas: Vec<u32>,
72
73    /// Component of its governorship (0 for none).
74    governorship: u32,
75
76    /// Its personality group.
77    personality: String,
78
79    /// Its starting personality (empty for none).
80    starting_personality: String,
81
82    /// Components of the regions of its victory conditions.
83    victory_regions: Vec<u32>,
84
85    /// Its horde's mobiles, the first leading (empty for factions with regions).
86    horde: Vec<u32>,
87}
88
89//---------------------------------------------------------------------------//
90//                              Implementations
91//---------------------------------------------------------------------------//
92
93/// Returns the component of the faction whose capital each map region is, by map region index.
94///
95/// # Arguments
96///
97/// * `index` - The campaign's startpos rows.
98/// * `world` - The world's objects.
99/// * `plan` - Component ids.
100/// * `map_regions` - Regions of the map, in map order.
101pub fn capitals(index: &CampaignIndex, world: &WorldView, plan: &CaiPlan, map_regions: &[MapRegion]) -> HashMap<usize, u32> {
102    world.factions().iter().zip(plan.factions())
103        .filter_map(|(faction, ids)| {
104            let capital = index.capital_of(faction.key())?;
105            let region = map_regions.iter().position(|region| region.key() == capital.key())?;
106            Some((region, ids.faction()))
107        })
108        .collect()
109}
110
111/// Returns the components of the factions with victory conditions on each region, by region key, in faction order.
112///
113/// # Arguments
114///
115/// * `tables` - Tables with `start_pos_victory_conditions`.
116/// * `index` - The campaign's startpos rows.
117/// * `world` - The world's objects.
118/// * `plan` - Component ids.
119///
120/// # Errors
121///
122/// Returns an error if a table or column is missing.
123pub fn victory_factions(tables: &StartposTables, index: &CampaignIndex, world: &WorldView, plan: &CaiPlan) -> Result<HashMap<String, Vec<u32>>> {
124    let components = world.factions().iter().zip(plan.factions()).map(|(faction, ids)| (faction.key().as_str(), ids.faction())).collect::<HashMap<_, _>>();
125    let mut factions: HashMap<String, Vec<u32>> = HashMap::new();
126    for row in tables.table("start_pos_victory_conditions")?.rows() {
127        let Some(component) = index.faction_by_id(&row.str("start_pos_faction")?).and_then(|faction| components.get(faction.key().as_str())) else { continue };
128        let list = factions.entry(row.str("region")?.into_owned()).or_default();
129        if !list.contains(component) {
130            list.push(*component);
131        }
132    }
133    for list in factions.values_mut() {
134        list.sort_unstable();
135    }
136    Ok(factions)
137}
138
139/// Returns what each AI faction refers to: the world factions, then the rebels.
140///
141/// # Arguments
142///
143/// * `tables` - Tables with `start_pos_factions` and `start_pos_victory_conditions`.
144/// * `index` - The campaign's startpos rows.
145/// * `world` - The world's objects.
146/// * `plan` - Component ids.
147/// * `map_regions` - Regions of the map, in map order.
148///
149/// # Errors
150///
151/// Returns an error if a table or column is missing.
152pub fn faction_links(tables: &StartposTables, index: &CampaignIndex, world: &WorldView, plan: &CaiPlan, map_regions: &[MapRegion]) -> Result<Vec<FactionLinks>> {
153    let region_index = map_regions.iter().enumerate().map(|(position, region)| (region.key().as_str(), position)).collect::<HashMap<_, _>>();
154    let region_component = |key: &str| region_index.get(key).map(|position| plan.regions()[*position].id());
155    let mut personalities = HashMap::new();
156    for row in tables.table("start_pos_factions")?.rows() {
157        if let Some(faction) = index.faction_by_id(&row.str("ID")?) {
158            personalities.insert(faction.key().to_owned(), (row.str("cai_personality_group")?.into_owned(), row.str("cai_starting_personality")?.into_owned()));
159        }
160    }
161    let mut victory_regions: HashMap<String, Vec<u32>> = HashMap::new();
162    for row in tables.table("start_pos_victory_conditions")?.rows() {
163        if let (Some(faction), Some(region)) = (index.faction_by_id(&row.str("start_pos_faction")?), region_component(&row.str("region")?)) {
164            victory_regions.entry(faction.key().to_owned()).or_default().push(region);
165        }
166    }
167
168    // Regions and the parts of provinces and seas are listed by owner's world id.
169    let owned_regions = |owner: u32| -> Vec<u32> {
170        let mut regions = world.regions().iter().filter(|region| region.owner() == owner).filter_map(|region| region_index.get(region.key().as_str()).copied()).collect::<Vec<_>>();
171        regions.sort_unstable();
172        regions.into_iter().map(|position| plan.regions()[position].id()).collect()
173    };
174    let parts = |owner: u32| -> (Vec<u32>, Vec<u32>) {
175        let provinces = world.provinces().iter().zip(plan.provinces())
176            .flat_map(|(province, ids)| province.managers().iter().zip(ids.faction_provinces()))
177            .filter(|(part, _)| part.faction() == owner)
178            .map(|(_, id)| *id)
179            .collect();
180        let seas = world.seas().iter().zip(plan.faction_seas())
181            .flat_map(|(sea, ids)| sea.managers().iter().zip(ids))
182            .filter(|(part, _)| part.faction() == owner)
183            .map(|(_, id)| *id)
184            .collect();
185        (provinces, seas)
186    };
187
188    let mut links = vec![];
189    for (position, faction) in world.factions().iter().enumerate() {
190        let characters = &plan.characters()[position];
191        let regions = owned_regions(faction.id());
192        let mobiles = characters.iter().filter_map(|character| character.mobile()).collect::<Vec<_>>();
193        let leader_id = faction.posts().iter().map(|(_, holder)| *holder).find(|holder| *holder != 0);
194        let leader = leader_id.and_then(|leader| faction.characters().iter().position(|character| character.id() == leader)).map(|position| characters[position].character()).unwrap_or_default();
195        let (provinces, seas) = parts(faction.id());
196        let (personality, starting_personality) = personalities.get(faction.key()).cloned().unwrap_or_default();
197        links.push(FactionLinks {
198            world_id: faction.id(),
199            groups: if regions.is_empty() { vec![] } else { vec![0] },
200            horde: if regions.is_empty() { mobiles.to_vec() } else { vec![] },
201            regions,
202            characters: characters.iter().map(|character| character.character()).collect(),
203            mobiles,
204            capital: index.capital_of(faction.key()).and_then(|capital| region_component(capital.key())).unwrap_or_default(),
205            leader,
206            posts: plan.posts()[position].to_vec(),
207            provinces,
208            seas,
209            governorship: plan.governorships()[position],
210            personality,
211            starting_personality,
212            victory_regions: victory_regions.remove(faction.key()).unwrap_or_default(),
213        });
214    }
215
216    let regions = owned_regions(REBELS_WORLD_ID);
217    let (provinces, seas) = parts(REBELS_WORLD_ID);
218    links.push(FactionLinks {
219        world_id: REBELS_WORLD_ID,
220        groups: if regions.is_empty() { vec![] } else { vec![0] },
221        regions,
222        provinces,
223        seas,
224        personality: REBELS_PERSONALITY.to_owned(),
225        ..Default::default()
226    });
227    Ok(links)
228}
229
230/// Writes the factions list: every world faction, then the rebels.
231///
232/// # Arguments
233///
234/// * `layout` - Record layouts.
235/// * `plan` - Component ids.
236/// * `links` - What each faction refers to, in plan order.
237///
238/// # Errors
239///
240/// Returns an error if the layout lacks the records.
241pub fn write_factions(layout: &StartposLayout, plan: &CaiPlan, links: &[FactionLinks]) -> Result<RecordNode> {
242    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_FACTIONS");
243    let faction_path = child_path(&path, "CAI_FACTION");
244
245    let mut factions = layout.builder(&path)?;
246    for (position, (ids, links)) in plan.factions().iter().zip(links).enumerate() {
247        let mut personality = layout.builder(&child_path(&faction_path, "CAI_FACTION_PERSONALITY"))?;
248        personality.group()?
249            .set_u32("^+1", ids.faction())?
250            .set_str("^+2", &links.starting_personality)?
251            .set_str("^+3", &links.personality)?;
252
253        let mut record = layout.builder(&faction_path)?;
254        record.group()?
255            .set_u32s("^+1", links.regions.to_vec())?
256            .set_u32s("^+2", if links.regions.is_empty() { vec![] } else { vec![plan.theatre()] })?
257            .set_u32s("^+3", links.groups.to_vec())?
258            .set_u32s("^+4", links.mobiles.to_vec())?
259            .set_u32s("^+5", links.characters.to_vec())?
260            .set_u32("^+6", links.capital)?
261            .set_u32("^+7", links.world_id)?
262            .set_u32s("^+8", vec![])?
263            .set_u32("^+9", ids.tree().unwrap_or_default())?
264            .set_u32s("^+10", if links.governorship == 0 { vec![] } else { vec![links.governorship] })?
265            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &faction_path, ids.cai_faction(), vec![])?))?
266            .set_u32("CAI_BDI_NEW_TURN+1", links.capital)?
267            .set_u32s("CAI_BDI_NEW_TURN+2", links.victory_regions.to_vec())?
268            .set_u32("CAI_BDI_NEW_TURN+3", links.leader)?
269            .set_u32s("CAI_BDI_NEW_TURN+4", links.posts.to_vec())?
270            .set_u32s("CAI_BDI_NEW_TURN+5", links.provinces.to_vec())?
271            .set_record("CAI_FACTION_PERSONALITY", Some(personality.build()?))?
272            .set_u32s("CAI_FACTION_PERSONALITY+1", links.seas.to_vec())?
273            .set_u32("CAI_FACTION_PERSONALITY+2", (position + 1) as u32)?
274            .set_record("CAI_FACTION_HORDE_MOBILE_GROUPS", Some(write_horde_groups(layout, &faction_path, &links.horde)?))?;
275
276        factions.push_group()?
277            .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, ids.faction())?))?
278            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, ids.faction(), vec![])?))?
279            .set_record("CAI_FACTION", Some(record.build()?))?;
280    }
281    factions.build()
282}
283
284/// Writes the technology trees list, one per faction with a tree.
285///
286/// # Errors
287///
288/// Returns an error if the layout lacks the records.
289pub fn write_technology_trees(layout: &StartposLayout, plan: &CaiPlan) -> Result<RecordNode> {
290    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_TECHNOLOGY_TREES");
291    let mut trees = layout.builder(&path)?;
292    for ids in plan.factions() {
293        let Some(tree_id) = ids.tree() else { continue };
294        let mut tree = layout.builder(&child_path(&path, "CAI_TECHNOLOGY_TREE"))?;
295        tree.group()?.set_u32s("^+1", ids.technologies().to_vec())?;
296
297        trees.push_group()?
298            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, tree_id, vec![])?))?
299            .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, ids.faction())?))?
300            .set_record("CAI_TECHNOLOGY_TREE", Some(tree.build()?))?;
301    }
302    trees.build()
303}
304
305/// Writes the technologies list: each faction's technologies, faction by faction.
306///
307/// # Arguments
308///
309/// * `layout` - Record layouts.
310/// * `plan` - Component ids.
311/// * `technologies` - Technology keys of each world faction's tree, in plan order.
312///
313/// # Errors
314///
315/// Returns an error if the layout lacks the records.
316pub fn write_technologies(layout: &StartposLayout, plan: &CaiPlan, technologies: &[Vec<String>]) -> Result<RecordNode> {
317    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_TECHNOLOGIES");
318    let mut list = layout.builder(&path)?;
319    for (ids, keys) in plan.factions().iter().zip(technologies) {
320        let tree_id = ids.tree().unwrap_or_default();
321        for (id, key) in ids.technologies().iter().zip(keys) {
322            let mut technology = layout.builder(&child_path(&path, "CAI_TECHNOLOGY"))?;
323            technology.group()?
324                .set_u32("^+1", tree_id)?
325                .set_str("^+2", key)?;
326
327            list.push_group()?
328                .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, *id, vec![])?))?
329                .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, ids.faction())?))?
330                .set_record("CAI_TECHNOLOGY", Some(technology.build()?))?;
331        }
332    }
333    list.build()
334}
335
336/// Writes the horde groups of a faction: none, or one with its mobiles led by the first.
337fn write_horde_groups(layout: &StartposLayout, faction_path: &str, horde: &[u32]) -> Result<RecordNode> {
338    let path = child_path(faction_path, "CAI_FACTION_HORDE_MOBILE_GROUPS");
339    let mut groups = layout.builder(&path)?;
340    groups.group()?.set_u32("^+1", u32::from(!horde.is_empty()))?;
341    if let Some(leader) = horde.first() {
342        let mut group = layout.builder(&child_path(&path, "CAI_FACTION_HORDE_MOBILE_GROUP"))?;
343        group.group()?
344            .set_u32s("^+1", horde.to_vec())?
345            .set_u32("^+2", *leader)?;
346        groups.group()?.set_record("CAI_FACTION_HORDE_MOBILE_GROUP", Some(group.build()?))?;
347    }
348    groups.build()
349}
350
351/// Writes the owner record (`OWNED_DIRECT`) of an object of a list.
352pub(crate) fn owned_direct(layout: &StartposLayout, list_path: &str, owner: u32) -> Result<RecordNode> {
353    let mut record = layout.builder(&child_path(list_path, "OWNED_DIRECT"))?;
354    record.group()?.set_u32("^+1", owner)?;
355    record.build()
356}