Skip to main content

rpfm_extensions/startpos/cai/
interface.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 state (`CAI_INTERFACE`): the world mirror, the central pool of analysers, the faction managers
12//! and their AI levels, the director's pool, the high-level pathfinder and the influence maps. Also writes the
13//! campaign director (`CAMPAIGN_DIRECTOR`), which refers to the director's pool.
14//!
15//! The pathfinder is copied from the map's `hlp_data.esf`. The influence maps and every belief start empty: the game
16//! fills them in while playing. The largest distance between regions comes from the pathfinder's graph; the median
17//! distance between bordering regions is left for the game to compute at load.
18
19use rpfm_lib::error::{RLibError, Result};
20use rpfm_lib::files::esf::{NodeType, RecordNode};
21
22use super::managers::{component_map_order, empty_analyser, pool_component, write_managers, ComponentFields, ManagerInputs, ManagerKind};
23use super::mobiles::mobile_cells;
24use super::pathfinder::Pathfinder;
25use super::stances::StartingStances;
26use super::world::{CaiContext, CaiInputs};
27use super::CAI_PATH;
28use crate::startpos::esf_nav::{as_record, child_record};
29use crate::startpos::layout::{child_path, variant_path, StartposLayout};
30use crate::startpos::writer::{bool_node, i32_node, u32_node};
31use crate::startpos::world::calendar::Calendar;
32
33#[cfg(test)] #[path = "interface_test.rs"] mod interface_test;
34
35/// Type codes of the central pool's desires, in creation order.
36pub const CENTRAL_DESIRES: [u32; 29] = [32, 33, 36, 35, 42, 31, 43, 30, 45, 47, 92, 39, 128, 130, 132, 136, 134, 46, 153, 155, 157, 162, 164, 166, 168, 178, 181, 187, 181];
37
38/// Type code of the director pool's time belief.
39const DIRECTOR_TIME: u32 = 3;
40
41/// Type code of the director pool's world belief.
42const DIRECTOR_WORLD: u32 = 2;
43
44/// Type codes of the director pool's beliefs, in creation order.
45pub const DIRECTOR_BELIEFS: [u32; 2] = [DIRECTOR_TIME, DIRECTOR_WORLD];
46
47/// Median distance between bordering regions that makes the game compute it at load.
48const MEDIAN_DISTANCE_UNSET: u32 = u32::MAX;
49
50/// Path of the campaign director.
51const CAMPAIGN_DIRECTOR_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/CAMPAIGN_DIRECTOR";
52
53/// Type codes of the director pool's desires, in creation order: the player rival score and unit balance analysers,
54/// the top level desire and the event generator.
55pub const DIRECTOR_DESIRES: [u32; 4] = [5, 21, 18, 34];
56
57/// Values of the ownership entry linking the director's top level desire to its event generator.
58const TOP_LEVEL_LINK: (f32, f32, u32) = (1000.0, 0.0, 0);
59
60/// Size and tile size of the influence maps, which start without tiles.
61const SPARSE_MAP_SIZE: (u32, u32) = (1024, 8);
62
63/// Cell the influence maps of mobiles hold before their first update.
64const NO_CELL: u16 = u16::MAX;
65
66//---------------------------------------------------------------------------//
67//                              Implementations
68//---------------------------------------------------------------------------//
69
70impl CaiContext {
71
72    /// Writes the AI state (`CAI_INTERFACE`).
73    ///
74    /// # Arguments
75    ///
76    /// * `inputs` - What the AI writer is built from.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if a table is missing, the layout lacks a record or the pathfinder file has no pathfinder.
81    pub fn write_interface(&self, inputs: &CaiInputs) -> Result<RecordNode> {
82        let layout = inputs.layout;
83        let plan = self.plan();
84        let keys = self.world().factions().iter().map(|faction| faction.key().as_str()).collect::<Vec<_>>();
85        let stances = StartingStances::new(inputs.tables, inputs.index, &keys)?;
86        let characters = plan.characters().iter().map(|characters| characters.iter().map(|character| character.character()).collect()).collect::<Vec<_>>();
87        let mobiles = plan.characters().iter().map(|characters| characters.iter().filter_map(|character| character.mobile()).collect()).collect::<Vec<_>>();
88        let start_year = Calendar::new(inputs.tables, inputs.campaign)?.start_year();
89        let managers = write_managers(&ManagerInputs { layout, plan, stances: &stances, characters: &characters, mobiles: &mobiles, start_year })?;
90
91        let (world, influence) = self.with_mobiles(inputs, |mobile_inputs| {
92            let world = self.write_world_with(inputs, mobile_inputs)?;
93            let cells = mobile_cells(mobile_inputs).into_iter().map(|(mobile, cell)| (mobile, (cell.x(), cell.y()))).collect::<Vec<_>>();
94            Ok((world, self.write_influence_maps(layout, &cells)?))
95        })?;
96
97        let hlp_root = as_record(inputs.hlp_data.root_node()).ok_or_else(|| RLibError::StartposLayoutError("the pathfinder file has no root record".to_owned()))?;
98        let pathfinder = child_record(hlp_root, "CAI_HIGH_LEVEL_PATHFINDER").ok_or_else(|| RLibError::StartposLayoutError("the pathfinder file has no pathfinder".to_owned()))?;
99
100        let mut max_distance = layout.builder(&child_path(CAI_PATH, "CAI_INTERFACE_MAX_REGION_DISTANCE"))?;
101        max_distance.group()?.set_u32("^+1", Pathfinder::read(pathfinder)?.max_region_distance())?;
102
103        // The game computes the median at load when it finds this value.
104        let mut median_distance = layout.builder(&child_path(CAI_PATH, "CAI_INTERFACE_MEDIAN_BORDERING_INTERREGION_DISTANCE"))?;
105        median_distance.group()?.set_u32("^+1", MEDIAN_DISTANCE_UNSET)?;
106
107        let levels = plan.factions().iter().zip(plan.manager_kinds())
108            .flat_map(|(ids, kind)| [ids.faction(), kind.level()])
109            .collect();
110
111        // The central pool's slots hold the central desires' ids, which match the game's: they're created first.
112        let mut central = layout.builder(&child_path(CAI_PATH, "CAI_CENTRAL_BDI_POOL"))?;
113        central.group()?.set_u32("^+1", plan.next_id())?;
114
115        let mut record = layout.builder(CAI_PATH)?;
116        record.group()?
117            .set_record("CAI_WORLD", Some(world))?
118            .set_record("CAI_BDI_POOL", Some(self.write_central_pool(layout)?))?
119            .set_record("CAI_CENTRAL_BDI_POOL", Some(central.build()?))?
120            .set_record("CAI_INTERFACE_MANAGERS", Some(managers))?
121            .set_u32s("CAI_INTERFACE_MANAGERS+1", levels)?
122            .set_record("CAI_INTERFACE_DIRECTOR_BDI_POOL", Some(self.write_director_pool(layout)?))?
123            .set_record("CAI_HIGH_LEVEL_PATHFINDER", Some(pathfinder.clone()))?
124            .set_record("CAI_INTERFACE_MAX_REGION_DISTANCE", Some(max_distance.build()?))?
125            .set_record("CAI_INTERFACE_MEDIAN_BORDERING_INTERREGION_DISTANCE", Some(median_distance.build()?))?
126            .set_record("CAI_INFLUENCE_MAP_MANAGER", Some(influence))?;
127        record.build()
128    }
129
130    /// Writes the campaign director (`CAMPAIGN_DIRECTOR`), which the game binds to the AI director's world belief
131    /// on load: a missing belief aborts the load, and another object under its id gets corrupted.
132    ///
133    /// # Arguments
134    ///
135    /// * `layout` - Record layouts.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the layout lacks the records.
140    pub fn write_campaign_director(&self, layout: &StartposLayout) -> Result<RecordNode> {
141        let plan = self.plan();
142        let world_belief = DIRECTOR_BELIEFS.iter().position(|code| *code == DIRECTOR_WORLD).map(|index| plan.director()[index]).unwrap_or_default();
143        let playable = plan.factions().iter().zip(plan.manager_kinds())
144            .filter(|(_, kind)| **kind == ManagerKind::Human)
145            .map(|(ids, _)| ids.faction())
146            .collect();
147
148        let mut interface = layout.builder(&child_path(CAMPAIGN_DIRECTOR_PATH, "CDIR_INTERFACE"))?;
149        interface.group()?
150            .set_u32("^+1", world_belief)?
151            .set_u32s("CDIR_INTERFACE_UBER_FACTIONS+2", playable)?;
152
153        let mut director = layout.builder(CAMPAIGN_DIRECTOR_PATH)?;
154        director.push_group()?.set_record("CDIR_INTERFACE", Some(interface.build()?))?;
155        director.build()
156    }
157
158    /// Writes the central pool: its analysers and log manager, with their type's defaults.
159    fn write_central_pool(&self, layout: &StartposLayout) -> Result<RecordNode> {
160        let path = child_path(CAI_PATH, "CAI_BDI_POOL");
161        let desires_path = child_path(&path, "CAI_BDI_POOL_DESIRES");
162        let ids = (0..CENTRAL_DESIRES.len() as u32).map(|index| self.plan().first_central_desire() + index).collect::<Vec<_>>();
163        let mut desires = layout.builder(&desires_path)?;
164        for index in component_map_order(&ids) {
165            let code = CENTRAL_DESIRES[index];
166            let variant = variant_path(&desires_path, code);
167            let component = pool_component(layout, &variant, ids[index], ComponentFields::default())?;
168            let group = desires.push_variant(code)?;
169            group.set_record("CAI_BDI_COMPONENT", Some(component))?;
170            if let Some(analyser) = empty_analyser(layout, &variant)? {
171                group.set_record("CAI_ANALYSER", Some(analyser))?;
172            }
173        }
174
175        let mut pool = layout.builder(&path)?;
176        pool.group()?.set_record("CAI_BDI_POOL_DESIRES", Some(desires.build()?))?;
177        pool.build()
178    }
179
180    /// Writes the director's pool: its time and world beliefs, its two analysers, its top level desire and the event
181    /// generator the top level desire owns.
182    fn write_director_pool(&self, layout: &StartposLayout) -> Result<RecordNode> {
183        let path = child_path(CAI_PATH, "CAI_INTERFACE_DIRECTOR_BDI_POOL");
184        let pool_path = child_path(&path, "CAI_BDI_POOL");
185        let ids = self.plan().director();
186        let (belief_ids, desire_ids) = ids.split_at(DIRECTOR_BELIEFS.len());
187        let (time, generator) = (belief_ids[0], desire_ids[3]);
188
189        let beliefs_path = child_path(&pool_path, "CAI_BDI_POOL_BELIEFS");
190        let mut beliefs = layout.builder(&beliefs_path)?;
191        for index in component_map_order(belief_ids) {
192            let (code, id) = (DIRECTOR_BELIEFS[index], belief_ids[index]);
193            let variant = variant_path(&beliefs_path, code);
194            let references = if id == time { vec![generator] } else { vec![] };
195            let group = beliefs.push_variant(code)?;
196            group.set_record("CAI_BDI_COMPONENT", Some(pool_component(layout, &variant, id, ComponentFields { references, ..Default::default() })?))?;
197            if id != time {
198                let mut world = layout.builder(&child_path(&variant, "CDIR_WORLD"))?;
199                world.group()?.set_u32("^+1", time)?;
200                group.set_record("CDIR_WORLD", Some(world.build()?))?;
201            }
202        }
203
204        let desires_path = child_path(&pool_path, "CAI_BDI_POOL_DESIRES");
205        let mut desires = layout.builder(&desires_path)?;
206        for index in component_map_order(desire_ids) {
207            let (code, id) = (DIRECTOR_DESIRES[index], desire_ids[index]);
208            let links = if index == 2 { vec![(generator, TOP_LEVEL_LINK)] } else { vec![] };
209            let variant = variant_path(&desires_path, code);
210            let component = pool_component(layout, &variant, id, ComponentFields { links, ..Default::default() })?;
211            let group = desires.push_variant(code)?;
212            group.set_record("CAI_BDI_COMPONENT", Some(component))?;
213            if let Some(analyser) = empty_analyser(layout, &variant)? {
214                group.set_record("CAI_ANALYSER", Some(analyser))?;
215            }
216        }
217
218        let mut pool = layout.builder(&pool_path)?;
219        pool.group()?
220            .set_record("CAI_BDI_POOL_BELIEFS", Some(beliefs.build()?))?
221            .set_record("CAI_BDI_POOL_DESIRES", Some(desires.build()?))?;
222
223        let mut analysers = layout.builder(&child_path(&path, "CDIR_BDI_POOL"))?;
224        analysers.group()?
225            .set_u32("^+1", desire_ids[0])?
226            .set_u32("^+2", desire_ids[1])?;
227
228        let mut director = layout.builder(&path)?;
229        director.group()?
230            .set_record("CAI_BDI_POOL", Some(pool.build()?))?
231            .set_record("CDIR_BDI_POOL", Some(analysers.build()?))?;
232        director.build()
233    }
234
235    /// Writes the influence maps: two per mobile (where it is and where it can reach), in their constructor state,
236    /// and one per faction. Every map starts without tiles.
237    ///
238    /// # Arguments
239    ///
240    /// * `layout` - Record layouts.
241    /// * `cells` - Each mobile's component and cell, in creation order.
242    fn write_influence_maps(&self, layout: &StartposLayout, cells: &[(u32, (u16, u16))]) -> Result<RecordNode> {
243        let path = child_path(CAI_PATH, "CAI_INFLUENCE_MAP_MANAGER");
244        let map_path = child_path(&path, "CAI_SPARSE_MAP");
245        let empty_map = || -> Result<NodeType> {
246            let record = layout.raw_record(&map_path, vec![u32_node(SPARSE_MAP_SIZE.0), u32_node(SPARSE_MAP_SIZE.1), u32_node(0)])?;
247            Ok(NodeType::Record(Box::new(record)))
248        };
249
250        let mut children = vec![u32_node(cells.len() as u32)];
251        let mobiles = cells.iter().map(|(mobile, _)| *mobile).collect::<Vec<_>>();
252        for index in component_map_order(&mobiles) {
253            let (mobile, (x, y)) = cells[index];
254            children.extend([u32_node(mobile), empty_map()?, bool_node(false), NodeType::U16(NO_CELL), NodeType::U16(NO_CELL), u32_node(0), u32_node(0), u32_node(0)]);
255            children.extend([empty_map()?, i32_node(0), bool_node(false), NodeType::U16(x), NodeType::U16(y), u32_node(0), u32_node(0), u32_node(0)]);
256        }
257
258        let factions = self.plan().factions();
259        children.push(u32_node(factions.len() as u32));
260        for ids in factions {
261            children.extend([u32_node(ids.faction()), empty_map()?]);
262        }
263        layout.raw_record(&path, children)
264    }
265}