rpfm_extensions/startpos/world/pools.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//! Mercenary pools (`FACTION/FACTION_MERCENARY_POOL`, `PROVINCE/PROVINCE_MERCENARY_POOL`) and levy pools
12//! (`FACTION/LEVY_MERCENARY_MANAGER`).
13//!
14//! Verified on Attila's main campaign: 118 of 118 faction pools, 62 of 62 province pools and 3 of 3 levy managers
15//! match. The game stores the units of a pool in hash order; these functions return them in table order.
16
17use getset::{CopyGetters, Getters};
18
19use std::collections::{BTreeSet, HashMap};
20
21use rpfm_lib::error::Result;
22
23use super::CampaignIndex;
24use crate::startpos::tables::StartposTables;
25
26#[cfg(test)] #[path = "pools_test.rs"] mod pools_test;
27
28/// Diplomatic stances that make a faction raise levies for its overlord.
29const SUBJECT_STANCES: [&str; 2] = ["vassal", "client_state"];
30
31//---------------------------------------------------------------------------//
32// Types
33//---------------------------------------------------------------------------//
34
35/// A unit of a mercenary pool (`MERCENARY_POOL_UNIT_ARRAY` entry).
36#[derive(Debug, Clone, PartialEq, Default, Getters, CopyGetters)]
37pub struct MercenaryUnit {
38
39 /// Unit key (`mercenary_unit_groups.unit_record`).
40 #[getset(get = "pub")]
41 unit: String,
42
43 /// Units available at the start (`initial_unit_count`).
44 #[getset(get_copy = "pub")]
45 initial_count: u32,
46
47 /// Chance to get a unit back each turn, as stored in the table.
48 #[getset(get_copy = "pub")]
49 replenish_chance: f32,
50
51 /// Maximum units in the pool. The startpos stores it twice.
52 #[getset(get_copy = "pub")]
53 max_count: u32,
54
55 /// Maximum units replenished per turn.
56 #[getset(get_copy = "pub")]
57 max_replenish_per_turn: u32,
58}
59
60/// A levy pool: an empty mercenary pool an overlord gets for each province where a subject owns regions.
61#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Getters)]
62#[getset(get = "pub")]
63pub struct LevyPool {
64
65 /// Overlord faction key.
66 overlord: String,
67
68 /// Subject faction key.
69 subject: String,
70
71 /// Province key.
72 province: String,
73}
74
75//---------------------------------------------------------------------------//
76// Implementations
77//---------------------------------------------------------------------------//
78
79/// Returns the mercenary pool of a faction.
80///
81/// # Arguments
82///
83/// * `tables` - Tables with `faction_to_mercenary_set_junctions`, `mercenary_pool_to_groups_junctions` and
84/// `mercenary_unit_groups`.
85/// * `faction` - Faction key.
86///
87/// # Returns
88///
89/// The units of every set of the faction, in table order. Empty for factions without sets.
90///
91/// # Errors
92///
93/// Returns an error if a table or column is missing.
94pub fn faction_mercenary_pool(tables: &StartposTables, faction: &str) -> Result<Vec<MercenaryUnit>> {
95 mercenary_pool(tables, "faction_to_mercenary_set_junctions", "faction", faction)
96}
97
98/// Returns the mercenary pool of a province.
99///
100/// # Arguments
101///
102/// * `tables` - Tables with `province_to_mercenary_set_junctions`, `mercenary_pool_to_groups_junctions` and
103/// `mercenary_unit_groups`.
104/// * `province` - Province key.
105///
106/// # Returns
107///
108/// The units of every set of the province, in table order. Empty for provinces without sets.
109///
110/// # Errors
111///
112/// Returns an error if a table or column is missing.
113pub fn province_mercenary_pool(tables: &StartposTables, province: &str) -> Result<Vec<MercenaryUnit>> {
114 mercenary_pool(tables, "province_to_mercenary_set_junctions", "province", province)
115}
116
117/// Returns the units of the mercenary sets assigned to an owner by a junction table.
118///
119/// The requirement columns of `mercenary_pool_to_groups_junctions` are blank in vanilla Attila and aren't applied.
120fn mercenary_pool(tables: &StartposTables, junctions: &str, owner_column: &str, owner: &str) -> Result<Vec<MercenaryUnit>> {
121 let mut groups = HashMap::new();
122 for row in tables.table("mercenary_unit_groups")?.rows() {
123 groups.insert(row.str("key")?.into_owned(), row);
124 }
125
126 let pool_rows = tables.table("mercenary_pool_to_groups_junctions")?;
127 let mut units = vec![];
128 for set_row in tables.table(junctions)?.rows() {
129 if set_row.str(owner_column)? != owner {
130 continue;
131 }
132 let set = set_row.str("mercenary_set")?;
133 for row in pool_rows.rows() {
134 if row.str("pool")? != set {
135 continue;
136 }
137 let Some(group) = groups.get(row.str("group")?.as_ref()) else { continue };
138 units.push(MercenaryUnit {
139 unit: group.str("unit_record")?.into_owned(),
140 initial_count: count(row.int("initial_unit_count")?),
141 replenish_chance: group.f32("chance_to_replenish")?,
142 max_count: count(group.int("max_count")?),
143 max_replenish_per_turn: count(group.int("max_replenish_per_turn")?),
144 });
145 }
146 }
147
148 Ok(units)
149}
150
151/// Returns the levy pools of every overlord.
152///
153/// Each `start_pos_diplomacy` row with stance `vassal` or `client_state` (`faction1` the subject, `faction2` the
154/// overlord) gives the overlord one pool per province where the subject owns regions.
155///
156/// # Arguments
157///
158/// * `tables` - Tables with `start_pos_diplomacy` and `region_to_province_junctions`.
159/// * `index` - The campaign's startpos rows.
160///
161/// # Returns
162///
163/// The pools, sorted by overlord, subject and province key. The game sorts them by the startpos ids of the subject
164/// and the province, which aren't known at this stage.
165///
166/// # Errors
167///
168/// Returns an error if a table or column is missing.
169pub fn levy_pools(tables: &StartposTables, index: &CampaignIndex) -> Result<Vec<LevyPool>> {
170 let mut province_of = HashMap::new();
171 for row in tables.table("region_to_province_junctions")?.rows() {
172 province_of.insert(row.str("region")?.into_owned(), row.str("province")?.into_owned());
173 }
174
175 let mut pools = BTreeSet::new();
176 for row in tables.table("start_pos_diplomacy")?.rows() {
177 if !SUBJECT_STANCES.contains(&row.str("stance")?.as_ref()) {
178 continue;
179 }
180 let (Some(subject), Some(overlord)) = (index.faction_by_id(&row.str("faction1")?), index.faction_by_id(&row.str("faction2")?)) else { continue };
181 for region in index.regions_of(subject.key()) {
182 if let Some(province) = province_of.get(region.key()) {
183 pools.insert(LevyPool {
184 overlord: overlord.key().to_owned(),
185 subject: subject.key().to_owned(),
186 province: province.to_owned(),
187 });
188 }
189 }
190 }
191
192 Ok(pools.into_iter().collect())
193}
194
195/// Converts a table count to the unsigned value the startpos stores, clamping negative values to 0.
196fn count(value: i64) -> u32 {
197 u32::try_from(value.max(0)).unwrap_or(u32::MAX)
198}