1use 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
46const CHARACTER_ARRAY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY";
48
49const CHARACTER_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY/CHARACTER";
51
52const DETAILS_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/CHARACTER_ARRAY/CHARACTER/CHARACTER_DETAILS";
54
55
56pub(crate) const COLONEL_AGENT: &str = "colonel";
58
59pub(crate) const GENERAL_AGENT: &str = "general";
61
62const COLONEL_AGE: u32 = 30;
64
65pub(crate) const NAME_KEY_PREFIX: &str = "names_name_";
67
68const ART_SET_SUFFIX: &str = "0";
70
71const MAX_LISTED_SKILLS: usize = 2;
73
74const OFFICE_ROUNDS_VARIABLE: &str = "rounds_to_hold_office_before_loss_hits_loyalty";
78
79pub struct CharacterWriter<'a, 'b> {
85
86 inputs: &'b WorldInputs<'a>,
88
89 forces: &'b ForcePlan<'a>,
91
92 posts: &'b PostPlan,
94
95 sight: &'b SightPlan,
97
98 rows: HashMap<String, Row<'a>>,
100
101 traits: HashMap<String, Vec<(String, i64)>>,
103
104 office_rounds: u32,
106
107 loyalty_range: RangeInclusive<i64>,
109
110 skills: HashMap<String, Vec<(String, u32)>>,
113
114 agents: HashMap<String, (u32, f32)>,
116
117 art_sets: Vec<ArtSet>,
119
120 bodyguards: HashMap<String, Option<Bodyguard>>,
122
123 parties: HashMap<String, Vec<PoliticalParty>>,
125
126 family_ids: Vec<u32>,
128
129 entry_family_ids: HashMap<String, u32>,
131
132 start_year: u32,
134}
135
136#[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
148struct DetailsSubject<'r> {
150
151 path: &'r str,
153
154 faction: &'r FactionEntry,
156
157 origin: &'r FactionEntry,
159
160 entry: Option<&'r CharacterEntry>,
162
163 character_id: u32,
165
166 family_id: u32,
168
169 death: Option<(u32, i32)>,
171
172 bodyguard: Option<Bodyguard>,
174}
175
176struct CharacterInfo<'r> {
178 agent: String,
179 is_male: bool,
180 birth_year: u32,
181 row: Option<&'r Row<'r>>,
182}
183
184impl<'a, 'b> CharacterWriter<'a, 'b> {
189
190 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(®ion)));
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(®ion)),
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
599fn art_set_agent(agent: &str) -> &str {
601 if agent == COLONEL_AGENT { GENERAL_AGENT } else { agent }
602}
603
604fn 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
633pub(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
666fn 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
684fn 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}