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