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