Skip to main content

rpfm_extensions/startpos/world/writer/
family.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 family tree (`WORLD/FAMILY_TREE`): a member per family id, in id order. Characters of the world
12//! only hold their links; dead table characters also hold their details.
13//!
14//! Links come from `start_pos_family_relationships`, in table order: a child points to its father or mother (by the
15//! parent's gender) and is listed among the parent's children (adopted ones in their own list); living spouses point
16//! to each other, and spouses of the dead list each other as former spouses. Verified on Attila's main campaign:
17//! every relationship the game keeps lands where it does in the game's tree (the game drops a few conflicting rows
18//! of one family, and orders the lists by its own link order).
19
20use std::collections::HashMap;
21
22use rpfm_lib::error::{RLibError, Result};
23use rpfm_lib::files::esf::{NodeType, RecordNode};
24
25use super::{CharacterInfo, CharacterWriter, DetailsSubject};
26use crate::startpos::layout::child_path;
27use crate::startpos::world::{CharacterEntry, FactionEntry};
28use crate::startpos::writer::{bool_node, u32_node};
29
30#[cfg(test)] #[path = "family_test.rs"] mod family_test;
31
32/// Path of the family tree.
33const FAMILY_TREE_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FAMILY_TREE";
34
35/// Age from which living members count as adults.
36const ADULT_AGE: i64 = 16;
37
38/// Death type codes of the dead (`CHARACTER_DETAILS` `DATE@2+1`), by `start_pos_characters.death_type`.
39pub(crate) const DEATH_TYPE_CODES: [(&str, i32); 4] = [("KILLED_IN_ACTION", 1), ("OLD_AGE", 3), ("EXECUTED", 5), ("DISEASE", 17)];
40
41//---------------------------------------------------------------------------//
42//                              Types
43//---------------------------------------------------------------------------//
44
45/// Links and flags of a family member.
46#[derive(Debug, Clone, Default)]
47struct Member {
48    mother: u32,
49    father: u32,
50    spouse: u32,
51    former_spouses: Vec<u32>,
52    children: Vec<u32>,
53    adopted_children: Vec<u32>,
54    bastard: bool,
55    adopted: bool,
56}
57
58//---------------------------------------------------------------------------//
59//                              Implementations
60//---------------------------------------------------------------------------//
61
62impl CharacterWriter<'_, '_> {
63
64    /// Writes the family tree.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if a table, column or layout is missing.
69    pub fn write_family_tree(&self) -> Result<RecordNode> {
70        let index = self.inputs.index;
71        let entries = index.characters().iter().filter_map(|entry| self.entry_family_ids.get(entry.id()).map(|id| (*id, entry))).collect::<HashMap<_, _>>();
72        let members = self.family_links(&entries)?;
73        let count = self.family_ids.iter().chain(self.entry_family_ids.values()).copied().max().unwrap_or_default();
74
75        let mut children = vec![u32_node(count)];
76        for id in 1..=count {
77            let member = members.get(&id).cloned().unwrap_or_default();
78            children.push(NodeType::Record(Box::new(self.write_member(id, entries.get(&id).copied(), &member)?)));
79        }
80
81        // Version 2 trees end with a list of ids (count, then ids), empty at the start.
82        children.push(u32_node(0));
83        self.inputs.layout.raw_record(FAMILY_TREE_PATH, children)
84    }
85
86    /// Works out the links of the members from `start_pos_family_relationships`.
87    fn family_links(&self, entries: &HashMap<u32, &CharacterEntry>) -> Result<HashMap<u32, Member>> {
88        let mut members = HashMap::<u32, Member>::new();
89        let is_dead = |id: u32| entries.get(&id).is_some_and(|entry| !entry.death_type().is_empty());
90        for row in self.inputs.tables.table("start_pos_family_relationships")?.rows() {
91            let (Some(&character), Some(&related)) = (self.entry_family_ids.get(row.str("character")?.as_ref()), self.entry_family_ids.get(row.str("related_to")?.as_ref())) else { continue };
92            match row.str("relationship")?.as_ref() {
93                "child" => {
94                    let adopted = row.bool("adopted")?;
95                    let parent_is_male = entries.get(&related).and_then(|entry| self.rows.get(entry.id())).map(|row| row.bool("is_male")).transpose()?.unwrap_or(true);
96                    let member = members.entry(character).or_default();
97                    if parent_is_male { member.father = related } else { member.mother = related }
98                    member.bastard |= row.bool("bastard")?;
99                    member.adopted |= adopted;
100                    let parent = members.entry(related).or_default();
101                    let list = if adopted { &mut parent.adopted_children } else { &mut parent.children };
102                    if !list.contains(&character) {
103                        list.push(character);
104                    }
105                },
106                "spouse" if is_dead(character) || is_dead(related) => {
107                    for (member, other) in [(character, related), (related, character)] {
108                        let former = &mut members.entry(member).or_default().former_spouses;
109                        if !former.contains(&other) {
110                            former.push(other);
111                        }
112                    }
113                },
114                "spouse" => {
115                    members.entry(character).or_default().spouse = related;
116                    members.entry(related).or_default().spouse = character;
117                },
118                _ => {},
119            }
120        }
121        Ok(members)
122    }
123
124    /// Writes a family member. Dead table characters carry their faction and details; generated characters only
125    /// their id.
126    fn write_member(&self, id: u32, entry: Option<&CharacterEntry>, member: &Member) -> Result<RecordNode> {
127        let layout = self.inputs.layout;
128        let path = child_path(FAMILY_TREE_PATH, "FAMILY_MEMBER");
129        let date = |year: u32| -> Result<NodeType> {
130            let mut record = layout.builder(&child_path(&path, "DATE"))?;
131            record.group()?.set_u32("^+1", year)?;
132            Ok(NodeType::Record(Box::new(record.build()?)))
133        };
134        let list = |ids: &[u32]| std::iter::once(u32_node(ids.len() as u32)).chain(ids.iter().map(|id| u32_node(*id))).collect::<Vec<_>>();
135
136        let dead = entry.filter(|entry| !entry.death_type().is_empty());
137        let mut children = vec![u32_node(id), bool_node(dead.is_some())];
138        if let Some(entry) = dead {
139            let faction = self.inputs.index.faction(entry.faction()).ok_or_else(|| missing_faction(entry))?;
140            children.push(NodeType::Ascii(faction.key().to_owned()));
141            children.push(NodeType::Record(Box::new(self.write_dead_details(id, entry, faction, &path)?)));
142        }
143
144        let married = member.spouse != 0 || !member.former_spouses.is_empty();
145        let adult = entry.filter(|entry| entry.death_type().is_empty()).is_none_or(|entry| entry.age() >= ADULT_AGE);
146        children.extend([u32_node(0), u32_node(member.mother), u32_node(member.father), u32_node(member.spouse)]);
147        children.push(date(if married { self.start_year } else { 0 })?);
148        children.extend(list(&member.former_spouses));
149        children.extend(list(&member.children));
150        children.extend(list(&member.adopted_children));
151        children.extend([bool_node(false), date(0)?, u32_node(0), bool_node(adult), bool_node(false), bool_node(member.bastard), bool_node(false)]);
152        children.extend([u32_node(0), u32_node(u32::MAX), bool_node(false), bool_node(entry.is_some_and(|entry| entry.progenitor())), bool_node(member.adopted)]);
153        children.push(NodeType::Ascii(String::new()));
154        layout.raw_record(&path, children)
155    }
156
157    /// Writes the details of a dead table character: born and died the given number of years before the start.
158    fn write_dead_details(&self, family_id: u32, entry: &CharacterEntry, faction: &FactionEntry, member_path: &str) -> Result<RecordNode> {
159        let row = self.rows.get(entry.id());
160        // The column is an optional string.
161        let died_before = row.map(|row| row.str("turns_died_before_start")).transpose()?.and_then(|value| value.parse::<i64>().ok()).unwrap_or_default();
162        let death_year = u32::try_from(i64::from(self.start_year) - died_before).unwrap_or_default();
163        let info = CharacterInfo {
164            agent: entry.agent().to_owned(),
165            is_male: row.map(|row| row.bool("is_male")).transpose()?.unwrap_or(true),
166            birth_year: u32::try_from(i64::from(death_year) - entry.age()).unwrap_or_default(),
167            row,
168        };
169        let code = DEATH_TYPE_CODES.iter().find(|(death_type, _)| *death_type == entry.death_type()).map(|(_, code)| *code).unwrap_or_default();
170        let path = child_path(member_path, "CHARACTER_DETAILS");
171        let subject = DetailsSubject { path: &path, faction, origin: faction, entry: Some(entry), character_id: 0, family_id, death: Some((death_year, code)), bodyguard: None };
172        self.write_details(&subject, &info)
173    }
174}
175
176/// Builds the error for a character whose faction isn't in the campaign.
177fn missing_faction(entry: &CharacterEntry) -> RLibError {
178    RLibError::StartposInvalidValue("start_pos_characters".to_owned(), "faction".to_owned(), entry.faction().to_owned())
179}