Skip to main content

rpfm_extensions/startpos/world/
provinces.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//! Per-faction managers of provinces (`PROVINCE/FACTION_PROVINCE_MANAGER_ARRAY`) and sea regions
12//! (`WORLD/SEA_REGION_MANAGER`).
13//!
14//! Verified on Attila's main campaign: 94 of 94 province managers match in their regions and 93 in their main region,
15//! and 25 of 25 sea regions match. The game lists managers and regions in orders derived from its hash-ordered region
16//! array; these functions use table and map order instead, which changes the main region of provinces without capital
17//! or major settlement (one rebel province in Attila).
18
19use getset::Getters;
20
21use std::collections::{BTreeMap, HashMap};
22
23use rpfm_lib::error::Result;
24
25use super::CampaignIndex;
26use crate::startpos::region_map::RegionMap;
27use crate::startpos::settlements::SettlementLayout;
28use crate::startpos::tables::StartposTables;
29
30#[cfg(test)] #[path = "provinces_test.rs"] mod provinces_test;
31
32/// Position of the port slot in a settlement layout.
33const PORT_SLOT: usize = 1;
34
35//---------------------------------------------------------------------------//
36//                              Types
37//---------------------------------------------------------------------------//
38
39/// A faction's holdings in a province.
40#[derive(Debug, Clone, PartialEq, Eq, Default, Getters)]
41#[getset(get = "pub")]
42pub struct ProvinceManager {
43
44    /// Province key.
45    province: String,
46
47    /// Faction key, `None` for the rebels.
48    faction: Option<String>,
49
50    /// Keys of the faction's regions in the province, in `region_to_province_junctions` order.
51    regions: Vec<String>,
52
53    /// The faction's main region in the province: its capital if there, else the major settlement, else the first in
54    /// table order.
55    main_region: String,
56}
57
58/// A sea region and the factions with ports on it.
59#[derive(Debug, Clone, PartialEq, Eq, Default, Getters)]
60#[getset(get = "pub")]
61pub struct SeaRegion {
62
63    /// Sea region key.
64    key: String,
65
66    /// Port regions on the sea, grouped by faction (`None` for the rebels), sorted by region key.
67    ports: Vec<(Option<String>, Vec<String>)>,
68}
69
70//---------------------------------------------------------------------------//
71//                              Implementations
72//---------------------------------------------------------------------------//
73
74/// Returns the province managers: one per faction (or the rebels) owning regions in a province.
75///
76/// # Arguments
77///
78/// * `tables` - Tables with `region_to_province_junctions`.
79/// * `index` - The campaign's startpos rows.
80///
81/// # Returns
82///
83/// The managers, by province in table order, then by faction in order of their first region.
84///
85/// # Errors
86///
87/// Returns an error if a table or column is missing.
88pub fn province_managers(tables: &StartposTables, index: &CampaignIndex) -> Result<Vec<ProvinceManager>> {
89    let mut managers = Vec::<ProvinceManager>::new();
90    for row in tables.table("region_to_province_junctions")?.rows() {
91        let Some(region) = index.region(&row.str("region")?) else { continue };
92        let province = row.str("province")?;
93        let faction = region.owner().to_owned();
94
95        match managers.iter_mut().find(|manager| manager.province == province && manager.faction == faction) {
96            Some(manager) => manager.regions.push(region.key().to_owned()),
97            None => managers.push(ProvinceManager {
98                province: province.into_owned(),
99                faction,
100                regions: vec![region.key().to_owned()],
101                main_region: String::new(),
102            }),
103        }
104    }
105
106    for manager in &mut managers {
107        let capital = manager.faction.as_deref().and_then(|faction| index.capital_of(faction)).map(|capital| capital.key().as_str());
108        let is_major = |region: &str| index.settlement_of(region).is_some_and(|settlement| settlement.is_major());
109        manager.main_region = manager.regions.iter()
110            .find(|region| Some(region.as_str()) == capital)
111            .or_else(|| manager.regions.iter().find(|region| is_major(region)))
112            .or_else(|| manager.regions.first())
113            .cloned()
114            .unwrap_or_default();
115    }
116
117    Ok(managers)
118}
119
120/// Returns the sea regions with ports: every sea a port zone cell lies in, with the port regions of each faction.
121///
122/// # Arguments
123///
124/// * `map` - Region map of the campaign map.
125/// * `layouts` - Settlement layouts of the campaign map (their port slot zones).
126/// * `index` - The campaign's startpos rows.
127///
128/// # Returns
129///
130/// The seas in map order. Seas without ports (rivers, lakes) are left out.
131pub fn sea_regions(map: &RegionMap, layouts: &[SettlementLayout], index: &CampaignIndex) -> Vec<SeaRegion> {
132    let mut seas = BTreeMap::<usize, BTreeMap<String, Option<String>>>::new();
133    let map_position = map.keys().iter().enumerate().map(|(position, key)| (key.as_str(), position)).collect::<HashMap<_, _>>();
134
135    for layout in layouts {
136        let Some(region) = index.region(layout.region()) else { continue };
137        for cell in layout.slots().get(PORT_SLOT).map(|slot| slot.zone().as_slice()).unwrap_or_default() {
138            let Some(sea) = map.region_at(*cell) else { continue };
139            if index.region(sea).is_some() {
140                continue;
141            }
142            if let Some(&position) = map_position.get(sea) {
143                seas.entry(position).or_default().insert(region.key().to_owned(), region.owner().to_owned());
144            }
145        }
146    }
147
148    seas.into_iter().map(|(position, ports)| {
149        let mut by_faction = Vec::<(Option<String>, Vec<String>)>::new();
150        for (region, faction) in ports {
151            match by_faction.iter_mut().find(|(owner, _)| *owner == faction) {
152                Some((_, regions)) => regions.push(region),
153                None => by_faction.push((faction, vec![region])),
154            }
155        }
156        SeaRegion { key: map.keys()[position].to_owned(), ports: by_faction }
157    }).collect()
158}