1use 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
40const CHARACTER_ARRAY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY";
42
43const CHARACTER_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY/CHARACTER";
45
46const DETAILS_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY/CHARACTER/CHARACTER_DETAILS";
48
49
50pub(crate) const COLONEL_AGENT: &str = "colonel";
52
53pub(crate) const GENERAL_AGENT: &str = "general";
55
56const COLONEL_AGE: u32 = 30;
58
59pub(crate) const NAME_KEY_PREFIX: &str = "names_name_";
61
62const ART_SET_SUFFIX: &str = "0";
64
65const LEADER_VALUE: u32 = 4;
67
68pub struct CharacterWriter<'a, 'b> {
74
75 inputs: &'b WorldInputs<'a>,
77
78 forces: &'b ForcePlan<'a>,
80
81 posts: &'b PostPlan,
83
84 sight: &'b SightPlan,
86
87 rows: HashMap<String, Row<'a>>,
89
90 traits: HashMap<String, Vec<(String, i64)>>,
92
93 agents: HashMap<String, (u32, f32)>,
95
96 art_sets: Vec<ArtSet>,
98
99 bodyguards: HashMap<String, Option<Bodyguard>>,
101
102 parties: HashMap<String, Vec<PoliticalParty>>,
104
105 family_ids: Vec<u32>,
107
108 entry_family_ids: HashMap<String, u32>,
110
111 start_year: u32,
113}
114
115#[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
126struct DetailsSubject<'r> {
128
129 path: &'r str,
131
132 faction: &'r FactionEntry,
134
135 origin: &'r FactionEntry,
137
138 entry: Option<&'r CharacterEntry>,
140
141 character_id: u32,
143
144 family_id: u32,
146
147 death: Option<(u32, i32)>,
149
150 bodyguard: Option<Bodyguard>,
152}
153
154struct CharacterInfo<'r> {
156 agent: String,
157 is_male: bool,
158 birth_year: u32,
159 row: Option<&'r Row<'r>>,
160}
161
162impl<'a, 'b> CharacterWriter<'a, 'b> {
167
168 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(®ion)));
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(®ion)),
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
536fn art_set_agent(agent: &str) -> &str {
538 if agent == COLONEL_AGENT { GENERAL_AGENT } else { agent }
539}
540
541fn 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
570pub(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}