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