Skip to main content

rpfm_extensions/startpos/cai/
lists.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 smaller world lists: trade route segments, international trade routes, region boundaries,
12//! provinces and the factions' parts of them, the factions' views of the seas, governorships and government posts.
13
14use std::collections::HashMap;
15
16use rpfm_lib::error::Result;
17use rpfm_lib::files::esf::RecordNode;
18
19use super::factions::{owned_direct, REBELS_WORLD_ID};
20use super::map::MapRegion;
21use super::plan::CaiPlan;
22use super::world_view::WorldView;
23use super::{component, CAI_WORLD_PATH};
24use crate::startpos::layout::{child_path, StartposLayout};
25use crate::startpos::world::CampaignIndex;
26
27#[cfg(test)] #[path = "lists_test.rs"] mod lists_test;
28
29//---------------------------------------------------------------------------//
30//                              Implementations
31//---------------------------------------------------------------------------//
32
33/// Writes the trade route segments list: one per trade spline, holding its index.
34///
35/// # Arguments
36///
37/// * `layout` - Record layouts.
38/// * `plan` - Component ids.
39/// * `splines` - Number of trade splines of the map.
40///
41/// # Errors
42///
43/// Returns an error if the layout lacks the records.
44pub fn write_trade_segments(layout: &StartposLayout, plan: &CaiPlan, splines: usize) -> Result<RecordNode> {
45    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_TRADE_ROUTE_SEGMENTS");
46    let mut list = layout.builder(&path)?;
47    for index in 0..splines as u32 {
48        let mut record = layout.builder(&child_path(&path, "CAI_TRADE_ROUTE_SEGMENT"))?;
49        record.group()?.set_u32("^+1", index)?;
50        list.push_group()?
51            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, plan.first_trade_segment() + index, vec![])?))?
52            .set_record("CAI_TRADE_ROUTE_SEGMENT", Some(record.build()?))?;
53    }
54    list.build()
55}
56
57/// Writes the international trade routes list: one per route, in route id order, with its owner and the owner of the
58/// other route of its agreement (routes come in pairs, one per side: 1 and 2, 3 and 4...).
59///
60/// # Arguments
61///
62/// * `layout` - Record layouts.
63/// * `plan` - Component ids.
64/// * `world` - The world's objects.
65/// * `routes` - Each route's owner faction (runtime id) and route id.
66///
67/// # Errors
68///
69/// Returns an error if the layout lacks the records.
70pub fn write_trade_routes(layout: &StartposLayout, plan: &CaiPlan, world: &WorldView, routes: &[(u32, u32)]) -> Result<RecordNode> {
71    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_TRADE_ROUTES");
72    let mut components = world.factions().iter().zip(plan.factions()).map(|(faction, ids)| (faction.id(), ids.faction())).collect::<HashMap<_, _>>();
73    if let Some(rebels) = plan.factions().last() {
74        components.insert(REBELS_WORLD_ID, rebels.faction());
75    }
76    let owners = routes.iter().map(|(owner, route)| (*route, components.get(owner).copied().unwrap_or_default())).collect::<HashMap<_, _>>();
77    let mut ids = owners.keys().copied().collect::<Vec<_>>();
78    ids.sort_unstable();
79
80    let mut list = layout.builder(&path)?;
81    for (index, route) in ids.iter().enumerate() {
82        let partner = if route % 2 == 1 { route + 1 } else { route - 1 };
83        let mut record = layout.builder(&child_path(&path, "CAI_TRADE_ROUTE"))?;
84        record.group()?
85            .set_u32("^+2", *route)?
86            .set_u32("^+3", owners.get(&partner).copied().unwrap_or_default())?;
87        list.push_group()?
88            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, plan.first_trade_route() + index as u32, vec![])?))?
89            .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, owners[route])?))?
90            .set_record("CAI_TRADE_ROUTE", Some(record.build()?))?;
91    }
92    list.build()
93}
94
95/// Writes the region boundaries list.
96///
97/// # Errors
98///
99/// Returns an error if the layout lacks the records.
100pub fn write_boundaries(layout: &StartposLayout, plan: &CaiPlan) -> Result<RecordNode> {
101    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_REGION_BOUNDARIES");
102    let mut list = layout.builder(&path)?;
103    for boundary in plan.boundaries() {
104        let mut record = layout.builder(&child_path(&path, "CAI_REGION_BOUNDARY"))?;
105        record.group()?
106            .set_u32("^+1", plan.regions()[boundary.region()].id())?
107            .set_u32("^+2", plan.regions()[boundary.other()].id())?
108            .set_f32("^+3", boundary.value() as f32)?;
109        list.push_group()?
110            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, boundary.id(), vec![])?))?
111            .set_record("CAI_REGION_BOUNDARY", Some(record.build()?))?;
112    }
113    list.build()
114}
115
116/// Writes the provinces list and the list of the factions' parts of them.
117///
118/// # Arguments
119///
120/// * `layout` - Record layouts.
121/// * `plan` - Component ids.
122/// * `world` - The world's objects.
123/// * `map_regions` - Regions of the map, in map order.
124/// * `index` - The campaign's startpos rows, to find each province's major settlement.
125///
126/// # Returns
127///
128/// The provinces list and the factions' parts list.
129///
130/// # Errors
131///
132/// Returns an error if the layout lacks the records.
133pub fn write_provinces(layout: &StartposLayout, plan: &CaiPlan, world: &WorldView, map_regions: &[MapRegion], index: &CampaignIndex) -> Result<(RecordNode, RecordNode)> {
134    let lookups = Lookups::new(plan, world, map_regions);
135    let provinces_path = child_path(CAI_WORLD_PATH, "CAI_PROVINCES");
136    let parts_path = child_path(CAI_WORLD_PATH, "CAI_FACTION_PROVINCES");
137
138    let mut provinces = layout.builder(&provinces_path)?;
139    let mut parts = layout.builder(&parts_path)?;
140    for (province, ids) in world.provinces().iter().zip(plan.provinces()) {
141        for (part, id) in province.managers().iter().zip(ids.faction_provinces()) {
142
143            // The main region swaps places with the first one.
144            let mut regions = part.regions().iter().map(|region| lookups.region_by_id(*region)).collect::<Vec<_>>();
145            let main = lookups.region_by_id(part.main_region());
146            if let Some(position) = regions.iter().position(|region| *region == main) {
147                regions.swap(0, position);
148            }
149
150            let mut record = layout.builder(&child_path(&parts_path, "CAI_FACTION_PROVINCE"))?;
151            record.group()?
152                .set_u32("^+1", part.id())?
153                .set_u32("^+2", ids.province())?
154                .set_u32s("^+3", regions)?
155                .set_u32("^+4", lookups.region_by_id(part.main_region()))?;
156            parts.push_group()?
157                .set_record("OWNED_DIRECT", Some(owned_direct(layout, &parts_path, lookups.faction(part.faction()))?))?
158                .set_record("CAI_BDI_COMPONENT", Some(component(layout, &parts_path, *id, vec![])?))?
159                .set_record("CAI_FACTION_PROVINCE", Some(record.build()?))?;
160        }
161
162        // The province's main region is the one with its major settlement.
163        let major = province.regions().iter().find(|key| index.settlement_of(key).is_some_and(|settlement| settlement.is_major()));
164        let main_region = match major {
165            Some(key) => lookups.region_by_key(key),
166            None => province.managers().first().map(|part| lookups.region_by_id(part.main_region())).unwrap_or_default(),
167        };
168        let mut record = layout.builder(&child_path(&provinces_path, "CAI_PROVINCE"))?;
169        record.group()?
170            .set_u32("^+1", province.id())?
171            .set_u32("^+2", main_region)?
172            .set_u32s("^+3", province.regions().iter().map(|key| lookups.region_by_key(key)).collect())?
173            .set_u32s("^+4", ids.faction_provinces().to_vec())?;
174        provinces.push_group()?
175            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &provinces_path, ids.province(), vec![])?))?
176            .set_record("CAI_PROVINCE", Some(record.build()?))?;
177    }
178    Ok((provinces.build()?, parts.build()?))
179}
180
181/// Writes the list of the factions' views of the seas: each sea's views, sea by sea.
182///
183/// # Arguments
184///
185/// * `layout` - Record layouts.
186/// * `plan` - Component ids.
187/// * `world` - The world's objects.
188/// * `map_regions` - Regions of the map, in map order.
189///
190/// # Errors
191///
192/// Returns an error if the layout lacks the records.
193pub fn write_faction_seas(layout: &StartposLayout, plan: &CaiPlan, world: &WorldView, map_regions: &[MapRegion]) -> Result<RecordNode> {
194    let lookups = Lookups::new(plan, world, map_regions);
195    let path = child_path(CAI_WORLD_PATH, "CAI_FACTION_SEA_REGIONS");
196    let mut list = layout.builder(&path)?;
197    for (sea, ids) in world.seas().iter().zip(plan.faction_seas()) {
198        for (part, id) in sea.managers().iter().zip(ids) {
199            let mut record = layout.builder(&child_path(&path, "CAI_FACTION_SEA_REGION"))?;
200            record.group()?
201                .set_u32("^+1", lookups.region_by_key(sea.key()))?
202                .set_u32("^+2", part.id())?
203                .set_u32s("^+3", part.regions().iter().map(|region| lookups.region_by_id(*region)).collect())?;
204            list.push_group()?
205                .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, lookups.faction(part.faction()))?))?
206                .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, *id, vec![])?))?
207                .set_record("CAI_FACTION_SEA_REGION", Some(record.build()?))?;
208        }
209    }
210    list.build()
211}
212
213/// Writes the governorships list: one per world faction, over the theatre.
214///
215/// # Errors
216///
217/// Returns an error if the layout lacks the records.
218pub fn write_governorships(layout: &StartposLayout, plan: &CaiPlan) -> Result<RecordNode> {
219    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_GOVERNORSHIPS");
220    let mut list = layout.builder(&path)?;
221    for (id, faction) in plan.governorships().iter().zip(plan.factions()) {
222        let mut record = layout.builder(&child_path(&path, "CAI_GOVERNORSHIP"))?;
223        record.group()?.set_u32("^+1", plan.theatre())?;
224        list.push_group()?
225            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, *id, vec![])?))?
226            .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, faction.faction())?))?
227            .set_record("CAI_GOVERNORSHIP", Some(record.build()?))?;
228    }
229    list.build()
230}
231
232/// Writes the government posts list: each faction's posts, with their holders.
233///
234/// # Errors
235///
236/// Returns an error if the layout lacks the records.
237pub fn write_posts(layout: &StartposLayout, plan: &CaiPlan, world: &WorldView) -> Result<RecordNode> {
238    let path = child_path(CAI_WORLD_PATH, "CAI_CHARACTER_POSTS");
239    let mut list = layout.builder(&path)?;
240    for (position, faction) in world.factions().iter().enumerate() {
241        let characters = &plan.characters()[position];
242        for ((post, holder), id) in faction.posts().iter().zip(&plan.posts()[position]) {
243            let holder = faction.characters().iter().position(|character| *holder != 0 && character.id() == *holder).map(|index| characters[index].character()).unwrap_or_default();
244            let mut record = layout.builder(&child_path(&path, "CAI_CHARACTER_POST"))?;
245            record.group()?
246                .set_u32("^+1", *post)?
247                .set_u32("^+2", holder)?;
248            list.push_group()?
249                .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, plan.factions()[position].faction())?))?
250                .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, *id, vec![])?))?
251                .set_record("CAI_CHARACTER_POST", Some(record.build()?))?;
252        }
253    }
254    list.build()
255}
256
257/// Component lookups by world ids and keys.
258struct Lookups {
259
260    /// Component of each region, by key.
261    regions: HashMap<String, u32>,
262
263    /// Key of each world region, by runtime id.
264    region_keys: HashMap<u32, String>,
265
266    /// Component of each faction, by world id.
267    factions: HashMap<u32, u32>,
268}
269
270impl Lookups {
271
272    /// Builds the lookups of a world.
273    fn new(plan: &CaiPlan, world: &WorldView, map_regions: &[MapRegion]) -> Self {
274        let regions = map_regions.iter().zip(plan.regions()).map(|(region, ids)| (region.key().to_owned(), ids.id())).collect();
275        let region_keys = world.regions().iter().map(|region| (region.id(), region.key().to_owned())).collect();
276        let mut factions = world.factions().iter().zip(plan.factions()).map(|(faction, ids)| (faction.id(), ids.faction())).collect::<HashMap<_, _>>();
277        if let Some(rebels) = plan.factions().last() {
278            factions.insert(REBELS_WORLD_ID, rebels.faction());
279        }
280        Self { regions, region_keys, factions }
281    }
282
283    /// Returns the component of a region by key, or 0.
284    fn region_by_key(&self, key: &str) -> u32 {
285        self.regions.get(key).copied().unwrap_or_default()
286    }
287
288    /// Returns the component of a region by world id, or 0.
289    fn region_by_id(&self, id: u32) -> u32 {
290        self.region_keys.get(&id).map(|key| self.region_by_key(key)).unwrap_or_default()
291    }
292
293    /// Returns the component of a faction by world id, or 0.
294    fn faction(&self, id: u32) -> u32 {
295        self.factions.get(&id).copied().unwrap_or_default()
296    }
297}