1use std::collections::HashMap;
17
18use rpfm_lib::error::Result;
19use rpfm_lib::files::esf::{NodeType, RecordNode};
20
21use super::sight::{write_line_of_sight, SightPlan};
22use super::{child, horde_details, next_id, WorldInputs, WorldPlan, REBEL_FACTION_ID, WORLD_PATH};
23use crate::startpos::layout::child_path;
24use crate::startpos::tables::{hash_set_order, hash_set_order_by, Row, StartposTables};
25use crate::startpos::vision::Vision;
26use crate::startpos::world::pools::{province_mercenary_pool, MercenaryUnit};
27use crate::startpos::world::provinces::{junction_rows, province_managers, sea_regions, ProvinceManager, SeaRegion};
28
29const PROVINCE_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/PROVINCE_MANAGER/PROVINCE_ARRAY/PROVINCE";
31
32const PROVINCE_MANAGER_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/PROVINCE_MANAGER/PROVINCE_ARRAY/PROVINCE/FACTION_PROVINCE_MANAGER_ARRAY/FACTION_PROVINCE_MANAGER";
34
35const SEA_REGION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/SEA_REGION_MANAGER/SEA_REGIONS_BLOCK/SEA_REGION";
37
38const SEA_MANAGER_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/SEA_REGION_MANAGER/SEA_REGIONS_BLOCK/SEA_REGION/FACTION_SEA_REGION_MANAGER_BLOCK/FACTION_SEA_REGION_MANAGER";
40
41const FACTION_HASH_KEY: u32 = 0x4a54_5eed;
43
44const MAX_PUBLIC_ORDER: i64 = 100;
46
47const HAPPINESS_THRESHOLDS: [i64; 6] = [80, 50, 20, -20, -50, -80];
50
51struct ProvinceWriter<'a, 'b> {
57
58 inputs: &'b WorldInputs<'a>,
60
61 plan: &'b WorldPlan<'a>,
63
64 sight: &'b SightPlan,
66
67 regions: HashMap<String, Row<'a>>,
69
70 next_recruitment: u32,
72
73 next_pool: u32,
75
76 province_ids: HashMap<String, u32>,
78}
79
80#[derive(Debug, Clone)]
82pub struct ProvinceRecords {
83
84 pub provinces: RecordNode,
86
87 pub seas: RecordNode,
89
90 pub next_recruitment: u32,
92
93 pub next_pool: u32,
95
96 pub province_ids: HashMap<String, u32>,
98}
99
100pub fn write_province_and_sea_managers(inputs: &WorldInputs, plan: &WorldPlan, sight: &SightPlan) -> Result<ProvinceRecords> {
114 let mut regions = HashMap::new();
115 for row in inputs.tables.table("start_pos_regions")?.rows() {
116 if row.str("campaign")? == *inputs.index.campaign() {
117 regions.insert(row.str("region")?.into_owned(), row);
118 }
119 }
120
121 let first_recruitment = u32::try_from(horde_details(inputs)?.len()).unwrap_or(u32::MAX).saturating_add(1);
122 let first_pool = u32::try_from(plan.factions().len()).unwrap_or(u32::MAX).saturating_add(REBEL_FACTION_ID + 1);
124 let mut writer = ProvinceWriter { inputs, plan, sight, regions, next_recruitment: first_recruitment, next_pool: first_pool, province_ids: HashMap::new() };
125 let provinces = writer.write_province_manager()?;
126 let seas = writer.write_sea_region_manager()?;
127 Ok(ProvinceRecords {
128 provinces,
129 seas,
130 next_recruitment: writer.next_recruitment,
131 next_pool: writer.next_pool,
132 province_ids: writer.province_ids,
133 })
134}
135
136impl ProvinceWriter<'_, '_> {
137
138 fn write_province_manager(&mut self) -> Result<RecordNode> {
140 let inputs = self.inputs;
141 let managers = province_managers(inputs.tables, inputs.index)?;
142
143 let provinces = campaign_provinces(inputs.tables, self.plan)?;
144
145 let mut next_manager = 1;
146 let mut array = child(inputs.layout, &format!("{WORLD_PATH}/PROVINCE_MANAGER"), "PROVINCE_ARRAY")?;
147 for (index, (province, regions)) in provinces.iter().enumerate() {
148 let province_id = u32::try_from(index + 1).unwrap_or(u32::MAX);
149 self.province_ids.insert(province.to_owned(), province_id);
150 let mut manager_array = child(inputs.layout, PROVINCE_PATH, "FACTION_PROVINCE_MANAGER_ARRAY")?;
151 let province_managers = managers.iter().filter(|manager| manager.province() == province).collect::<Vec<_>>();
153 let factions = hash_set_order_by(province_managers.iter().map(|manager| self.faction_id(manager.faction().as_deref())), |id| id ^ FACTION_HASH_KEY);
154 let ordered = factions.iter().filter_map(|id| province_managers.iter().find(|manager| self.faction_id(manager.faction().as_deref()) == *id).copied()).collect::<Vec<_>>();
155 for manager in ordered {
156 let record = self.write_province_faction_manager(manager, next_id(&mut next_manager))?;
157 manager_array.push_group()?.set_record("FACTION_PROVINCE_MANAGER", Some(record))?;
158 }
159
160 let mut record = self.inputs.layout.builder(PROVINCE_PATH)?;
161 record.group()?
162 .set_u32("^+1", province_id)?
163 .set_record("FACTION_PROVINCE_MANAGER_ARRAY", Some(manager_array.build()?))?
164 .set_record("PROVINCE_MERCENARY_POOL", Some(self.write_province_mercenary_pool(province)?))?
165 .set_record("PROVINCE_FERTILITY", Some(self.write_fertility(regions)?))?;
166 array.push_group()?
167 .set_str("^+1", province)?
168 .set_record("PROVINCE", Some(record.build()?))?;
169 }
170
171 let mut manager = child(inputs.layout, WORLD_PATH, "PROVINCE_MANAGER")?;
172 manager.group()?.set_record("PROVINCE_ARRAY", Some(array.build()?))?;
173 manager.build()
174 }
175
176 fn write_province_faction_manager(&mut self, manager: &ProvinceManager, id: u32) -> Result<RecordNode> {
178 let layout = self.inputs.layout;
179 let public_order = self.sum(manager.regions(), "initial_public_order")?.clamp(-MAX_PUBLIC_ORDER, MAX_PUBLIC_ORDER);
180 let happiness = HAPPINESS_THRESHOLDS.iter().position(|threshold| public_order >= *threshold).unwrap_or(HAPPINESS_THRESHOLDS.len());
181
182 let mut info = child(layout, &format!("{PROVINCE_MANAGER_PATH}/FACTION_PROVINCE_PUBLIC_ORDER"), "FACTION_PROVINCE_PUBLIC_ORDER_INFO")?;
183 info.group()?
184 .set_i32("^+4", i32::try_from(public_order).unwrap_or_default())?
185 .set_i32("^+7", i32::try_from(happiness).unwrap_or_default())?;
186 let mut public_order_record = child(layout, PROVINCE_MANAGER_PATH, "FACTION_PROVINCE_PUBLIC_ORDER")?;
187 public_order_record.group()?.set_record("FACTION_PROVINCE_PUBLIC_ORDER_INFO", Some(info.build()?))?;
188
189 let mut development = child(layout, PROVINCE_MANAGER_PATH, "PROVINCE_DEVELOPMENT")?;
190 development.group()?.set_u32("^+1", u32::try_from(self.sum(manager.regions(), "development_points")?).unwrap_or_default())?;
191
192 let region_ids = manager.regions().iter().map(|region| self.plan.region_id(region).unwrap_or_default()).collect();
193 let mut record = layout.builder(PROVINCE_MANAGER_PATH)?;
194 record.group()?
195 .set_u32("^+1", id)?
196 .set_u32("^+2", self.faction_id(manager.faction().as_deref()))?
197 .set_u32s("^+3", region_ids)?
198 .set_record("RECRUITMENT_MANAGER", Some(self.write_recruitment_manager(PROVINCE_MANAGER_PATH)?))?
199 .set_record("FACTION_PROVINCE_PUBLIC_ORDER", Some(public_order_record.build()?))?
200 .set_record("PROVINCE_DEVELOPMENT", Some(development.build()?))?
201 .set_u32("CAMPAIGN_EFFECT_BUNDLES+4", self.plan.region_id(manager.main_region()).unwrap_or_default())?;
202 record.build()
203 }
204
205 fn write_province_mercenary_pool(&mut self, province: &str) -> Result<RecordNode> {
207 let id = next_id(&mut self.next_pool);
208 let path = format!("{PROVINCE_PATH}/PROVINCE_MERCENARY_POOL");
209 let units = province_mercenary_pool(self.inputs.tables, province)?;
210 let mut record = self.inputs.layout.builder(&path)?;
211 record.group()?
212 .set_u32("^+1", id)?
213 .set_record("MERCENARY_POOL", Some(write_mercenary_pool(self.inputs, &path, &units)?))?;
214 record.build()
215 }
216
217 fn write_fertility(&self, regions: &[String]) -> Result<RecordNode> {
219 let path = format!("{PROVINCE_PATH}/PROVINCE_FERTILITY");
220 let mut block = child(self.inputs.layout, &path, "REGION_BLOCK")?;
221 for region in regions {
222 block.push_group()?.set_str("^+1", region)?;
223 }
224
225 let mut record = self.inputs.layout.builder(&path)?;
226 record.group()?
227 .set_i32("^+1", i32::try_from(self.sum(regions, "base_fertility")?).unwrap_or_default())?
228 .set_record("REGION_BLOCK", Some(block.build()?))?;
229 record.build()
230 }
231
232 fn write_sea_region_manager(&mut self) -> Result<RecordNode> {
234 let inputs = self.inputs;
235 let seas = sea_regions(inputs.region_map, inputs.settlements, inputs.index);
236
237 let mut next_manager = 1;
238 let mut block = child(inputs.layout, &format!("{WORLD_PATH}/SEA_REGION_MANAGER"), "SEA_REGIONS_BLOCK")?;
239 for (index, sea) in seas.iter().enumerate() {
240 let record = self.write_sea_region(sea, u32::try_from(index + 1).unwrap_or(u32::MAX), &mut next_manager)?;
241 block.push_group()?
242 .set_str("^+1", sea.key())?
243 .set_record("SEA_REGION", Some(record))?;
244 }
245
246 let mut manager = child(inputs.layout, WORLD_PATH, "SEA_REGION_MANAGER")?;
247 manager.group()?.set_record("SEA_REGIONS_BLOCK", Some(block.build()?))?;
248 manager.build()
249 }
250
251 fn write_sea_region(&mut self, sea: &SeaRegion, id: u32, next_manager: &mut u32) -> Result<RecordNode> {
253 let layout = self.inputs.layout;
254 let mut managers = child(layout, SEA_REGION_PATH, "FACTION_SEA_REGION_MANAGER_BLOCK")?;
255 let mut ports = sea.ports().iter().collect::<Vec<_>>();
257 ports.sort_by_key(|(_, regions)| regions.iter().filter_map(|region| self.plan.region_id(region)).min());
258 for (faction, regions) in ports {
259 let mut region_ids = regions.iter().map(|region| self.plan.region_id(region).unwrap_or_default()).collect::<Vec<_>>();
260 region_ids.sort_unstable();
261
262 let mut vision = Vision::default();
264 for region in regions {
265 if let Some(settlement) = self.sight.settlement(region) {
266 vision.extend(settlement);
267 }
268 }
269
270 let mut manager = layout.builder(SEA_MANAGER_PATH)?;
271 manager.group()?
272 .set_u32("^+1", next_id(next_manager))?
273 .set_u32("^+2", self.faction_id(faction.as_deref()))?
274 .set_u32s("^+3", region_ids)?
275 .set_record("RECRUITMENT_MANAGER", Some(self.write_recruitment_manager(SEA_MANAGER_PATH)?))?
276 .set_record("LINE_OF_SIGHT", Some(write_line_of_sight(layout, SEA_MANAGER_PATH, self.sight.grid(), Some(&vision))?))?;
277 managers.push_group()?.set_record("FACTION_SEA_REGION_MANAGER", Some(manager.build()?))?;
278 }
279
280 let mut record = layout.builder(SEA_REGION_PATH)?;
281 record.group()?
282 .set_u32("^+1", id)?
283 .set_record("FACTION_SEA_REGION_MANAGER_BLOCK", Some(managers.build()?))?;
284 record.build()
285 }
286
287 fn write_recruitment_manager(&mut self, parent: &str) -> Result<RecordNode> {
289 let mut record = child(self.inputs.layout, parent, "RECRUITMENT_MANAGER")?;
290 record.group()?.set_u32("^+1", next_id(&mut self.next_recruitment))?;
291 record.build()
292 }
293
294 fn faction_id(&self, faction: Option<&str>) -> u32 {
296 faction.and_then(|faction| self.plan.faction_id(faction)).unwrap_or(REBEL_FACTION_ID)
297 }
298
299 fn sum(&self, regions: &[String], column: &str) -> Result<i64> {
301 let mut total = 0;
302 for region in regions {
303 if let Some(row) = self.regions.get(region) {
304 total += row.int(column)?;
305 }
306 }
307 Ok(total)
308 }
309}
310
311pub(crate) fn campaign_provinces(tables: &StartposTables, plan: &WorldPlan) -> Result<Vec<(String, Vec<String>)>> {
325 let mut provinces = Vec::<(String, Vec<String>)>::new();
326 for row in junction_rows(tables)? {
327 let region = row.str("region")?;
328 if plan.region_id(®ion).is_none() {
329 continue;
330 }
331 let province = row.str("province")?;
332 match provinces.iter_mut().find(|(key, _)| *key == province) {
333 Some((_, province_regions)) => province_regions.push(region.into_owned()),
334 None => provinces.push((province.into_owned(), vec![region.into_owned()])),
335 }
336 }
337
338 let order = hash_set_order(provinces.iter().map(|(province, _)| province.as_str())).into_iter()
339 .enumerate()
340 .map(|(index, province)| (province.to_owned(), index))
341 .collect::<HashMap<_, _>>();
342 provinces.sort_by_key(|(province, _)| order.get(province).copied());
343 Ok(provinces)
344}
345
346pub(super) fn write_mercenary_pool(inputs: &WorldInputs, parent: &str, units: &[MercenaryUnit]) -> Result<RecordNode> {
348 let path = format!("{parent}/MERCENARY_POOL");
349 let mut array = child(inputs.layout, &path, "MERCENARY_POOL_UNIT_ARRAY")?;
350
351 let unit_layout = inputs.layout.record(&child_path(&path, "MERCENARY_POOL_UNIT_ARRAY"));
353 let whole_chance = unit_layout.and_then(|layout| layout.leaf_default("^+3")).is_some_and(|default| matches!(default, NodeType::I32(_)));
354 let second_chance = unit_layout.is_some_and(|layout| layout.has_slot("^+12"));
355 for unit in units {
356 let group = array.push_group()?;
357 group.set_str("^+1", unit.unit())?
358 .set_u32("^+2", unit.initial_count())?
359 .set_u32("^+4", unit.max_count())?
360 .set_u32("^+5", unit.max_replenish_per_turn())?
361 .set_u32("^+6", unit.max_count())?
362 .set_u32("^+8", unit.creation_index())?;
363 if whole_chance {
364 let chance = unit.replenish_chance().round() as i32;
365 group.set_i32("^+3", chance)?;
366 if second_chance {
367 group.set_i32("^+12", chance)?;
368 }
369 } else {
370 group.set_f32("^+3", unit.replenish_chance())?;
371 }
372 }
373
374 let mut record = inputs.layout.builder(&path)?;
375 record.group()?.set_record("MERCENARY_POOL_UNIT_ARRAY", Some(array.build()?))?;
376 record.build()
377}