1use std::collections::HashMap;
14
15use rpfm_lib::error::Result;
16use rpfm_lib::files::esf::RecordNode;
17
18use super::characters::CharacterWriter;
19use super::forces::{ForcePlan, PlannedForce, PlannedUnit};
20use super::regions::{settlement_keys, settlement_name_key};
21use super::{child, horde_details, next_id, start_pos_faction_row, WorldInputs, HORDE_BUILDING_COLUMNS};
22use crate::startpos::world::calendar::Calendar;
23use crate::startpos::world::FactionEntry;
24use crate::startpos::world::factions::faction_summary;
25use crate::startpos::world::legacy::{legacies, legacy_skill_set, ForceKind, Legacy, LegacyForce};
26use crate::startpos::world::unit_bonuses::StatBonusRules;
27
28const FACTION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION";
30
31const ARMY_ARRAY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY";
33
34const FORCE_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE";
36
37const UNIT_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/UNIT_CONTAINER/UNITS_ARRAY/UNIT";
39
40const LEGACY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/MILITARY_FORCE_LEGACY";
42
43const HISTORY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/MILITARY_FORCE_LEGACY/MILITARY_FORCE_LEGACY_HISTORY";
45
46const HORDE_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/MILITARY_FORCE_HORDE_DETAILS";
48
49const HORDE_SLOT_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/MILITARY_FORCE_HORDE_DETAILS/MILITARY_FORCE_SLOT_MANAGER/SLOT_BLOCK/MILITARY_FORCE_SLOT";
51
52const HORDE_PRIMARY_SLOT: &str = "horde_primary";
54
55const HORDE_SECONDARY_SLOT: &str = "horde_secondary";
57
58const HORDE_BUILDING_VALUE: f32 = 1.0;
60
61const GENERAL_ACTION_POINTS_MULTIPLIER: f32 = 1.1;
63
64pub struct ArmyWriter<'a, 'b> {
70
71 inputs: &'b WorldInputs<'a>,
73
74 forces: &'b ForcePlan<'a>,
76
77 residences: &'b HashMap<String, u32>,
79
80 stats: HashMap<String, UnitStats>,
82
83 start_year: u32,
85
86 hordes: HashMap<u32, HordePlan>,
88
89 characters: &'b CharacterWriter<'a, 'b>,
91
92 settlement_keys: HashMap<String, String>,
94
95 provinces: HashMap<String, String>,
97
98 stat_bonuses: StatBonusRules,
100}
101
102#[derive(Debug, Clone, Default)]
104struct HordePlan {
105
106 recruitment_id: u32,
108
109 first_slot_id: u32,
111
112 buildings: Vec<Option<(String, u32)>>,
114
115 development: u32,
117
118 government: String,
120}
121
122#[derive(Debug, Clone, Copy, Default)]
124struct UnitStats {
125
126 men: u32,
128
129 action_points: u32,
131}
132
133impl<'a, 'b> ArmyWriter<'a, 'b> {
138
139 pub fn new(inputs: &'b WorldInputs<'a>, forces: &'b ForcePlan<'a>, characters: &'b CharacterWriter<'a, 'b>, residences: &'b HashMap<String, u32>, late_recruitment: u32) -> Result<Self> {
154 let mut provinces = HashMap::new();
155 for row in inputs.tables.table("region_to_province_junctions")?.rows() {
156 provinces.insert(row.str("region")?.into_owned(), row.str("province")?.into_owned());
157 }
158
159 Ok(Self {
160 hordes: horde_plans(inputs, forces, late_recruitment)?,
161 settlement_keys: settlement_keys(inputs)?,
162 provinces,
163 characters,
164 stats: unit_stats(inputs)?,
165 stat_bonuses: StatBonusRules::new(inputs.tables)?,
166 start_year: start_year(inputs)?,
167 inputs,
168 forces,
169 residences,
170 })
171 }
172
173 pub fn write_army_array(&self, faction: &FactionEntry) -> Result<RecordNode> {
179 let faction_forces = self.forces.forces_of(faction.key()).collect::<Vec<_>>();
180 let force_legacies = self.legacies(faction)?;
181
182 let mut array = self.inputs.layout.builder(ARMY_ARRAY_PATH)?;
183 for (force, legacy) in faction_forces.iter().zip(&force_legacies) {
184 array.push_group()?.set_record("MILITARY_FORCE", Some(self.write_force(force, legacy)?))?;
185 }
186 array.build()
187 }
188
189 fn legacies(&self, faction: &FactionEntry) -> Result<Vec<Legacy>> {
191 let legacy_forces = self.forces.forces_of(faction.key()).map(|force| LegacyForce {
192 kind: force.kind(),
193 general: self.forces.characters()[force.commander()].entry().map(|entry| entry.id().to_owned()),
194 }).collect::<Vec<_>>();
195 legacies(self.inputs.tables, self.inputs.index, faction, &legacy_forces, self.inputs.legacy_overrides)
196 }
197
198 pub fn write_used_legacies(&self, faction: &FactionEntry) -> Result<RecordNode> {
206 let mut used = [vec![], vec![], vec![], vec![]];
207 let mut overrides = false;
208 for (force, legacy) in self.forces.forces_of(faction.key()).zip(self.legacies(faction)?) {
209 let navy = usize::from(force.kind() == ForceKind::Navy);
210 match force.kind() {
211 ForceKind::Army | ForceKind::Navy if legacy.override_name().is_empty() => used[navy].push(legacy.name().to_owned()),
212 ForceKind::Army | ForceKind::Navy => overrides = true,
213 ForceKind::GarrisonArmy | ForceKind::GarrisonNavy => continue,
214 }
215 used[2 + navy].push(legacy.emblem().to_owned());
216 }
217
218 let [army_names, navy_names, army_emblems, navy_emblems] = used;
219 let mut record = child(self.inputs.layout, FACTION_PATH, "MILITARY_FORCE_LEGACY_NAMES_AND_EMBLEMS")?;
220 record.group()?
221 .set_strings("^+3", army_names)?
222 .set_strings("^+6", navy_names)?
223 .set_strings("^+9", army_emblems)?
224 .set_strings("^+12", navy_emblems)?
225 .set_bool("^+13", overrides)?;
226 record.build()
227 }
228
229 fn write_force(&self, force: &PlannedForce, legacy: &Legacy) -> Result<RecordNode> {
231 let layout = self.inputs.layout;
232 let residence = |region: Option<&String>| region.and_then(|region| self.residences.get(region)).copied().unwrap_or_default();
233 let garrison_residence = |kind| residence(force.region().as_ref().filter(|_| force.kind() == kind));
234 let commander_id = self.forces.characters()[force.commander()].id();
235
236 let mut units = child(layout, &format!("{FORCE_PATH}/UNIT_CONTAINER"), "UNITS_ARRAY")?;
237 for (unit, id) in force.units().iter().zip(force.unit_ids()) {
238 let commander = if unit.commands() { commander_id } else { 0 };
239 units.push_group()?.set_record("UNIT", Some(self.write_unit(force, unit, id, commander)?))?;
240 }
241 let mut container = child(layout, FORCE_PATH, "UNIT_CONTAINER")?;
242 container.group()?.set_record("UNITS_ARRAY", Some(units.build()?))?;
243
244 let mut record = layout.builder(FORCE_PATH)?;
245 record.group()?
246 .set_u32("^+1", force.id())?
247 .set_u32("^+2", commander_id)?
248 .set_record("MILITARY_FORCE_LEGACY", Some(self.write_legacy(force, legacy)?))?
249 .set_record("UNIT_CONTAINER", Some(container.build()?))?
250 .set_u32("UNIT_CONTAINER+1", residence(force.stationed_in().as_ref()))?
251 .set_u32("UNIT_CONTAINER+11", garrison_residence(ForceKind::GarrisonArmy))?
252 .set_u32("UNIT_CONTAINER+12", garrison_residence(ForceKind::GarrisonNavy))?
253 .set_bool("UNIT_CONTAINER+13", is_navy(force))?;
254 if let Some(horde) = self.hordes.get(&force.id()) {
255 record.group()?
256 .set_bool("MILITARY_FORCE_MORALE+1", true)?
257 .set_record("MILITARY_FORCE_HORDE_DETAILS", Some(self.write_horde_details(force, horde)?))?;
258 }
259 record.build()
260 }
261
262 fn write_horde_details(&self, force: &PlannedForce, horde: &HordePlan) -> Result<RecordNode> {
264 let layout = self.inputs.layout;
265 let mut recruitment = child(layout, HORDE_PATH, "RECRUITMENT_MANAGER")?;
266 recruitment.group()?.set_u32("^+1", horde.recruitment_id)?;
267
268 let mut slots = child(layout, &format!("{HORDE_PATH}/MILITARY_FORCE_SLOT_MANAGER"), "SLOT_BLOCK")?;
269 for (index, building) in horde.buildings.iter().enumerate() {
270 let primary = index == 0;
271 let base = building.as_ref().map(|(key, id)| {
272 let mut base = child(layout, &format!("{HORDE_SLOT_PATH}/MILITARY_FORCE_BUILDING_MANAGER"), "BUILDING_BASE")?;
273 base.group()?
274 .set_u32("^+1", *id)?
275 .set_str("^+2", key)?
276 .set_f32("^+3", HORDE_BUILDING_VALUE)?
277 .set_f32("^+4", HORDE_BUILDING_VALUE)?
278 .set_str("^+5", force.faction().key())?
279 .set_str("^+6", &horde.government)?;
280 base.build()
281 }).transpose()?;
282
283 let mut manager = child(layout, HORDE_SLOT_PATH, "MILITARY_FORCE_BUILDING_MANAGER")?;
284 manager.group()?
285 .set_bool("^+2", base.is_some())?
286 .set_record("BUILDING_BASE", base)?;
287
288 let slot_type = if primary { HORDE_PRIMARY_SLOT } else { HORDE_SECONDARY_SLOT };
289 let mut slot = layout.builder(HORDE_SLOT_PATH)?;
290 slot.group()?
291 .set_u32("^+1", horde.first_slot_id + u32::try_from(index).unwrap_or_default())?
292 .set_u32("^+2", u32::try_from(index).unwrap_or_default())?
293 .set_record("MILITARY_FORCE_BUILDING_MANAGER", Some(manager.build()?))?
294 .set_str("MILITARY_FORCE_BUILDING_MANAGER+1", slot_type)?
295 .set_str("MILITARY_FORCE_BUILDING_MANAGER+2", slot_type)?
296 .set_bool("MILITARY_FORCE_BUILDING_MANAGER+4", primary)?;
297 slots.push_group()?.set_record("MILITARY_FORCE_SLOT", Some(slot.build()?))?;
298 }
299 let mut slot_manager = child(layout, HORDE_PATH, "MILITARY_FORCE_SLOT_MANAGER")?;
300 slot_manager.group()?.set_record("SLOT_BLOCK", Some(slots.build()?))?;
301
302 let mut development = child(layout, HORDE_PATH, "MILITARY_FORCE_DEVELOPMENT")?;
303 development.group()?.set_u32("^+1", horde.development)?;
304
305 let mut record = layout.builder(HORDE_PATH)?;
306 record.group()?
307 .set_record("RECRUITMENT_MANAGER", Some(recruitment.build()?))?
308 .set_record("MILITARY_FORCE_SLOT_MANAGER", Some(slot_manager.build()?))?
309 .set_record("MILITARY_FORCE_DEVELOPMENT", Some(development.build()?))?;
310 record.build()
311 }
312
313 fn write_legacy(&self, force: &PlannedForce, legacy: &Legacy) -> Result<RecordNode> {
315 let layout = self.inputs.layout;
316 let mut skills = child(layout, LEGACY_PATH, "CAMPAIGN_SKILLS")?;
317 skills.group()?.set_str("^+2", &legacy_skill_set(force.faction(), is_navy(force)))?;
318
319 let mut name = child(layout, LEGACY_PATH, "CAMPAIGN_LOCALISATION")?;
320 name.group()?.set_str("^+1", legacy.name())?;
321
322 let mut record = layout.builder(LEGACY_PATH)?;
323 record.group()?
324 .set_record("MILITARY_FORCE_LEGACY_HISTORY", Some(self.write_history(force)?))?
325 .set_u32("^+1", force.id())?
326 .set_record("CAMPAIGN_SKILLS", Some(skills.build()?))?
327 .set_record("CAMPAIGN_LOCALISATION", Some(name.build()?))?
328 .set_str("CAMPAIGN_LOCALISATION+1", legacy.override_name())?
329 .set_u32("CAMPAIGN_LOCALISATION+2", force.kind() as u32)?
330 .set_str("CAMPAIGN_LOCALISATION+3", legacy.emblem())?
331 .set_u32("CAMPAIGN_LOCALISATION+4", legacy.number())?;
332 record.build()
333 }
334
335 fn write_history(&self, force: &PlannedForce) -> Result<RecordNode> {
339 let layout = self.inputs.layout;
340 let index = self.inputs.index;
341 let commander = &self.forces.characters()[force.commander()];
342 let (_, cell) = self.characters.position(commander)?;
343 let region = self.inputs.region_map.region_at(cell)
344 .and_then(|key| index.region(key))
345 .or_else(|| index.capital_of(force.faction().key()));
346
347 let mut record = layout.builder(HISTORY_PATH)?;
348 let mut localisation = child(layout, HISTORY_PATH, "CAMPAIGN_LOCALISATION")?;
349 let Some(region) = region else {
350 localisation.group()?.set_str("^+1", "")?;
351 let mut date = child(layout, HISTORY_PATH, "DATE")?;
352 date.group()?.set_u32("^+1", 0)?;
353 record.group()?
354 .set_record("DATE", Some(date.build()?))?
355 .set_record("CHARACTER_NAME", Some(self.characters.write_name(HISTORY_PATH, None)?))?
356 .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
357 .set_str("CAMPAIGN_LOCALISATION+1", "")?;
358 return record.build();
359 };
360
361 let settlement_key = index.settlement_of(region.key()).and_then(|settlement| self.settlement_keys.get(settlement.id())).cloned().unwrap_or_default();
362 localisation.group()?.set_str("^+1", &settlement_name_key(&settlement_key, region))?.unset("^+2")?;
363 let mut date = child(layout, HISTORY_PATH, "DATE")?;
364 date.group()?.set_u32("^+1", self.start_year)?;
365 record.group()?
366 .set_record("DATE", Some(date.build()?))?
367 .set_record("CHARACTER_NAME", Some(self.characters.write_name(HISTORY_PATH, commander.entry())?))?
368 .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
369 .set_str("CAMPAIGN_LOCALISATION+1", self.provinces.get(region.key()).map(String::as_str).unwrap_or_default())?;
370 record.build()
371 }
372
373 fn write_unit(&self, force: &PlannedForce, unit: &PlannedUnit, id: u32, commander: u32) -> Result<RecordNode> {
378 let layout = self.inputs.layout;
379 let stats = self.stats.get(unit.unit()).copied().unwrap_or_default();
380 let multiplier = if matches!(force.kind(), ForceKind::Army | ForceKind::Navy) { GENERAL_ACTION_POINTS_MULTIPLIER } else { 1.0 };
381 let action_points = (stats.action_points as f32 * multiplier).round() as u32;
382
383 let mut key = child(layout, UNIT_PATH, "UNIT_RECORD_KEY")?;
384 key.group()?.set_str("^+1", unit.unit())?;
385
386 let mut date = child(layout, &format!("{UNIT_PATH}/UNIT_HISTORY"), "DATE")?;
387 date.group()?.set_u32("^+1", self.start_year)?;
388 let mut history = child(layout, UNIT_PATH, "UNIT_HISTORY")?;
389 history.group()?.set_record("DATE", Some(date.build()?))?;
390
391 let mut commander_details = child(layout, UNIT_PATH, "COMMANDER_DETAILS")?;
392 commander_details.group()?.set_str("^+1", force.faction().key())?;
393
394 let mut record = layout.builder(UNIT_PATH)?;
395 record.group()?
396 .set_u32("^+1", id)?
397 .set_record("UNIT_RECORD_KEY", Some(key.build()?))?
398 .set_record("UNIT_HISTORY", Some(history.build()?))?
399 .set_record("COMMANDER_DETAILS", Some(commander_details.build()?))?
400 .set_u32("COMMANDER_DETAILS+1", stats.men * unit.soldiers() / 100)?
401 .set_u32("COMMANDER_DETAILS+2", stats.men)?
402 .set_u32("COMMANDER_DETAILS+3", action_points)?
403 .set_u32("COMMANDER_DETAILS+4", commander)?
404 .set_u32("CAMPAIGN_LOCALISATION+1", stats.action_points)?
405 .set_f32("CAMPAIGN_LOCALISATION+2", multiplier)?
406 .set_u32("CAMPAIGN_LOCALISATION+3", id.saturating_sub(1))?;
407 if let Some(bonuses) = self.write_stat_bonuses(force, unit)? {
408 record.group()?.set_record("UNIT_STATISTICS_BONUSES", Some(bonuses))?;
409 }
410 record.build()
411 }
412
413 fn write_stat_bonuses(&self, force: &PlannedForce, unit: &PlannedUnit) -> Result<Option<RecordNode>> {
416 let Some(region) = force.region() else { return Ok(None) };
417 let province = self.provinces.get(region);
418 let index = self.inputs.index;
419 let owned = index.regions_of(force.faction().key()).filter(|other| province.is_some() && self.provinces.get(other.key()) == province);
420 let buildings = owned.filter_map(|other| index.settlement_of(other.key()).map(|settlement| (settlement, other.key() == region)))
421 .flat_map(|(settlement, own_region)| settlement.all_buildings().filter(|building| !building.is_empty()).map(move |building| (building, own_region)))
422 .collect::<Vec<_>>();
423 let bonuses = self.stat_bonuses.bonuses(unit.unit(), buildings);
424 if bonuses.is_empty() {
425 return Ok(None);
426 }
427
428 let layout = self.inputs.layout;
429 let path = format!("{UNIT_PATH}/UNIT_STATISTICS_BONUSES");
430 let block_path = format!("{path}/STATS_BLOCK");
431 let mut block = child(layout, &path, "STATS_BLOCK")?;
432 for (stat, value) in bonuses {
433 let mut bonus = child(layout, &block_path, "UNIT_STATISTICS_BONUS")?;
434 bonus.group()?
435 .set_i32("^+1", i32::try_from(stat).unwrap_or_default())?
436 .set_i32("^+2", 0)?
437 .set_i32("^+3", i32::try_from(value).unwrap_or_default())?;
438 block.push_group()?.set_record("UNIT_STATISTICS_BONUS", Some(bonus.build()?))?;
439 }
440 let mut record = layout.builder(&path)?;
441 record.group()?.set_record("STATS_BLOCK", Some(block.build()?))?;
442 record.build().map(Some)
443 }
444}
445
446fn is_navy(force: &PlannedForce) -> bool {
448 matches!(force.kind(), ForceKind::Navy | ForceKind::GarrisonNavy)
449}
450
451fn horde_plans(inputs: &WorldInputs, forces: &ForcePlan, late_recruitment: u32) -> Result<HashMap<u32, HordePlan>> {
459 let rows = horde_details(inputs)?.into_iter()
460 .map(|row| Ok((row.str("general")?.into_owned(), row)))
461 .collect::<Result<HashMap<_, _>>>()?;
462
463 let mut horde_factions = HashMap::new();
464 let (mut next_recruitment, mut late_recruitment, mut next_slot, mut next_building) = (1, late_recruitment, 1, 1);
465 let mut plans = HashMap::new();
466 for force in forces.forces().iter().filter(|force| force.kind() == ForceKind::Army) {
467 let faction = force.faction();
468 if !horde_factions.contains_key(faction.key()) {
469 let is_horde = faction_summary(inputs.tables, inputs.index, faction)?.horde() == Some(true);
470 horde_factions.insert(faction.key().to_owned(), is_horde);
471 }
472 if !horde_factions[faction.key()] {
473 continue;
474 }
475
476 let row = forces.characters()[force.commander()].entry().and_then(|entry| rows.get(entry.id()));
477 let mut buildings = vec![];
478 for column in HORDE_BUILDING_COLUMNS {
479 if let Some(building) = row.map(|row| row.str(column)).transpose()?.filter(|building| !building.is_empty()) {
480 buildings.push(Some((building.into_owned(), next_id(&mut next_building))));
481 }
482 }
483 buildings.resize(HORDE_BUILDING_COLUMNS.len(), None);
484
485 let first_slot_id = next_slot;
486 next_slot += u32::try_from(HORDE_BUILDING_COLUMNS.len()).unwrap_or_default();
487 plans.insert(force.id(), HordePlan {
488 recruitment_id: if row.is_some() { next_id(&mut next_recruitment) } else { next_id(&mut late_recruitment) },
489 first_slot_id,
490 buildings,
491 development: u32::try_from(row.map(|row| row.int("starting_dev_points")).transpose()?.unwrap_or_default()).unwrap_or_default() + 1,
492 government: start_pos_faction_row(inputs.tables, faction)?.str("government_type")?.into_owned(),
493 });
494 }
495 Ok(plans)
496}
497
498fn unit_stats(inputs: &WorldInputs) -> Result<HashMap<String, UnitStats>> {
500 let mut land_points = HashMap::new();
501 for row in inputs.tables.table("land_units")?.rows() {
502 land_points.insert(row.str("key")?.into_owned(), row.int("campaign_action_points")?);
503 }
504 let mut naval_points = HashMap::new();
505 for row in inputs.tables.table("naval_units")?.rows() {
506 naval_points.insert(row.str("key")?.into_owned(), row.int("campaign_action_points")?);
507 }
508
509 let mut stats = HashMap::new();
510 for row in inputs.tables.table("main_units")?.rows() {
511 let points = if row.bool("is_naval")? { naval_points.get(row.str("naval_unit")?.as_ref()) } else { land_points.get(row.str("land_unit")?.as_ref()) };
512 stats.insert(row.str("unit")?.into_owned(), UnitStats {
513 men: u32::try_from(row.int("num_men")?).unwrap_or_default(),
514 action_points: points.and_then(|points| u32::try_from(*points).ok()).unwrap_or_default(),
515 });
516 }
517 Ok(stats)
518}
519
520pub(crate) fn start_year(inputs: &WorldInputs) -> Result<u32> {
522 Ok(Calendar::new(inputs.tables, inputs.index.campaign())?.start_year())
523}