Skip to main content

rpfm_extensions/startpos/cai/
world_view.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//! What the campaign AI mirrors of a written `WORLD` record: its factions, characters, forces, regions, provinces and
12//! seas, with their runtime ids, read through the record layouts.
13
14use getset::{CopyGetters, Getters};
15
16use rpfm_lib::error::{RLibError, Result};
17use rpfm_lib::files::esf::RecordNode;
18
19use crate::startpos::layout::{child_path, GroupView, StartposLayout};
20
21#[cfg(test)] #[path = "world_view_test.rs"] mod world_view_test;
22
23/// Path of the world.
24const WORLD_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD";
25
26/// Path of the trade manager.
27const TRADE_MANAGER_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/CAMPAIGN_TRADE_MANAGER";
28
29//---------------------------------------------------------------------------//
30//                              Types
31//---------------------------------------------------------------------------//
32
33/// The objects of a world, in world order.
34#[derive(Debug, Clone, Default, Getters)]
35#[getset(get = "pub")]
36pub struct WorldView {
37
38    /// Factions, in `FACTION_ARRAY` order.
39    factions: Vec<FactionView>,
40
41    /// Regions, in `REGIONS_ARRAY` order.
42    regions: Vec<RegionView>,
43
44    /// Provinces, in `PROVINCE_ARRAY` order.
45    provinces: Vec<ProvinceView>,
46
47    /// Sea regions, in `SEA_REGIONS_BLOCK` order.
48    seas: Vec<SeaView>,
49}
50
51/// A faction of the world.
52#[derive(Debug, Clone, Default, Getters, CopyGetters)]
53pub struct FactionView {
54
55    /// Runtime id.
56    #[getset(get_copy = "pub")]
57    id: u32,
58
59    /// Faction key.
60    #[getset(get = "pub")]
61    key: String,
62
63    /// Characters, in `CHARACTER_ARRAY` order.
64    #[getset(get = "pub")]
65    characters: Vec<CharacterView>,
66
67    /// Forces, in `ARMY_ARRAY` order.
68    #[getset(get = "pub")]
69    forces: Vec<ForceView>,
70
71    /// Government posts, as (runtime id, holder's character id or 0) pairs, in government order.
72    #[getset(get = "pub")]
73    posts: Vec<(u32, u32)>,
74}
75
76/// A character of the world.
77#[derive(Debug, Clone, Default, Getters, CopyGetters)]
78pub struct CharacterView {
79
80    /// Runtime id.
81    #[getset(get_copy = "pub")]
82    id: u32,
83
84    /// Agent type (`general`, `colonel`, `spy`...).
85    #[getset(get = "pub")]
86    agent: String,
87
88    /// Cell the character stands on.
89    #[getset(get_copy = "pub")]
90    cell: (u16, u16),
91
92    /// Runtime id of the force the character commands, 0 for none.
93    #[getset(get_copy = "pub")]
94    force: u32,
95
96    /// Runtime id of the character's own unit (bodyguard or flagship), 0 for none.
97    #[getset(get_copy = "pub")]
98    unit: u32,
99}
100
101/// A force (army, navy or garrison) of the world.
102#[derive(Debug, Clone, Default, Getters, CopyGetters)]
103pub struct ForceView {
104
105    /// Runtime id.
106    #[getset(get_copy = "pub")]
107    id: u32,
108
109    /// Runtime id of the commander.
110    #[getset(get_copy = "pub")]
111    commander: u32,
112
113    /// Runtime ids of the units.
114    #[getset(get = "pub")]
115    units: Vec<u32>,
116
117    /// Garrison residence of the settlement the force is stationed in, 0 for none.
118    #[getset(get_copy = "pub")]
119    residence: u32,
120
121    /// Garrison residence the force defends as its garrison army, 0 for none.
122    #[getset(get_copy = "pub")]
123    garrison_army_of: u32,
124
125    /// Garrison residence the force defends as its garrison navy, 0 for none.
126    #[getset(get_copy = "pub")]
127    garrison_navy_of: u32,
128
129    /// The force is a navy.
130    #[getset(get_copy = "pub")]
131    navy: bool,
132
133    /// Runtime ids of a horde army's building slots.
134    #[getset(get = "pub")]
135    horde_slots: Vec<u32>,
136}
137
138/// A region of the world.
139#[derive(Debug, Clone, Default, Getters, CopyGetters)]
140pub struct RegionView {
141
142    /// Runtime id.
143    #[getset(get_copy = "pub")]
144    id: u32,
145
146    /// Region key.
147    #[getset(get = "pub")]
148    key: String,
149
150    /// Runtime id of the owner faction.
151    #[getset(get_copy = "pub")]
152    owner: u32,
153
154    /// Garrison residence of the settlement.
155    #[getset(get_copy = "pub")]
156    residence: u32,
157
158    /// Runtime ids of the settlement's slots.
159    #[getset(get = "pub")]
160    slots: Vec<u32>,
161}
162
163/// A province of the world.
164#[derive(Debug, Clone, Default, Getters, CopyGetters)]
165pub struct ProvinceView {
166
167    /// Runtime id.
168    #[getset(get_copy = "pub")]
169    id: u32,
170
171    /// Province key.
172    #[getset(get = "pub")]
173    key: String,
174
175    /// Keys of the province's regions.
176    #[getset(get = "pub")]
177    regions: Vec<String>,
178
179    /// Each faction's part of the province.
180    #[getset(get = "pub")]
181    managers: Vec<FactionPartView>,
182}
183
184/// A sea region of the world.
185#[derive(Debug, Clone, Default, Getters, CopyGetters)]
186pub struct SeaView {
187
188    /// Runtime id.
189    #[getset(get_copy = "pub")]
190    id: u32,
191
192    /// Sea region key.
193    #[getset(get = "pub")]
194    key: String,
195
196    /// Each faction's view of the sea.
197    #[getset(get = "pub")]
198    managers: Vec<FactionPartView>,
199}
200
201/// A faction's part of a province or sea (`FACTION_PROVINCE_MANAGER`, `FACTION_SEA_REGION_MANAGER`).
202#[derive(Debug, Clone, Default, Getters, CopyGetters)]
203pub struct FactionPartView {
204
205    /// Runtime id.
206    #[getset(get_copy = "pub")]
207    id: u32,
208
209    /// Runtime id of the faction.
210    #[getset(get_copy = "pub")]
211    faction: u32,
212
213    /// Runtime ids of the faction's regions in it.
214    #[getset(get = "pub")]
215    regions: Vec<u32>,
216
217    /// Runtime id of the main region of a faction's part of a province (0 for seas).
218    #[getset(get_copy = "pub")]
219    main_region: u32,
220}
221
222//---------------------------------------------------------------------------//
223//                              Implementations
224//---------------------------------------------------------------------------//
225
226impl WorldView {
227
228    /// Reads the objects of a written world.
229    ///
230    /// # Arguments
231    ///
232    /// * `layout` - Record layouts the world was written with.
233    /// * `world` - The `WORLD` record.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if the world doesn't fit its layouts.
238    pub fn new(layout: &StartposLayout, world: &RecordNode) -> Result<Self> {
239        let reader = Reader { layout };
240        let world_group = layout.read_single(WORLD_PATH, world)?;
241
242        let faction_array = child_path(WORLD_PATH, "FACTION_ARRAY");
243        let factions = layout.read_items(&faction_array, &world_group, "FACTION_ARRAY", "FACTION")?.into_iter()
244            .map(|(_, faction)| reader.faction(&child_path(&faction_array, "FACTION"), &faction))
245            .collect::<Result<Vec<_>>>()?;
246
247        let manager_path = child_path(WORLD_PATH, "REGION_MANAGER");
248        let manager = layout.read_single(&manager_path, reader.record(&world_group, "REGION_MANAGER")?)?;
249        let region_array = child_path(&manager_path, "REGIONS_ARRAY");
250        let regions = layout.read_items(&region_array, &manager, "REGIONS_ARRAY", "REGION")?.into_iter()
251            .map(|(_, region)| reader.region(&child_path(&region_array, "REGION"), &region))
252            .collect::<Result<Vec<_>>>()?;
253
254        let manager_path = child_path(WORLD_PATH, "PROVINCE_MANAGER");
255        let manager = layout.read_single(&manager_path, reader.record(&world_group, "PROVINCE_MANAGER")?)?;
256        let province_array = child_path(&manager_path, "PROVINCE_ARRAY");
257        let province_path = child_path(&province_array, "PROVINCE");
258        let provinces = layout.read_items(&province_array, &manager, "PROVINCE_ARRAY", "PROVINCE")?.into_iter()
259            .map(|(entry, province)| Ok(ProvinceView {
260                id: province.u32("^+1").unwrap_or_default(),
261                key: entry.str("^+1").unwrap_or_default().to_owned(),
262                regions: reader.province_regions(&province_path, &province)?,
263                managers: reader.parts(&province_path, &province, "FACTION_PROVINCE_MANAGER_ARRAY", "FACTION_PROVINCE_MANAGER")?,
264            }))
265            .collect::<Result<Vec<_>>>()?;
266
267        let manager_path = child_path(WORLD_PATH, "SEA_REGION_MANAGER");
268        let manager = layout.read_single(&manager_path, reader.record(&world_group, "SEA_REGION_MANAGER")?)?;
269        let sea_block = child_path(&manager_path, "SEA_REGIONS_BLOCK");
270        let sea_path = child_path(&sea_block, "SEA_REGION");
271        let seas = layout.read_items(&sea_block, &manager, "SEA_REGIONS_BLOCK", "SEA_REGION")?.into_iter()
272            .map(|(entry, sea)| Ok(SeaView {
273                id: sea.u32("^+1").unwrap_or_default(),
274                key: entry.str("^+1").unwrap_or_default().to_owned(),
275                managers: reader.parts(&sea_path, &sea, "FACTION_SEA_REGION_MANAGER_BLOCK", "FACTION_SEA_REGION_MANAGER")?,
276            }))
277            .collect::<Result<Vec<_>>>()?;
278
279        Ok(Self { factions, regions, provinces, seas })
280    }
281}
282
283/// Reads the international trade routes of a written `CAMPAIGN_TRADE_MANAGER`.
284///
285/// # Arguments
286///
287/// * `layout` - Record layouts.
288/// * `trade_manager` - The written trade manager.
289///
290/// # Returns
291///
292/// Each route's owner faction (runtime id) and route id, in manager order.
293///
294/// # Errors
295///
296/// Returns an error if the record doesn't fit its layout.
297pub fn international_routes(layout: &StartposLayout, trade_manager: &RecordNode) -> Result<Vec<(u32, u32)>> {
298    let manager = layout.read_single(TRADE_MANAGER_PATH, trade_manager)?;
299    let Some(routes) = manager.record("INTERNATIONAL_TRADE_ROUTES") else { return Ok(vec![]) };
300
301    let routes_path = child_path(TRADE_MANAGER_PATH, "INTERNATIONAL_TRADE_ROUTES");
302    let faction_routes_path = child_path(&routes_path, "FACTION_INTERNATIONAL_TRADE_ROUTES_ARRAY");
303    let mut list = vec![];
304    for faction in layout.read(&routes_path, routes)? {
305        let owner = faction.u32("^+1").unwrap_or_default();
306        for (_, route) in layout.read_items(&faction_routes_path, &faction, "FACTION_INTERNATIONAL_TRADE_ROUTES_ARRAY", "INTERNATIONAL_TRADE_ROUTE")? {
307            list.push((owner, route.u32("^+1").unwrap_or_default()));
308        }
309    }
310    Ok(list)
311}
312
313/// Reads records through their layouts.
314struct Reader<'l> {
315    layout: &'l StartposLayout,
316}
317
318impl<'l> Reader<'l> {
319
320    /// Returns a child record of a group.
321    fn record<'a>(&self, group: &GroupView<'a>, key: &str) -> Result<&'a RecordNode> {
322        group.record(key).ok_or_else(|| world_error(&format!("missing {key}")))
323    }
324
325    /// Reads a faction.
326    fn faction(&self, path: &str, faction: &GroupView) -> Result<FactionView> {
327        let character_array = child_path(path, "CHARACTER_ARRAY");
328        let character_path = child_path(&character_array, "CHARACTER");
329        let characters = self.layout.read_items(&character_array, faction, "CHARACTER_ARRAY", "CHARACTER")?.into_iter()
330            .map(|(_, character)| {
331                let cell = match character.record("LOCOMOTABLE") {
332                    Some(record) => {
333                        let locomotable = self.layout.read_single(&child_path(&character_path, "LOCOMOTABLE"), record)?;
334                        (locomotable.u32("^+2").unwrap_or_default() as u16, locomotable.u32("^+3").unwrap_or_default() as u16)
335                    },
336                    None => (0, 0),
337                };
338                Ok(CharacterView {
339                    id: character.u32("LOCOMOTABLE+1").unwrap_or_default(),
340                    agent: character.str("CHARACTER_DETAILS+1").unwrap_or_default().to_owned(),
341                    cell,
342                    force: character.u32("CHARACTER_DETAILS+2").unwrap_or_default(),
343                    unit: character.u32("CHARACTER_DETAILS+3").unwrap_or_default(),
344                })
345            })
346            .collect::<Result<Vec<_>>>()?;
347
348        let army_array = child_path(path, "ARMY_ARRAY");
349        let force_path = child_path(&army_array, "MILITARY_FORCE");
350        let forces = self.layout.read_items(&army_array, faction, "ARMY_ARRAY", "MILITARY_FORCE")?.into_iter()
351            .map(|(_, force)| self.force(&force_path, &force))
352            .collect::<Result<Vec<_>>>()?;
353
354        let government_path = child_path(path, "GOVERNMENT");
355        let mut posts = vec![];
356        if let Some(government) = faction.record("GOVERNMENT") {
357            let government = self.layout.read_single(&government_path, government)?;
358            let posts_path = child_path(&government_path, "CHARACTER_POSTS");
359            let post_path = child_path(&posts_path, "CHARACTER_POST");
360            if let Some(array) = government.record("CHARACTER_POSTS") {
361                for entry in self.layout.read(&posts_path, array)? {
362                    for index in 1.. {
363                        let key = if index == 1 { "CHARACTER_POST".to_owned() } else { format!("CHARACTER_POST@{index}") };
364                        let Some(post) = entry.record(&key) else { break };
365                        let post = self.layout.read_single(&post_path, post)?;
366                        posts.push((post.u32("^+1").unwrap_or_default(), post.u32("^+3").unwrap_or_default()));
367                    }
368                }
369            }
370        }
371
372        Ok(FactionView {
373            id: faction.u32("^+1").unwrap_or_default(),
374            key: faction.str("^+2").unwrap_or_default().to_owned(),
375            characters,
376            forces,
377            posts,
378        })
379    }
380
381    /// Reads a force.
382    fn force(&self, path: &str, force: &GroupView) -> Result<ForceView> {
383        let container_path = child_path(path, "UNIT_CONTAINER");
384        let units_path = child_path(&container_path, "UNITS_ARRAY");
385        let mut units = vec![];
386        if let Some(container) = force.record("UNIT_CONTAINER") {
387            let container = self.layout.read_single(&container_path, container)?;
388            for (_, unit) in self.layout.read_items(&units_path, &container, "UNITS_ARRAY", "UNIT")? {
389                units.push(unit.u32("^+1").unwrap_or_default());
390            }
391        }
392
393        let horde_path = child_path(path, "MILITARY_FORCE_HORDE_DETAILS");
394        let slot_manager_path = child_path(&horde_path, "MILITARY_FORCE_SLOT_MANAGER");
395        let slot_block_path = child_path(&slot_manager_path, "SLOT_BLOCK");
396        let mut horde_slots = vec![];
397        if let Some(horde) = force.record("MILITARY_FORCE_HORDE_DETAILS") {
398            let horde = self.layout.read_single(&horde_path, horde)?;
399            if let Some(manager) = horde.record("MILITARY_FORCE_SLOT_MANAGER") {
400                let manager = self.layout.read_single(&slot_manager_path, manager)?;
401                for (_, slot) in self.layout.read_items(&slot_block_path, &manager, "SLOT_BLOCK", "MILITARY_FORCE_SLOT")? {
402                    horde_slots.push(slot.u32("^+1").unwrap_or_default());
403                }
404            }
405        }
406
407        Ok(ForceView {
408            id: force.u32("^+1").unwrap_or_default(),
409            commander: force.u32("^+2").unwrap_or_default(),
410            units,
411            residence: force.u32("UNIT_CONTAINER+1").unwrap_or_default(),
412            garrison_army_of: force.u32("UNIT_CONTAINER+11").unwrap_or_default(),
413            garrison_navy_of: force.u32("UNIT_CONTAINER+12").unwrap_or_default(),
414            navy: force.bool("UNIT_CONTAINER+13").unwrap_or_default(),
415            horde_slots,
416        })
417    }
418
419    /// Reads a region.
420    fn region(&self, path: &str, region: &GroupView) -> Result<RegionView> {
421        let settlement_path = child_path(path, "SETTLEMENT");
422        let mut residence = 0;
423        if let Some(settlement) = region.record("SETTLEMENT") {
424            let settlement = self.layout.read_single(&settlement_path, settlement)?;
425            if let Some(record) = settlement.record("GARRISON_RESIDENCE") {
426                residence = self.layout.read_single(&child_path(&settlement_path, "GARRISON_RESIDENCE"), record)?.u32("^+1").unwrap_or_default();
427            }
428        }
429
430        let slot_manager_path = child_path(path, "REGION_SLOT_MANAGER");
431        let slot_array_path = child_path(&slot_manager_path, "REGION_SLOT_ARRAY");
432        let mut slots = vec![];
433        if let Some(manager) = region.record("REGION_SLOT_MANAGER") {
434            let manager = self.layout.read_single(&slot_manager_path, manager)?;
435            for (_, slot) in self.layout.read_items(&slot_array_path, &manager, "REGION_SLOT_ARRAY", "REGION_SLOT")? {
436                slots.push(slot.u32("^+1").unwrap_or_default());
437            }
438        }
439
440        Ok(RegionView {
441            id: region.u32("^+1").unwrap_or_default(),
442            key: region.str("^+2").unwrap_or_default().to_owned(),
443            owner: region.u32("SETTLEMENT+9").unwrap_or_default(),
444            residence,
445            slots,
446        })
447    }
448
449    /// Reads the keys of a province's regions (its fertility's region list).
450    fn province_regions(&self, path: &str, province: &GroupView) -> Result<Vec<String>> {
451        let fertility_path = child_path(path, "PROVINCE_FERTILITY");
452        let Some(fertility) = province.record("PROVINCE_FERTILITY") else { return Ok(vec![]) };
453        let fertility = self.layout.read_single(&fertility_path, fertility)?;
454        let block_path = child_path(&fertility_path, "REGION_BLOCK");
455        let Some(block) = fertility.record("REGION_BLOCK") else { return Ok(vec![]) };
456        Ok(self.layout.read(&block_path, block)?.iter().map(|entry| entry.str("^+1").unwrap_or_default().to_owned()).collect())
457    }
458
459    /// Reads the faction parts of a province or sea.
460    fn parts(&self, path: &str, parent: &GroupView, array_key: &str, item_key: &str) -> Result<Vec<FactionPartView>> {
461        let array_path = child_path(path, array_key);
462        self.layout.read_items(&array_path, parent, array_key, item_key)?.into_iter()
463            .map(|(_, part)| Ok(FactionPartView {
464                id: part.u32("^+1").unwrap_or_default(),
465                faction: part.u32("^+2").unwrap_or_default(),
466                regions: part.u32s("^+3").map(<[u32]>::to_vec).unwrap_or_default(),
467                main_region: part.u32("CAMPAIGN_EFFECT_BUNDLES+4").unwrap_or_default(),
468            }))
469            .collect()
470    }
471}
472
473/// Builds the error for a world that doesn't fit its layouts.
474fn world_error(message: &str) -> RLibError {
475    RLibError::StartposLayoutError(format!("reading the world: {message}"))
476}