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 walks `region_to_province_junctions` in key order ([`junction_rows`]),
16//! which gives the order of the regions of each manager; the order of the factions' managers within a province isn't
17//! reproduced yet, and sea regions use map order.
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::{Row, 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 [`junction_rows`] 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 /// [`junction_rows`] 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 rows of `region_to_province_junctions` in the order the game walks them: the key order of the
75/// province key followed by the region key (see [`crate::startpos::tables::Table::rows_in_key_order_by`]).
76///
77/// # Errors
78///
79/// Returns an error if the table or a column is missing.
80pub fn junction_rows(tables: &StartposTables) -> Result<Vec<Row<'_>>> {
81 tables.table("region_to_province_junctions")?.rows_in_key_order_by(|row| Ok(format!("{}{}", row.str("province")?, row.str("region")?)))
82}
83
84/// Returns the province managers: one per faction (or the rebels) owning regions in a province.
85///
86/// # Arguments
87///
88/// * `tables` - Tables with `region_to_province_junctions`.
89/// * `index` - The campaign's startpos rows.
90///
91/// # Returns
92///
93/// The managers, by province in [`junction_rows`] order, then by faction in order of their first region.
94///
95/// # Errors
96///
97/// Returns an error if a table or column is missing.
98pub fn province_managers(tables: &StartposTables, index: &CampaignIndex) -> Result<Vec<ProvinceManager>> {
99 let mut managers = Vec::<ProvinceManager>::new();
100 for row in junction_rows(tables)? {
101 let Some(region) = index.region(&row.str("region")?) else { continue };
102 let province = row.str("province")?;
103 let faction = region.owner().to_owned();
104
105 match managers.iter_mut().find(|manager| manager.province == province && manager.faction == faction) {
106 Some(manager) => manager.regions.push(region.key().to_owned()),
107 None => managers.push(ProvinceManager {
108 province: province.into_owned(),
109 faction,
110 regions: vec![region.key().to_owned()],
111 main_region: String::new(),
112 }),
113 }
114 }
115
116 for manager in &mut managers {
117 let capital = manager.faction.as_deref().and_then(|faction| index.capital_of(faction)).map(|capital| capital.key().as_str());
118 let is_major = |region: &str| index.settlement_of(region).is_some_and(|settlement| settlement.is_major());
119 manager.main_region = manager.regions.iter()
120 .find(|region| Some(region.as_str()) == capital)
121 .or_else(|| manager.regions.iter().find(|region| is_major(region)))
122 .or_else(|| manager.regions.first())
123 .cloned()
124 .unwrap_or_default();
125 }
126
127 Ok(managers)
128}
129
130/// Returns the sea regions with ports: every sea a port zone cell lies in, with the port regions of each faction.
131///
132/// # Arguments
133///
134/// * `map` - Region map of the campaign map.
135/// * `layouts` - Settlement layouts of the campaign map (their port slot zones).
136/// * `index` - The campaign's startpos rows.
137///
138/// # Returns
139///
140/// The seas in map order. Seas without ports (rivers, lakes) are left out.
141pub fn sea_regions(map: &RegionMap, layouts: &[SettlementLayout], index: &CampaignIndex) -> Vec<SeaRegion> {
142 let mut seas = BTreeMap::<usize, BTreeMap<String, Option<String>>>::new();
143 let map_position = map.keys().iter().enumerate().map(|(position, key)| (key.as_str(), position)).collect::<HashMap<_, _>>();
144
145 for layout in layouts {
146 let Some(region) = index.region(layout.region()) else { continue };
147 for cell in layout.slots().get(PORT_SLOT).map(|slot| slot.zone().as_slice()).unwrap_or_default() {
148 let Some(sea) = map.region_at(*cell) else { continue };
149 if index.region(sea).is_some() {
150 continue;
151 }
152 if let Some(&position) = map_position.get(sea) {
153 seas.entry(position).or_default().insert(region.key().to_owned(), region.owner().to_owned());
154 }
155 }
156 }
157
158 seas.into_iter().map(|(position, ports)| {
159 let mut by_faction = Vec::<(Option<String>, Vec<String>)>::new();
160 for (region, faction) in ports {
161 match by_faction.iter_mut().find(|(owner, _)| *owner == faction) {
162 Some((_, regions)) => regions.push(region),
163 None => by_faction.push((faction, vec![region])),
164 }
165 }
166 SeaRegion { key: map.keys()[position].to_owned(), ports: by_faction }
167 }).collect()
168}