1use getset::{CopyGetters, Getters};
38
39use std::collections::{HashMap, HashSet};
40
41use rpfm_lib::error::{RLibError, Result};
42
43use super::tables::{Row, StartposTables};
44use self::availability::available_chains;
45
46pub mod agents;
47pub mod availability;
48pub mod bodyguards;
49pub mod calendar;
50pub mod characters;
51pub mod diplomacy;
52pub mod estates;
53pub mod factions;
54pub mod garrisons;
55pub mod government;
56pub mod legacy;
57pub mod pools;
58pub mod provinces;
59pub mod rebels;
60pub mod unit_bonuses;
61pub mod writer;
62
63#[cfg(test)] pub(crate) mod world_test;
64
65const SPOUSE_RELATIONSHIP: &str = "spouse";
67
68pub const NAME_COLUMNS: [&str; 4] = ["Name", "Surname", "clan_name", "other_name"];
71
72const MAJOR_SETTLEMENT_INSTANCES: [&str; 2] = ["att_city_major", "vik_settlement"];
75
76#[derive(Debug, Clone, Default, Getters)]
82#[getset(get = "pub")]
83pub struct CampaignIndex {
84
85 campaign: String,
87
88 factions: Vec<FactionEntry>,
90
91 regions: Vec<RegionEntry>,
93
94 settlements: Vec<SettlementEntry>,
96
97 characters: Vec<CharacterEntry>,
99
100 #[getset(skip)]
102 faction_positions: HashMap<String, usize>,
103
104 #[getset(skip)]
106 region_positions: HashMap<String, usize>,
107}
108
109#[derive(Debug, Clone, PartialEq, Default, Getters, CopyGetters)]
111pub struct FactionEntry {
112
113 #[getset(get = "pub")]
115 id: String,
116
117 #[getset(get = "pub")]
119 key: String,
120
121 #[getset(get_copy = "pub")]
123 playable: bool,
124
125 #[getset(get_copy = "pub")]
127 starting_order: i64,
128
129 #[getset(get = "pub")]
131 subculture: String,
132
133 #[getset(get = "pub")]
135 culture: String,
136
137 #[getset(get = "pub")]
139 military_group: String,
140}
141
142#[derive(Debug, Clone, PartialEq, Default, Getters, CopyGetters)]
144pub struct RegionEntry {
145
146 #[getset(get = "pub")]
148 id: String,
149
150 #[getset(get = "pub")]
152 key: String,
153
154 #[getset(get = "pub")]
156 owner: Option<String>,
157
158 #[getset(get_copy = "pub")]
160 capital_flag: bool,
161}
162
163#[derive(Debug, Clone, PartialEq, Default, Getters)]
165#[getset(get = "pub")]
166pub struct SettlementEntry {
167
168 id: String,
170
171 region: String,
173
174 primary_building: String,
176
177 port_building: String,
179
180 buildings: Vec<String>,
182
183 slave_points: i64,
185
186 #[getset(skip)]
188 major: bool,
189}
190
191#[derive(Debug, Clone, PartialEq, Default, Getters, CopyGetters)]
193pub struct CharacterEntry {
194
195 #[getset(get = "pub")]
197 id: String,
198
199 #[getset(get = "pub")]
201 faction: String,
202
203 #[getset(get = "pub")]
205 agent: String,
206
207 #[getset(get_copy = "pub")]
209 age: i64,
210
211 #[getset(get_copy = "pub")]
213 is_male: bool,
214
215 #[getset(get = "pub")]
218 names: Vec<String>,
219
220 #[getset(get_copy = "pub")]
222 start_x: u16,
223
224 #[getset(get_copy = "pub")]
226 start_y: u16,
227
228 #[getset(get = "pub")]
230 ministerial_position: String,
231
232 #[getset(get_copy = "pub")]
234 loyalty: i64,
235
236 #[getset(get = "pub")]
238 death_type: String,
239
240 #[getset(get_copy = "pub")]
242 progenitor: bool,
243
244 #[getset(get = "pub")]
246 override_general_unit: String,
247
248 #[getset(get_copy = "pub")]
250 widowed: bool,
251
252 #[getset(get = "pub")]
255 husband_faction: Option<String>,
256}
257
258impl CampaignIndex {
263
264 pub fn new(tables: &StartposTables, campaign: &str) -> Result<Self> {
276 let cultures = tables.table("cultures_subcultures")?.rows()
277 .map(|row| Ok((row.str("subculture")?.into_owned(), row.str("culture")?.into_owned())))
278 .collect::<Result<HashMap<_, _>>>()?;
279 let game_factions = tables.table("factions")?.rows()
280 .map(|row| Ok((row.str("key")?.into_owned(), row)))
281 .collect::<Result<HashMap<_, _>>>()?;
282
283 let mut factions = vec![];
284 for row in tables.table("start_pos_factions")?.rows() {
285 if row.str("campaign")? != campaign {
286 continue;
287 }
288 let key = row.str("faction")?.into_owned();
289 let game = game_factions.get(&key).ok_or_else(|| RLibError::StartposInvalidValue("start_pos_factions".to_owned(), "faction".to_owned(), key.to_owned()))?;
290 let subculture = game.str("subculture")?.into_owned();
291 factions.push(FactionEntry {
292 id: row.str("ID")?.into_owned(),
293 playable: row.bool("playable")?,
294 starting_order: row.int("starting_order")?,
295 culture: cultures.get(&subculture).cloned().unwrap_or_default(),
296 military_group: game.str("military_group")?.into_owned(),
297 subculture,
298 key,
299 });
300 }
301 let faction_of_id = factions.iter().map(|faction| (faction.id.to_owned(), faction.key.to_owned())).collect::<HashMap<_, _>>();
302
303 let mut regions = vec![];
304 for row in tables.table("start_pos_regions")?.rows() {
305 if row.str("campaign")? != campaign {
306 continue;
307 }
308 regions.push(RegionEntry {
309 id: row.str("id")?.into_owned(),
310 key: row.str("region")?.into_owned(),
311 owner: faction_of_id.get(row.str("owning_faction")?.as_ref()).cloned(),
312 capital_flag: row.bool("faction_capital")?,
313 });
314 }
315 let region_of_id = regions.iter().map(|region| (region.id.to_owned(), region.key.to_owned())).collect::<HashMap<_, _>>();
316
317 let major_buildings = tables.table("building_levels")?.rows()
318 .filter(|row| row.str("building_instance_key").is_ok_and(|instance| MAJOR_SETTLEMENT_INSTANCES.contains(&instance.as_ref())))
319 .map(|row| row.str("level_name").map(|level| level.into_owned()))
320 .collect::<Result<HashSet<_>>>()?;
321
322 let mut available = HashMap::new();
324 for faction in &factions {
325 if let Some(chains) = available_chains(tables, campaign, faction)? {
326 available.insert(faction.key.to_owned(), chains);
327 }
328 }
329 let mut chain_of = HashMap::new();
330 if !available.is_empty() {
331 for row in tables.table("building_levels")?.rows() {
332 chain_of.insert(row.str("level_name")?.into_owned(), row.str("chain")?.into_owned());
333 }
334 }
335 let owner_of = regions.iter().map(|region| (region.key.as_str(), region.owner.as_deref())).collect::<HashMap<_, _>>();
336
337 let mut settlements = vec![];
338 for row in tables.table("start_pos_settlements")?.rows() {
339 let Some(region) = region_of_id.get(row.str("region")?.as_ref()) else { continue };
340 let chains = owner_of.get(region.as_str()).copied().flatten().and_then(|owner| available.get(owner));
341 let allowed = |building: &str| chains.is_none_or(|chains| chain_of.get(building).is_some_and(|chain| chains.contains(chain)));
342
343 let mut buildings = vec![];
344 for column in ["building1", "building2", "building3", "building4", "building5"] {
345 let building = row.str(column)?;
346 if !building.is_empty() && allowed(&building) {
347 buildings.push(building.into_owned());
348 }
349 }
350 let port_building = row.str("port_building")?;
351 let primary_building = row.str("primary_building")?.into_owned();
352 settlements.push(SettlementEntry {
353 id: row.str("id")?.into_owned(),
354 region: region.to_owned(),
355 major: major_buildings.contains(&primary_building),
356 primary_building,
357 port_building: if allowed(&port_building) { port_building.into_owned() } else { String::new() },
358 buildings,
359 slave_points: row.int("startpos_slave_points")?,
360 });
361 }
362
363 let name_kinds = name_kinds(tables)?;
364 let mut characters = vec![];
365 let mut women = HashSet::new();
366 for row in tables.table("start_pos_characters")?.rows() {
367 let Some(faction) = faction_of_id.get(row.str("faction")?.as_ref()) else { continue };
368 let is_male = !row.bool("is_male").is_ok_and(|is_male| !is_male);
369 if !is_male {
370 women.insert(row.str("ID")?.into_owned());
371 }
372 characters.push(CharacterEntry {
373 id: row.str("ID")?.into_owned(),
374 faction: faction.to_owned(),
375 agent: row.str("Type")?.into_owned(),
376 age: row.int("Age")?,
377 is_male,
378 names: character_names(&row, name_kinds.as_ref())?,
379 start_x: cell_coordinate(&row, "startx")?,
380 start_y: cell_coordinate(&row, "starty")?,
381 ministerial_position: row.str("ministerial_position")?.into_owned(),
382 loyalty: row.int("loyalty")?,
383 death_type: row.str("death_type")?.into_owned(),
384 progenitor: row.bool("progenitor")?,
385 override_general_unit: row.str("override_general_unit")?.into_owned(),
386 husband_faction: None,
387 widowed: false,
388 });
389 }
390
391 let husband_factions = husband_factions(tables, &characters, &women)?;
392 let widowed = widowed(tables, &characters)?;
393 for character in &mut characters {
394 character.husband_faction = husband_factions.get(&character.id).cloned();
395 character.widowed = widowed.contains(&character.id);
396 }
397
398 Ok(Self {
399 campaign: campaign.to_owned(),
400 faction_positions: factions.iter().enumerate().map(|(index, faction)| (faction.key.to_owned(), index)).collect(),
401 region_positions: regions.iter().enumerate().map(|(index, region)| (region.key.to_owned(), index)).collect(),
402 factions,
403 regions,
404 settlements,
405 characters,
406 })
407 }
408
409 pub fn faction(&self, key: &str) -> Option<&FactionEntry> {
411 self.faction_positions.get(key).map(|&index| &self.factions[index])
412 }
413
414 pub fn faction_by_id(&self, id: &str) -> Option<&FactionEntry> {
416 self.factions.iter().find(|faction| faction.id == id)
417 }
418
419 pub fn region(&self, key: &str) -> Option<&RegionEntry> {
421 self.region_positions.get(key).map(|&index| &self.regions[index])
422 }
423
424 pub fn regions_of<'a>(&'a self, faction: &'a str) -> impl Iterator<Item = &'a RegionEntry> + 'a {
426 self.regions.iter().filter(move |region| region.owner.as_deref() == Some(faction))
427 }
428
429 pub fn capital_of<'a>(&'a self, faction: &'a str) -> Option<&'a RegionEntry> {
431 self.regions_of(faction).find(|region| region.capital_flag)
432 }
433
434 pub fn settlement_of(&self, region: &str) -> Option<&SettlementEntry> {
436 self.settlements.iter().find(|settlement| settlement.region == region)
437 }
438
439 pub fn characters_of<'a>(&'a self, faction: &'a str) -> impl Iterator<Item = &'a CharacterEntry> + 'a {
441 self.characters.iter().filter(move |character| character.faction == faction)
442 }
443
444 pub fn factions_in_startpos_order(&self) -> Vec<&FactionEntry> {
449 let mut factions = self.factions.iter().collect::<Vec<_>>();
450 factions.sort_by_key(|faction| (!faction.playable, faction.starting_order));
451 factions
452 }
453}
454
455impl SettlementEntry {
456
457 pub fn is_major(&self) -> bool {
460 self.major
461 }
462
463 pub fn all_buildings(&self) -> impl Iterator<Item = &str> {
465 std::iter::once(self.primary_building.as_str())
466 .chain(std::iter::once(self.port_building.as_str()))
467 .chain(self.buildings.iter().map(String::as_str))
468 .filter(|building| !building.is_empty())
469 }
470}
471
472fn husband_factions(tables: &StartposTables, characters: &[CharacterEntry], women: &HashSet<String>) -> Result<HashMap<String, String>> {
474 let Ok(relationships) = tables.table("start_pos_family_relationships") else { return Ok(HashMap::new()) };
475 let faction_of = characters.iter().map(|character| (character.id.as_str(), character.faction.as_str())).collect::<HashMap<_, _>>();
476
477 let mut moves = HashMap::new();
478 for row in relationships.rows() {
479 if row.str("relationship")? != SPOUSE_RELATIONSHIP {
480 continue;
481 }
482 let (character, related) = (row.str("character")?, row.str("related_to")?);
483 for (wife, husband) in [(&character, &related), (&related, &character)] {
484 if !women.contains(wife.as_ref()) || women.contains(husband.as_ref()) {
485 continue;
486 }
487 if let (Some(wife_faction), Some(husband_faction)) = (faction_of.get(wife.as_ref()), faction_of.get(husband.as_ref())) {
488 if wife_faction != husband_faction {
489 moves.insert(wife.to_string(), husband_faction.to_string());
490 }
491 }
492 }
493 }
494 Ok(moves)
495}
496
497fn widowed(tables: &StartposTables, characters: &[CharacterEntry]) -> Result<HashSet<String>> {
499 let Ok(relationships) = tables.table("start_pos_family_relationships") else { return Ok(HashSet::new()) };
500 let dead = characters.iter().filter(|character| !character.death_type.is_empty()).map(|character| character.id.as_str()).collect::<HashSet<_>>();
501 let living = characters.iter().filter(|character| character.death_type.is_empty()).map(|character| character.id.as_str()).collect::<HashSet<_>>();
502
503 let mut widowed = HashSet::new();
504 for row in relationships.rows() {
505 if row.str("relationship")? != SPOUSE_RELATIONSHIP {
506 continue;
507 }
508 let (character, related) = (row.str("character")?, row.str("related_to")?);
509 for (survivor, spouse) in [(&character, &related), (&related, &character)] {
510 if living.contains(survivor.as_ref()) && dead.contains(spouse.as_ref()) {
511 widowed.insert(survivor.to_string());
512 }
513 }
514 }
515 Ok(widowed)
516}
517
518fn name_kinds(tables: &StartposTables) -> Result<Option<HashMap<String, HashSet<i64>>>> {
520 let Ok(names) = tables.table("names") else { return Ok(None) };
521 let mut kinds = HashMap::<String, HashSet<i64>>::new();
522 for row in names.rows() {
523 kinds.entry(row.str("id")?.into_owned()).or_default().insert(row.int("type")?);
524 }
525 Ok(Some(kinds))
526}
527
528fn character_names(row: &Row, kinds: Option<&HashMap<String, HashSet<i64>>>) -> Result<Vec<String>> {
530 NAME_COLUMNS.iter().zip(0..).map(|(column, kind)| {
531 let name = row.str(column)?;
532 let fits = kinds.is_none_or(|kinds| kinds.get(name.as_ref()).is_some_and(|name_kinds| name_kinds.contains(&kind)));
533 Ok(if fits { name.into_owned() } else { String::new() })
534 }).collect()
535}
536
537fn cell_coordinate(row: &Row, column: &str) -> Result<u16> {
539 let value = row.f32(column)?;
540 if !(0.0..=f32::from(u16::MAX)).contains(&value) {
541 return Err(RLibError::StartposInvalidValue("start_pos_characters".to_owned(), column.to_owned(), value.to_string()));
542 }
543 Ok(value as u16)
544}