Skip to main content

rpfm_extensions/startpos/cai/
mobiles.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 characters (`CAI_WORLD_CHARACTERS`), mobiles (`CAI_WORLD_RESOURCE_MOBILES`: forces, and agents
12//! moving on their own) and units (`CAI_WORLD_UNITS`).
13
14use std::collections::HashMap;
15
16use rpfm_lib::error::Result;
17use rpfm_lib::files::esf::RecordNode;
18
19use super::factions::owned_direct;
20use super::map::AreaGroups;
21use super::plan::CaiPlan;
22use super::world_view::{CharacterView, ForceView, WorldView};
23use super::{component, CAI_WORLD_PATH};
24use crate::startpos::coords::HexCell;
25use crate::startpos::layout::{child_path, StartposLayout};
26use crate::startpos::region_map::RegionMap;
27
28#[cfg(test)] #[path = "mobiles_test.rs"] mod mobiles_test;
29
30/// Agent codes (`CAI_CHARACTER^+7` and `^+8`) of characters moving with a force.
31const FORCE_AGENT_CODES: (u32, u32) = (0, 3);
32
33/// Agent codes of agents moving on their own (only spies are known).
34const AGENT_CODES: (u32, u32) = (1, 0);
35
36//---------------------------------------------------------------------------//
37//                              Types
38//---------------------------------------------------------------------------//
39
40/// What the mobiles' records need beyond the world and the plan.
41#[derive(Debug, Clone, Copy)]
42pub struct MobileInputs<'a> {
43
44    /// Record layouts.
45    pub layout: &'a StartposLayout,
46
47    /// Component ids.
48    pub plan: &'a CaiPlan,
49
50    /// The world's objects.
51    pub world: &'a WorldView,
52
53    /// Region of each cell.
54    pub region_map: &'a RegionMap,
55
56    /// Connectivity group of each cell.
57    pub groups: &'a AreaGroups<'a>,
58
59    /// Component of the settlement of each garrison residence.
60    pub settlements: &'a HashMap<u32, u32>,
61
62    /// Cell where the AI places the port of each garrison residence's settlement.
63    pub ports: &'a HashMap<u32, HexCell>,
64}
65
66//---------------------------------------------------------------------------//
67//                              Implementations
68//---------------------------------------------------------------------------//
69
70/// Writes the characters list, faction by faction.
71///
72/// # Errors
73///
74/// Returns an error if the layout lacks the records.
75pub fn write_characters(inputs: &MobileInputs) -> Result<RecordNode> {
76    let layout = inputs.layout;
77    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_CHARACTERS");
78    let mut list = layout.builder(&path)?;
79    for (position, faction) in inputs.world.factions().iter().enumerate() {
80        let faction_id = inputs.plan.factions()[position].faction();
81        let posts = &inputs.plan.posts()[position];
82        for (character, ids) in faction.characters().iter().zip(&inputs.plan.characters()[position]) {
83            let force = force_of(faction.forces(), character);
84            let mobile = ids.mobile().unwrap_or_default();
85            let own_unit = force.and_then(|force| force.units().iter().position(|unit| *unit == character.unit())).map(|unit| ids.units()[unit]).unwrap_or_default();
86            let post = faction.posts().iter().position(|(_, holder)| *holder == character.id()).map(|post| posts[post]).unwrap_or_default();
87            let codes = if force.is_none() && ids.mobile().is_some() { AGENT_CODES } else { FORCE_AGENT_CODES };
88
89            let mut owned = layout.builder(&child_path(&path, "OWNED_INDIRECT"))?;
90            owned.group()?.set_u32("^+1", faction_id)?;
91            let mut record = layout.builder(&child_path(&path, "CAI_CHARACTER"))?;
92            record.group()?
93                .set_u32("^+1", mobile)?
94                .set_u32("^+2", own_unit)?
95                .set_u32("^+3", mobile)?
96                .set_u32("^+4", character.id())?
97                .set_u32("^+6", if post == 0 { 0 } else { faction_id })?
98                .set_u32("^+7", codes.0)?
99                .set_u32("^+8", codes.1)?
100                .set_u32("^+9", post)?;
101
102            list.push_group()?
103                .set_record("OWNED_INDIRECT", Some(owned.build()?))?
104                .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, ids.character(), vec![])?))?
105                .set_record("CAI_CHARACTER", Some(record.build()?))?;
106        }
107    }
108    list.build()
109}
110
111/// Returns where each mobile stands, in creation order: its component and the cell of its `CAI_SITUATED`.
112///
113/// Mobiles stand on their commander's cell, except garrison fleets, which wait at their settlement's port.
114pub fn mobile_cells(inputs: &MobileInputs) -> Vec<(u32, HexCell)> {
115    let mut cells = vec![];
116    for (position, faction) in inputs.world.factions().iter().enumerate() {
117        for (character, ids) in faction.characters().iter().zip(&inputs.plan.characters()[position]) {
118            let Some(mobile) = ids.mobile() else { continue };
119            let port = force_of(faction.forces(), character).filter(|force| force.garrison_navy_of() != 0).and_then(|force| inputs.ports.get(&force.garrison_navy_of()));
120            let (x, y) = character.cell();
121            cells.push((mobile, port.copied().unwrap_or(HexCell::new(x, y))));
122        }
123    }
124    cells
125}
126
127/// Writes the mobiles list: each force, and each agent moving on its own, in character order.
128///
129/// # Errors
130///
131/// Returns an error if the layout lacks the records.
132pub fn write_mobiles(inputs: &MobileInputs) -> Result<RecordNode> {
133    let layout = inputs.layout;
134    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_RESOURCE_MOBILES");
135    let mut cells = mobile_cells(inputs).into_iter();
136    let mut list = layout.builder(&path)?;
137    for (position, faction) in inputs.world.factions().iter().enumerate() {
138        let faction_id = inputs.plan.factions()[position].faction();
139        for (character, ids) in faction.characters().iter().zip(&inputs.plan.characters()[position]) {
140            let Some(mobile) = ids.mobile() else { continue };
141            let force = force_of(faction.forces(), character);
142            let residence = force.map(|force| [force.garrison_army_of(), force.garrison_navy_of(), force.residence()].into_iter().find(|residence| *residence != 0).unwrap_or_default()).unwrap_or_default();
143
144            // Agents have no position of their own.
145            let cell = cells.next().map(|(_, cell)| cell).unwrap_or_default();
146            let (own_x, own_y) = if force.is_none() { (0, 0) } else { (cell.x(), cell.y()) };
147
148            let mut record = layout.builder(&child_path(&path, "CAI_RESOURCE_MOBILE"))?;
149            record.group()?
150                .set_u32("^+1", ids.character())?
151                .set_u32("^+2", inputs.settlements.get(&residence).copied().unwrap_or_default())?
152                .set_u32s("^+4", vec![ids.character()])?
153                .set_u32s("^+5", ids.units().to_vec())?
154                .set_bool("^+6", !force.is_some_and(ForceView::navy))?
155                .set_u32("^+12", force.map(ForceView::id).unwrap_or_default())?
156                .set_u32("^+17", u32::from(own_x))?
157                .set_u32("^+18", u32::from(own_y))?
158                .set_u32s("^+20", ids.horde_slots().to_vec())?;
159
160            list.push_group()?
161                .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, faction_id)?))?
162                .set_record("CAI_SITUATED", Some(situated(inputs, &path, cell)?))?
163                .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, mobile, vec![])?))?
164                .set_record("CAI_RESOURCE_MOBILE", Some(record.build()?))?;
165        }
166    }
167    list.build()
168}
169
170/// Writes the units list: each force's units, in character order.
171///
172/// # Errors
173///
174/// Returns an error if the layout lacks the records.
175pub fn write_units(inputs: &MobileInputs) -> Result<RecordNode> {
176    let layout = inputs.layout;
177    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_UNITS");
178    let mut list = layout.builder(&path)?;
179    for (position, faction) in inputs.world.factions().iter().enumerate() {
180        for (character, ids) in faction.characters().iter().zip(&inputs.plan.characters()[position]) {
181            let Some(force) = force_of(faction.forces(), character) else { continue };
182            for (unit, id) in force.units().iter().zip(ids.units()) {
183                let mut record = layout.builder(&child_path(&path, "CAI_UNIT"))?;
184                record.group()?
185                    .set_u32("^+1", if *unit == character.unit() { ids.character() } else { 0 })?
186                    .set_u32("^+2", *unit)?
187                    .set_u32("^+3", ids.mobile().unwrap_or_default())?;
188
189                list.push_group()?
190                    .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, *id, vec![])?))?
191                    .set_record("CAI_UNIT", Some(record.build()?))?;
192            }
193        }
194    }
195    list.build()
196}
197
198/// Writes where an AI object stands (`CAI_SITUATED`): its cell, the component of the map region the cell is in, the
199/// theatre, and the cell's connectivity group.
200pub(crate) fn situated(inputs: &MobileInputs, parent: &str, cell: HexCell) -> Result<RecordNode> {
201    let region = inputs.region_map.region_index_at(cell).and_then(|region| inputs.plan.regions().get(region)).map(|region| region.id()).unwrap_or_default();
202    let mut record = inputs.layout.builder(&child_path(parent, "CAI_SITUATED"))?;
203    record.group()?
204        .set_u32("^+1", u32::from(cell.x()))?
205        .set_u32("^+2", u32::from(cell.y()))?
206        .set_u32("^+3", region)?
207        .set_u32s("^+4", vec![inputs.plan.theatre()])?
208        .set_u32("^+5", inputs.groups.group_at(cell).unwrap_or_default())?;
209    record.build()
210}
211
212/// Returns the force a character commands.
213fn force_of<'a>(forces: &'a [ForceView], character: &CharacterView) -> Option<&'a ForceView> {
214    forces.iter().find(|force| character.force() != 0 && force.id() == character.force())
215}