Skip to main content

rpfm_extensions/startpos/world/writer/
characters.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 characters of a faction (`FACTION/CHARACTER_ARRAY`).
12//!
13//! The game draws some values from its RNG (art sets, birth seasons, colonels' ages and names): this writer picks
14//! valid ones deterministically.
15
16use std::collections::HashMap;
17use std::f32::consts::PI;
18
19use rpfm_lib::error::Result;
20use rpfm_lib::files::esf::{Coordinates2DNode, NodeType, RecordNode};
21
22use super::forces::{ForcePlan, PlannedCharacter};
23use super::government::PostPlan;
24use super::sight::{write_line_of_sight, SightPlan};
25use super::{child, WorldInputs, WorldPlan};
26use crate::startpos::layout::{child_path, StartposLayout};
27use crate::startpos::coords::HexCell;
28use crate::startpos::tables::Row;
29use crate::startpos::vision::Vision;
30use crate::startpos::world::agents::{agent_abilities, agent_attributes, skill_set, starting_skills, ABILITIES, ATTRIBUTES};
31use crate::startpos::world::bodyguards::{bodyguard, Bodyguard};
32use crate::startpos::world::characters::{birth_year, loyalty, placement, political_party, Placement};
33use crate::startpos::world::government::{faction_leader, political_parties, PoliticalParty};
34use crate::startpos::world::{CampaignIndex, CharacterEntry, FactionEntry, NAME_COLUMNS};
35
36#[path = "family.rs"] pub(crate) mod family;
37
38#[cfg(test)] #[path = "characters_test.rs"] mod characters_test;
39
40/// Path of the characters of a faction.
41const CHARACTER_ARRAY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY";
42
43/// Path of a character.
44const CHARACTER_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY/CHARACTER";
45
46/// Path of a character's details.
47const DETAILS_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY/CHARACTER/CHARACTER_DETAILS";
48
49
50/// Agent type of the generated garrison commanders.
51pub(crate) const COLONEL_AGENT: &str = "colonel";
52
53/// Agent type of the generals.
54pub(crate) const GENERAL_AGENT: &str = "general";
55
56/// Age of the generated colonels (the game draws it from its RNG).
57const COLONEL_AGE: u32 = 30;
58
59/// Localisation key prefix of the character names.
60pub(crate) const NAME_KEY_PREFIX: &str = "names_name_";
61
62/// Suffix the startpos adds to the art set of a character (`CHARACTER_DETAILS` #34).
63const ART_SET_SUFFIX: &str = "0";
64
65/// Value of `LINE_OF_SIGHT+18` (`CHARACTER` #28) for faction leaders (meaning unknown).
66const LEADER_VALUE: u32 = 4;
67
68//---------------------------------------------------------------------------//
69//                              Types
70//---------------------------------------------------------------------------//
71
72/// Writes the characters of the factions.
73pub struct CharacterWriter<'a, 'b> {
74
75    /// Tables, startpos rows and layouts.
76    inputs: &'b WorldInputs<'a>,
77
78    /// Characters and forces of the world.
79    forces: &'b ForcePlan<'a>,
80
81    /// Government posts of the factions.
82    posts: &'b PostPlan,
83
84    /// Vision of the world.
85    sight: &'b SightPlan,
86
87    /// `start_pos_characters` rows, by id.
88    rows: HashMap<String, Row<'a>>,
89
90    /// Traits of each character (trait key and points of its level), by `start_pos_characters` id.
91    traits: HashMap<String, Vec<(String, i64)>>,
92
93    /// Move points and line of sight of each agent type.
94    agents: HashMap<String, (u32, f32)>,
95
96    /// Art sets of the campaign.
97    art_sets: Vec<ArtSet>,
98
99    /// Default bodyguard of each faction's generals, by faction key.
100    bodyguards: HashMap<String, Option<Bodyguard>>,
101
102    /// Political parties of each faction, by faction key.
103    parties: HashMap<String, Vec<PoliticalParty>>,
104
105    /// Family member id of each planned character, by position in the plan.
106    family_ids: Vec<u32>,
107
108    /// Family member id of each table character, living or dead, by `start_pos_characters` id.
109    entry_family_ids: HashMap<String, u32>,
110
111    /// The campaign's start year.
112    start_year: u32,
113}
114
115/// An art set a character can use (`campaign_character_art_sets`).
116#[derive(Debug, Clone)]
117struct ArtSet {
118    id: String,
119    agent: String,
120    is_male: bool,
121    faction: String,
122    subculture: String,
123    culture: String,
124}
125
126/// Who a `CHARACTER_DETAILS` record describes: a character of the world, or a dead one of the family tree.
127struct DetailsSubject<'r> {
128
129    /// Path of the record.
130    path: &'r str,
131
132    /// The character's faction.
133    faction: &'r FactionEntry,
134
135    /// The faction the character was created in.
136    origin: &'r FactionEntry,
137
138    /// The character's table entry, `None` for colonels.
139    entry: Option<&'r CharacterEntry>,
140
141    /// Runtime id of the character, 0 for the dead.
142    character_id: u32,
143
144    /// Family member id of the character.
145    family_id: u32,
146
147    /// Death year and death type code, for the dead.
148    death: Option<(u32, i32)>,
149
150    /// Bodyguard of a general without a force.
151    bodyguard: Option<Bodyguard>,
152}
153
154/// What the writer needs to know about a character, from its table row or as a generated colonel.
155struct CharacterInfo<'r> {
156    agent: String,
157    is_male: bool,
158    birth_year: u32,
159    row: Option<&'r Row<'r>>,
160}
161
162//---------------------------------------------------------------------------//
163//                              Implementations
164//---------------------------------------------------------------------------//
165
166impl<'a, 'b> CharacterWriter<'a, 'b> {
167
168    /// Prepares the writer of the characters.
169    ///
170    /// # Arguments
171    ///
172    /// * `inputs` - Tables, startpos rows and layouts.
173    /// * `plan` - Order of the factions and regions.
174    /// * `forces` - Characters and forces of the world.
175    /// * `posts` - Government posts of the factions.
176    /// * `sight` - Vision of the world.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if a table or column is missing.
181    pub fn new(inputs: &'b WorldInputs<'a>, plan: &WorldPlan<'a>, forces: &'b ForcePlan<'a>, posts: &'b PostPlan, sight: &'b SightPlan) -> Result<Self> {
182        let tables = inputs.tables;
183        let mut rows = HashMap::new();
184        for row in tables.table("start_pos_characters")?.rows() {
185            rows.insert(row.str("ID")?.into_owned(), row);
186        }
187
188        let mut levels = HashMap::new();
189        for row in tables.table("character_trait_levels")?.rows() {
190            levels.insert(row.str("key")?.into_owned(), (row.str("trait")?.into_owned(), row.int("threshold_points")?));
191        }
192        let mut traits = HashMap::<String, Vec<(String, i64)>>::new();
193        for row in tables.table("start_pos_character_traits")?.rows() {
194            if let Some(level) = levels.get(row.str("trait_level")?.as_ref()) {
195                traits.entry(row.str("character_id")?.into_owned()).or_default().push(level.clone());
196            }
197        }
198
199        let mut agents = HashMap::new();
200        for row in tables.table("agents")?.rows() {
201            agents.insert(row.str("key")?.into_owned(), (u32::try_from(row.int("move_points")?).unwrap_or_default(), row.number("line_of_sight")? as f32));
202        }
203
204        let mut groups = vec![];
205        for row in tables.table("campaign_character_art_sets_group_junctions")?.rows() {
206            if row.str("campaign")? == *inputs.index.campaign() {
207                groups.push(row.str("group")?.into_owned());
208            }
209        }
210        let mut art_sets = vec![];
211        for row in tables.table("campaign_character_art_sets")?.rows() {
212            if groups.contains(&row.str("group")?.into_owned()) {
213                art_sets.push(ArtSet {
214                    id: row.str("art_set_id")?.into_owned(),
215                    agent: row.str("agent_type")?.into_owned(),
216                    is_male: row.bool("is_male")?,
217                    faction: row.str("faction")?.into_owned(),
218                    subculture: row.str("subculture")?.into_owned(),
219                    culture: row.str("culture")?.into_owned(),
220                });
221            }
222        }
223
224        let mut bodyguards = HashMap::new();
225        let mut parties = HashMap::new();
226        for faction in plan.factions() {
227            bodyguards.insert(faction.key().to_owned(), bodyguard(tables, faction, false)?);
228            parties.insert(faction.key().to_owned(), political_parties(tables, faction.key())?);
229        }
230
231        let (family_ids, entry_family_ids) = family_ids(inputs.index, plan, forces);
232        Ok(Self {
233            family_ids,
234            entry_family_ids,
235            start_year: super::armies::start_year(inputs)?,
236            inputs,
237            forces,
238            posts,
239            sight,
240            rows,
241            traits,
242            agents,
243            art_sets,
244            bodyguards,
245            parties,
246        })
247    }
248
249    /// Writes the characters of a faction.
250    ///
251    /// # Errors
252    ///
253    /// Returns an error if a table, column or layout is missing.
254    pub fn write_character_array(&self, faction: &FactionEntry) -> Result<RecordNode> {
255        let mut array = self.inputs.layout.builder(CHARACTER_ARRAY_PATH)?;
256        for position in self.forces.character_positions(faction.key()) {
257            let character = &self.forces.characters()[position];
258            array.push_group()?.set_record("CHARACTER", Some(self.write_character(position, character)?))?;
259        }
260        array.build()
261    }
262
263    /// Writes a character: its position, ids, details and the post it holds.
264    fn write_character(&self, position: usize, character: &PlannedCharacter) -> Result<RecordNode> {
265        let layout = self.inputs.layout;
266        let faction = character.faction();
267        let info = self.info(character)?;
268        let (move_points, line_of_sight) = self.agents.get(&info.agent).copied().unwrap_or_default();
269        let (world, cell) = self.position(character)?;
270        let force = character.force().map(|force| &self.forces.forces()[force]);
271        let is_leader = character.entry().is_some_and(|entry| faction_leader(self.inputs.index, faction.key()) == Some(entry));
272
273        let mut coordinates = Coordinates2DNode::default();
274        coordinates.set_x(world.0);
275        coordinates.set_y(world.1);
276        let mut locomotable = child(layout, CHARACTER_PATH, "LOCOMOTABLE")?;
277        locomotable.group()?
278            .set("^+1", NodeType::Coord2d(coordinates))?
279            .set_u32("^+2", u32::from(cell.x()))?
280            .set_u32("^+3", u32::from(cell.y()))?
281            .set_f32("^+4", PI)?
282            .set_u32("^+6", move_points)?
283            .set_u32("^+7", move_points)?;
284
285        let override_unit = character.entry().map(|entry| entry.override_general_unit().as_str()).unwrap_or_default();
286        let mut record = layout.builder(CHARACTER_PATH)?;
287        record.group()?
288            .set_record("LOCOMOTABLE", Some(locomotable.build()?))?
289            .set_u32("LOCOMOTABLE+1", character.id())?
290            .set_record("CHARACTER_DETAILS", Some(self.write_details(&self.subject(position, character, &info), &info)?))?
291            .set_str("CHARACTER_DETAILS+1", &info.agent)?
292            .set_u32("CHARACTER_DETAILS+2", force.map(|force| force.id()).unwrap_or_default())?
293            .set_u32("CHARACTER_DETAILS+3", force.and_then(|force| force.commander_unit_id()).unwrap_or_default())?
294            .set_u32("CHARACTER_DETAILS+6", if is_leader { self.posts.leader_post(faction.key()).unwrap_or_default() } else { 0 })?
295            .set_f32("LINE_OF_SIGHT+4", line_of_sight)?
296            .set_str("LINE_OF_SIGHT+16", override_unit)?
297            .set_u32("LINE_OF_SIGHT+18", if is_leader { LEADER_VALUE } else { 0 })?
298            .set_bool("LINE_OF_SIGHT+19", is_leader)?
299            .set_record("LINE_OF_SIGHT", Some(write_line_of_sight(layout, CHARACTER_PATH, self.sight.grid(), self.vision(character)?.as_ref())?))?;
300        record.build()
301    }
302
303    /// Returns what the details of a character of the world are written from.
304    fn subject<'r>(&'r self, position: usize, character: &'r PlannedCharacter, info: &CharacterInfo) -> DetailsSubject<'r> {
305        let faction = character.faction();
306        let bodyguard = self.bodyguards.get(faction.key()).cloned().flatten().filter(|_| info.agent != COLONEL_AGENT && character.force().is_none());
307        DetailsSubject {
308            path: DETAILS_PATH,
309            faction,
310            origin: character.origin(),
311            entry: character.entry(),
312            character_id: character.id(),
313            family_id: self.family_ids[position],
314            death: None,
315            bodyguard,
316        }
317    }
318
319    /// Writes the details of a character: names, birth (and death), traits, abilities, skills, art set and bodyguard.
320    fn write_details(&self, subject: &DetailsSubject, info: &CharacterInfo) -> Result<RecordNode> {
321        let layout = self.inputs.layout;
322        let (path, faction) = (subject.path, subject.faction);
323        let row = info.row;
324
325        let mut birth = child(layout, path, "DATE")?;
326        birth.group()?.set_u32("^+1", info.birth_year)?;
327
328        let mut traits = child(layout, &format!("{path}/TRAITS"), "TRAIT")?;
329        let character_traits = subject.entry.and_then(|entry| self.traits.get(entry.id())).map(Vec::as_slice).unwrap_or_default();
330        for (key, points) in character_traits {
331            traits.push_group()?
332                .set_str("^+1", key)?
333                .set_i32("^+2", i32::try_from(*points).unwrap_or_default())?;
334        }
335        let mut traits_record = child(layout, path, "TRAITS")?;
336        traits_record.group()?.set_record("TRAIT", Some(traits.build()?))?;
337
338        let mut abilities = child(layout, path, "AGENT_ABILITIES")?;
339        for (ability, enabled) in ABILITIES.iter().zip(agent_abilities(self.inputs.tables, &info.agent)?) {
340            abilities.push_group()?.set_str("^+1", ability)?.set_bool("^+2", enabled)?;
341        }
342        let mut attributes = child(layout, path, "AGENT_ATTRIBUTES")?;
343        for (attribute, value) in ATTRIBUTES.iter().zip(agent_attributes(self.inputs.tables, &info.agent)?) {
344            attributes.push_group()?.set_str("^+1", attribute)?.set_i32("^+2", i32::try_from(value).unwrap_or_default())?;
345        }
346
347        let set_key = skill_set(self.inputs.tables, self.inputs.index, &info.agent, faction)?.unwrap_or_default();
348        let mut skills_block = child(layout, &format!("{path}/CAMPAIGN_SKILLS"), "CAMPAIGN_SKILLS_BLOCK")?;
349        for skill in starting_skills(self.inputs.tables, self.inputs.index, &set_key, faction)? {
350            skills_block.push_group()?
351                .set_str("^+1", &skill.skill)?
352                .set_u32("^+2", skill.tier)?
353                .set_f32("^+3", skill.indent)?
354                .set_u32("^+4", skill.points)?;
355        }
356        let mut skills = child(layout, path, "CAMPAIGN_SKILLS")?;
357        skills.group()?
358            .set_str("^+2", &set_key)?
359            .set_bool("^+4", info.is_male)?
360            .set_record("CAMPAIGN_SKILLS_BLOCK", Some(skills_block.build()?))?;
361
362        let seed = if subject.character_id != 0 { subject.character_id } else { subject.family_id };
363        let art_set = self.art_set(faction, info, seed);
364        let mut art_set_info = child(layout, path, "CHARACTER_DETAILS_ART_SET_INFO")?;
365        art_set_info.group()?
366            .set_str("^+1", faction.key())?
367            .set_str("^+2", faction.culture())?
368            .set_str("^+3", faction.subculture())?
369            .set_str("^+4", art_set_agent(&info.agent))?
370            .set_str("^+5", art_set)?;
371
372        let mut record = layout.builder(path)?;
373        let group = record.group()?;
374        if let Some((year, code)) = subject.death {
375            let mut death = child(layout, path, "DATE@2")?;
376            death.group()?.set_u32("^+1", year)?;
377            group.set_record("DATE@2", Some(death.build()?))?.set_i32("DATE@2+1", code)?;
378        }
379        group.set_record("TRAITS", Some(traits_record.build()?))?
380            .set_record("CHARACTER_NAME", Some(self.write_name(path, subject.entry)?))?
381            .set_record("DATE", Some(birth.build()?))?
382            .set_str("DATE@2+2", faction.key())?
383            .set_u32("DATE@2+3", subject.character_id)?
384            .set_record("AGENT_ABILITIES", Some(abilities.build()?))?
385            .set_record("AGENT_ATTRIBUTES", Some(attributes.build()?))?
386            .set_bool("AGENT_ATTRIBUTES+3", info.is_male)?
387            .set_str("AGENT_ATTRIBUTES+6", &info.agent)?
388            .set_record("CAMPAIGN_SKILLS", Some(skills.build()?))?
389            .set_record("CHARACTER_DETAILS_ART_SET_INFO", Some(art_set_info.build()?))?
390            .set_record("GENERAL_BODYGUARD_DETAILS", Some(self.write_bodyguard(path, subject.bodyguard.as_ref())?))?
391            .set_u32("GENERAL_BODYGUARD_DETAILS+1", subject.family_id)?
392            .set_str("GENERAL_BODYGUARD_DETAILS+6", &format!("{art_set}{ART_SET_SUFFIX}"))?
393            .set_str("GENERAL_BODYGUARD_DETAILS+8", subject.origin.key())?;
394
395        if let (Some(row), Some(entry)) = (row, subject.entry) {
396            let ambition = if subject.death.is_some() { 0 } else { u32::try_from(row.int("ambition")?).unwrap_or_default() };
397            record.group()?
398                .set_u32("CAMPAIGN_EFFECT_BUNDLES+3", ambition)?
399                .set_str("CAMPAIGN_EFFECT_BUNDLES+4", political_party(entry, info.is_male, entry.widowed(), &row.str("political_party")?, self.parties.get(faction.key()).map(Vec::as_slice).unwrap_or_default()))?
400                .set_bool("GENERAL_BODYGUARD_DETAILS+2", row.bool("immortal")?)?
401                .set_i32("GENERAL_BODYGUARD_DETAILS+4", i32::try_from(loyalty(entry)).unwrap_or_default())?;
402        }
403        record.build()
404    }
405
406    /// Writes the names of a character (`CHARACTER_NAME`) under a parent record. See [`write_character_name`].
407    pub(super) fn write_name(&self, parent: &str, character: Option<&CharacterEntry>) -> Result<RecordNode> {
408        write_character_name(self.inputs.layout, parent, character)
409    }
410
411    /// Writes the bodyguard of a character: the faction's default one for generals without a force, empty otherwise
412    /// (a general's force holds its bodyguard as a unit).
413    ///
414    /// # Arguments
415    ///
416    /// * `details_path` - Path of the character's details.
417    /// * `default` - The bodyguard, if the character keeps one.
418    fn write_bodyguard(&self, details_path: &str, default: Option<&Bodyguard>) -> Result<RecordNode> {
419        let path = format!("{details_path}/GENERAL_BODYGUARD_DETAILS");
420        let mut record = self.inputs.layout.builder(&path)?;
421        let Some(default) = default else {
422            record.group()?.set_str("^+1", "")?;
423            return record.build();
424        };
425
426        // The unit's details are only there when the character keeps its bodyguard.
427        let bonuses = self.inputs.layout.default_record(&child_path(&path, "UNIT_STATISTICS_BONUSES"))?;
428        record.group()?
429            .set_str("^+1", default.unit())?
430            .set_u32("^+2", default.men())?
431            .set_u32("^+3", default.men())?
432            .set_u32("^+4", 0)?
433            .set_u32("^+5", 0)?
434            .set_f32("^+6", 0.0)?
435            .set_record("UNIT_STATISTICS_BONUSES", Some(bonuses))?;
436        record.build()
437    }
438
439    /// Returns the family member id of a faction's progenitor (`start_pos_characters.progenitor`, living or dead), or
440    /// 0 if it has none.
441    pub fn progenitor_family_id(&self, faction: &FactionEntry) -> u32 {
442        self.inputs.index.characters_of(faction.key())
443            .find(|entry| entry.progenitor())
444            .and_then(|entry| self.entry_family_ids.get(entry.id()))
445            .copied()
446            .unwrap_or_default()
447    }
448
449    /// Returns what a character sees: a disk of its agent's sight radius around it. Generals without a force don't
450    /// see anything.
451    ///
452    /// # Errors
453    ///
454    /// Returns an error if a table or column is missing.
455    pub fn vision(&self, character: &PlannedCharacter) -> Result<Option<Vision>> {
456        let agent = self.info(character)?.agent;
457        if agent == GENERAL_AGENT && character.force().is_none() {
458            return Ok(None);
459        }
460        let (_, line_of_sight) = self.agents.get(&agent).copied().unwrap_or_default();
461        let (position, _) = self.position(character)?;
462        Ok(Some(self.sight.grid().disk(position, line_of_sight)))
463    }
464
465    /// Returns the planned characters of a faction.
466    pub fn characters_of<'s>(&'s self, faction: &FactionEntry) -> impl Iterator<Item = &'s PlannedCharacter<'a>> + 's {
467        self.forces.characters_of(faction.key())
468    }
469
470    /// Returns the runtime id of a faction's leader, if it has one.
471    pub fn leader_id(&self, faction: &FactionEntry) -> Option<u32> {
472        let leader = faction_leader(self.inputs.index, faction.key())?;
473        self.forces.characters_of(faction.key()).find(|character| character.entry() == Some(leader)).map(|character| character.id())
474    }
475
476    /// Returns the agent type, gender, birth year and table row of a character.
477    fn info<'r>(&'r self, character: &PlannedCharacter) -> Result<CharacterInfo<'r>> {
478        let Some(entry) = character.entry() else {
479            return Ok(CharacterInfo { agent: COLONEL_AGENT.to_owned(), is_male: true, birth_year: self.start_year.saturating_sub(COLONEL_AGE), row: None });
480        };
481        let row = self.rows.get(entry.id());
482        Ok(CharacterInfo {
483            agent: entry.agent().to_owned(),
484            is_male: row.map(|row| row.bool("is_male")).transpose()?.unwrap_or(true),
485            birth_year: u32::try_from(birth_year(i64::from(self.start_year), entry)).unwrap_or_default(),
486            row,
487        })
488    }
489
490    /// Returns the art set of a character: one of the campaign's art sets for its agent type and gender, of its
491    /// faction, subculture (sets without faction) or culture (sets without faction and subculture). The game draws
492    /// one at random; this takes them in turns by `seed` (the character's runtime id, or family id for the dead).
493    fn art_set(&self, faction: &FactionEntry, info: &CharacterInfo, seed: u32) -> &str {
494        let candidates = self.art_sets.iter()
495            .filter(|art_set| art_set.agent == art_set_agent(&info.agent) && art_set.is_male == info.is_male)
496            .filter(|art_set| art_set.faction == *faction.key()
497                || (art_set.faction.is_empty() && art_set.subculture == *faction.subculture())
498                || (art_set.faction.is_empty() && art_set.subculture.is_empty() && art_set.culture == *faction.culture()))
499            .collect::<Vec<_>>();
500        candidates.get(seed as usize % candidates.len().max(1)).map(|art_set| art_set.id.as_str()).unwrap_or_default()
501    }
502
503    /// Returns the world position and cell of a character.
504    ///
505    /// Colonels stand on their settlement. Table characters stand on their starting cell, their linked settlement,
506    /// their faction's first general (factions without capital), or off the map.
507    pub(super) fn position(&self, character: &PlannedCharacter) -> Result<((f32, f32), HexCell)> {
508        let settlement_cell = |region: &str| self.inputs.settlements.iter()
509            .find(|settlement| settlement.region() == region)
510            .and_then(|settlement| settlement.slots().first())
511            .and_then(|slot| slot.position());
512        let scale = self.inputs.region_map.scale();
513        let at = |cell: Option<HexCell>| cell.map(|cell| (scale.world_position(cell), cell)).unwrap_or_default();
514
515        let Some(entry) = character.entry() else {
516            let region = character.force().and_then(|force| self.forces.forces()[force].region().clone()).unwrap_or_default();
517            return Ok(at(settlement_cell(&region)));
518        };
519
520        Ok(match placement(self.inputs.tables, self.inputs.index, entry)? {
521            Placement::Cell(cell) => at(Some(cell)),
522            Placement::Settlement(region) => at(settlement_cell(&region)),
523            Placement::FirstArmy => {
524                let first_general = self.forces.characters_of(character.faction().key())
525                    .find(|other| other.force().is_some() && other.entry().is_some_and(|other| other.id() != entry.id()));
526                match first_general {
527                    Some(general) => self.position(general)?,
528                    None => Default::default(),
529                }
530            },
531            Placement::OffMap => Default::default(),
532        })
533    }
534}
535
536/// Returns the agent type whose art sets a character uses: colonels look like generals.
537fn art_set_agent(agent: &str) -> &str {
538    if agent == COLONEL_AGENT { GENERAL_AGENT } else { agent }
539}
540
541/// Returns the family member ids: of every planned character (by position in the plan), and of every table
542/// character, living or dead (by `start_pos_characters` id). The counter numbers each faction's table characters
543/// (the living ones, then the dead ones, which only appear in the family tree), then the colonels.
544fn family_ids(index: &CampaignIndex, plan: &WorldPlan, forces: &ForcePlan) -> (Vec<u32>, HashMap<String, u32>) {
545    let mut ids = vec![0; forces.characters().len()];
546    let mut by_entry = HashMap::new();
547    let mut next_id = 1;
548    for faction in plan.factions() {
549        for (position, character) in forces.characters().iter().enumerate() {
550            if let Some(entry) = character.entry().filter(|_| character.origin().key() == faction.key()) {
551                ids[position] = next_id;
552                by_entry.insert(entry.id().to_owned(), next_id);
553                next_id += 1;
554            }
555        }
556        for entry in index.characters_of(faction.key()).filter(|entry| !entry.death_type().is_empty()) {
557            by_entry.insert(entry.id().to_owned(), next_id);
558            next_id += 1;
559        }
560    }
561    for (position, character) in forces.characters().iter().enumerate() {
562        if character.entry().is_none() {
563            ids[position] = next_id;
564            next_id += 1;
565        }
566    }
567    (ids, by_entry)
568}
569
570/// Writes the names of a character (`CHARACTER_NAME`) under a parent record: one block per name kind, with the name's
571/// localisation key if it has one. Nameless characters get the same blocks, all of kind 0.
572///
573/// # Arguments
574///
575/// * `layout` - Record layouts of the game.
576/// * `parent` - Path of the record holding the names.
577/// * `character` - The character, or `None` for a nameless one.
578///
579/// # Errors
580///
581/// Returns an error if the layout lacks the records.
582pub(crate) fn write_character_name(layout: &StartposLayout, parent: &str, character: Option<&CharacterEntry>) -> Result<RecordNode> {
583    let names_path = format!("{parent}/CHARACTER_NAME/NAMES_BLOCK");
584    let mut blocks = child(layout, &format!("{parent}/CHARACTER_NAME"), "NAMES_BLOCK")?;
585    for kind in 0..NAME_COLUMNS.len() {
586        let name = character.and_then(|character| character.names().get(kind)).map(String::as_str).unwrap_or_default();
587        let mut localisation = child(layout, &names_path, "CAMPAIGN_LOCALISATION")?;
588        if name.is_empty() {
589            localisation.group()?.set_str("^+1", "")?;
590        } else {
591            localisation.group()?.set_str("^+1", &format!("{NAME_KEY_PREFIX}{name}"))?.unset("^+2")?;
592        }
593        blocks.push_group()?
594            .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
595            .set_i32("CAMPAIGN_LOCALISATION+1", if character.is_some() { i32::try_from(kind).unwrap_or_default() } else { 0 })?;
596    }
597
598    let mut record = child(layout, parent, "CHARACTER_NAME")?;
599    record.group()?.set_record("NAMES_BLOCK", Some(blocks.build()?))?;
600    record.build()
601}