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};
26
27const FACTION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION";
29
30const ARMY_ARRAY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY";
32
33const FORCE_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE";
35
36const UNIT_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/UNIT_CONTAINER/UNITS_ARRAY/UNIT";
38
39const LEGACY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/MILITARY_FORCE_LEGACY";
41
42const HISTORY_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/MILITARY_FORCE_LEGACY/MILITARY_FORCE_LEGACY_HISTORY";
44
45const HORDE_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/ARMY_ARRAY/MILITARY_FORCE/MILITARY_FORCE_HORDE_DETAILS";
47
48const 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";
50
51const HORDE_PRIMARY_SLOT: &str = "horde_primary";
53
54const HORDE_SECONDARY_SLOT: &str = "horde_secondary";
56
57const HORDE_BUILDING_VALUE: f32 = 1.0;
59
60const GENERAL_ACTION_POINTS_MULTIPLIER: f32 = 1.1;
62
63pub struct ArmyWriter<'a, 'b> {
69
70 inputs: &'b WorldInputs<'a>,
72
73 forces: &'b ForcePlan<'a>,
75
76 residences: &'b HashMap<String, u32>,
78
79 stats: HashMap<String, UnitStats>,
81
82 start_year: u32,
84
85 hordes: HashMap<u32, HordePlan>,
87
88 characters: &'b CharacterWriter<'a, 'b>,
90
91 settlement_keys: HashMap<String, String>,
93
94 provinces: HashMap<String, String>,
96}
97
98#[derive(Debug, Clone, Default)]
100struct HordePlan {
101
102 recruitment_id: u32,
104
105 first_slot_id: u32,
107
108 buildings: Vec<Option<(String, u32)>>,
110
111 development: u32,
113
114 government: String,
116}
117
118#[derive(Debug, Clone, Copy, Default)]
120struct UnitStats {
121
122 men: u32,
124
125 action_points: u32,
127}
128
129impl<'a, 'b> ArmyWriter<'a, 'b> {
134
135 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> {
150 let mut provinces = HashMap::new();
151 for row in inputs.tables.table("region_to_province_junctions")?.rows() {
152 provinces.insert(row.str("region")?.into_owned(), row.str("province")?.into_owned());
153 }
154
155 Ok(Self {
156 hordes: horde_plans(inputs, forces, late_recruitment)?,
157 settlement_keys: settlement_keys(inputs)?,
158 provinces,
159 characters,
160 stats: unit_stats(inputs)?,
161 start_year: start_year(inputs)?,
162 inputs,
163 forces,
164 residences,
165 })
166 }
167
168 pub fn write_army_array(&self, faction: &FactionEntry) -> Result<RecordNode> {
174 let faction_forces = self.forces.forces_of(faction.key()).collect::<Vec<_>>();
175 let force_legacies = self.legacies(faction)?;
176
177 let mut array = self.inputs.layout.builder(ARMY_ARRAY_PATH)?;
178 for (force, legacy) in faction_forces.iter().zip(&force_legacies) {
179 array.push_group()?.set_record("MILITARY_FORCE", Some(self.write_force(force, legacy)?))?;
180 }
181 array.build()
182 }
183
184 fn legacies(&self, faction: &FactionEntry) -> Result<Vec<Legacy>> {
186 let legacy_forces = self.forces.forces_of(faction.key()).map(|force| LegacyForce {
187 kind: force.kind(),
188 general: self.forces.characters()[force.commander()].entry().map(|entry| entry.id().to_owned()),
189 }).collect::<Vec<_>>();
190 legacies(self.inputs.tables, self.inputs.index, faction, &legacy_forces, self.inputs.legacy_overrides)
191 }
192
193 pub fn write_used_legacies(&self, faction: &FactionEntry) -> Result<RecordNode> {
201 let mut used = [vec![], vec![], vec![], vec![]];
202 let mut overrides = false;
203 for (force, legacy) in self.forces.forces_of(faction.key()).zip(self.legacies(faction)?) {
204 let navy = usize::from(force.kind() == ForceKind::Navy);
205 match force.kind() {
206 ForceKind::Army | ForceKind::Navy if legacy.override_name().is_empty() => used[navy].push(legacy.name().to_owned()),
207 ForceKind::Army | ForceKind::Navy => overrides = true,
208 ForceKind::GarrisonArmy | ForceKind::GarrisonNavy => continue,
209 }
210 used[2 + navy].push(legacy.emblem().to_owned());
211 }
212
213 let [army_names, navy_names, army_emblems, navy_emblems] = used;
214 let mut record = child(self.inputs.layout, FACTION_PATH, "MILITARY_FORCE_LEGACY_NAMES_AND_EMBLEMS")?;
215 record.group()?
216 .set_strings("^+3", army_names)?
217 .set_strings("^+6", navy_names)?
218 .set_strings("^+9", army_emblems)?
219 .set_strings("^+12", navy_emblems)?
220 .set_bool("^+13", overrides)?;
221 record.build()
222 }
223
224 fn write_force(&self, force: &PlannedForce, legacy: &Legacy) -> Result<RecordNode> {
226 let layout = self.inputs.layout;
227 let residence = |region: Option<&String>| region.and_then(|region| self.residences.get(region)).copied().unwrap_or_default();
228 let garrison_residence = |kind| residence(force.region().as_ref().filter(|_| force.kind() == kind));
229 let commander_id = self.forces.characters()[force.commander()].id();
230
231 let mut units = child(layout, &format!("{FORCE_PATH}/UNIT_CONTAINER"), "UNITS_ARRAY")?;
232 for (unit, id) in force.units().iter().zip(force.unit_ids()) {
233 let commander = if unit.commands() { commander_id } else { 0 };
234 units.push_group()?.set_record("UNIT", Some(self.write_unit(force, unit, id, commander)?))?;
235 }
236 let mut container = child(layout, FORCE_PATH, "UNIT_CONTAINER")?;
237 container.group()?.set_record("UNITS_ARRAY", Some(units.build()?))?;
238
239 let mut record = layout.builder(FORCE_PATH)?;
240 record.group()?
241 .set_u32("^+1", force.id())?
242 .set_u32("^+2", commander_id)?
243 .set_record("MILITARY_FORCE_LEGACY", Some(self.write_legacy(force, legacy)?))?
244 .set_record("UNIT_CONTAINER", Some(container.build()?))?
245 .set_u32("UNIT_CONTAINER+1", residence(force.stationed_in().as_ref()))?
246 .set_u32("UNIT_CONTAINER+11", garrison_residence(ForceKind::GarrisonArmy))?
247 .set_u32("UNIT_CONTAINER+12", garrison_residence(ForceKind::GarrisonNavy))?
248 .set_bool("UNIT_CONTAINER+13", is_navy(force))?;
249 if let Some(horde) = self.hordes.get(&force.id()) {
250 record.group()?
251 .set_bool("MILITARY_FORCE_MORALE+1", true)?
252 .set_record("MILITARY_FORCE_HORDE_DETAILS", Some(self.write_horde_details(force, horde)?))?;
253 }
254 record.build()
255 }
256
257 fn write_horde_details(&self, force: &PlannedForce, horde: &HordePlan) -> Result<RecordNode> {
259 let layout = self.inputs.layout;
260 let mut recruitment = child(layout, HORDE_PATH, "RECRUITMENT_MANAGER")?;
261 recruitment.group()?.set_u32("^+1", horde.recruitment_id)?;
262
263 let mut slots = child(layout, &format!("{HORDE_PATH}/MILITARY_FORCE_SLOT_MANAGER"), "SLOT_BLOCK")?;
264 for (index, building) in horde.buildings.iter().enumerate() {
265 let primary = index == 0;
266 let base = building.as_ref().map(|(key, id)| {
267 let mut base = child(layout, &format!("{HORDE_SLOT_PATH}/MILITARY_FORCE_BUILDING_MANAGER"), "BUILDING_BASE")?;
268 base.group()?
269 .set_u32("^+1", *id)?
270 .set_str("^+2", key)?
271 .set_f32("^+3", HORDE_BUILDING_VALUE)?
272 .set_f32("^+4", HORDE_BUILDING_VALUE)?
273 .set_str("^+5", force.faction().key())?
274 .set_str("^+6", &horde.government)?;
275 base.build()
276 }).transpose()?;
277
278 let mut manager = child(layout, HORDE_SLOT_PATH, "MILITARY_FORCE_BUILDING_MANAGER")?;
279 manager.group()?
280 .set_bool("^+2", base.is_some())?
281 .set_record("BUILDING_BASE", base)?;
282
283 let slot_type = if primary { HORDE_PRIMARY_SLOT } else { HORDE_SECONDARY_SLOT };
284 let mut slot = layout.builder(HORDE_SLOT_PATH)?;
285 slot.group()?
286 .set_u32("^+1", horde.first_slot_id + u32::try_from(index).unwrap_or_default())?
287 .set_u32("^+2", u32::try_from(index).unwrap_or_default())?
288 .set_record("MILITARY_FORCE_BUILDING_MANAGER", Some(manager.build()?))?
289 .set_str("MILITARY_FORCE_BUILDING_MANAGER+1", slot_type)?
290 .set_str("MILITARY_FORCE_BUILDING_MANAGER+2", slot_type)?
291 .set_bool("MILITARY_FORCE_BUILDING_MANAGER+4", primary)?;
292 slots.push_group()?.set_record("MILITARY_FORCE_SLOT", Some(slot.build()?))?;
293 }
294 let mut slot_manager = child(layout, HORDE_PATH, "MILITARY_FORCE_SLOT_MANAGER")?;
295 slot_manager.group()?.set_record("SLOT_BLOCK", Some(slots.build()?))?;
296
297 let mut development = child(layout, HORDE_PATH, "MILITARY_FORCE_DEVELOPMENT")?;
298 development.group()?.set_u32("^+1", horde.development)?;
299
300 let mut record = layout.builder(HORDE_PATH)?;
301 record.group()?
302 .set_record("RECRUITMENT_MANAGER", Some(recruitment.build()?))?
303 .set_record("MILITARY_FORCE_SLOT_MANAGER", Some(slot_manager.build()?))?
304 .set_record("MILITARY_FORCE_DEVELOPMENT", Some(development.build()?))?;
305 record.build()
306 }
307
308 fn write_legacy(&self, force: &PlannedForce, legacy: &Legacy) -> Result<RecordNode> {
310 let layout = self.inputs.layout;
311 let mut skills = child(layout, LEGACY_PATH, "CAMPAIGN_SKILLS")?;
312 skills.group()?.set_str("^+2", &legacy_skill_set(force.faction(), is_navy(force)))?;
313
314 let mut name = child(layout, LEGACY_PATH, "CAMPAIGN_LOCALISATION")?;
315 name.group()?.set_str("^+1", legacy.name())?;
316
317 let mut record = layout.builder(LEGACY_PATH)?;
318 record.group()?
319 .set_record("MILITARY_FORCE_LEGACY_HISTORY", Some(self.write_history(force)?))?
320 .set_u32("^+1", force.id())?
321 .set_record("CAMPAIGN_SKILLS", Some(skills.build()?))?
322 .set_record("CAMPAIGN_LOCALISATION", Some(name.build()?))?
323 .set_str("CAMPAIGN_LOCALISATION+1", legacy.override_name())?
324 .set_u32("CAMPAIGN_LOCALISATION+2", force.kind() as u32)?
325 .set_str("CAMPAIGN_LOCALISATION+3", legacy.emblem())?
326 .set_u32("CAMPAIGN_LOCALISATION+4", legacy.number())?;
327 record.build()
328 }
329
330 fn write_history(&self, force: &PlannedForce) -> Result<RecordNode> {
334 let layout = self.inputs.layout;
335 let index = self.inputs.index;
336 let commander = &self.forces.characters()[force.commander()];
337 let (_, cell) = self.characters.position(commander)?;
338 let region = self.inputs.region_map.region_at(cell)
339 .and_then(|key| index.region(key))
340 .or_else(|| index.capital_of(force.faction().key()));
341
342 let mut record = layout.builder(HISTORY_PATH)?;
343 let mut localisation = child(layout, HISTORY_PATH, "CAMPAIGN_LOCALISATION")?;
344 let Some(region) = region else {
345 localisation.group()?.set_str("^+1", "")?;
346 let mut date = child(layout, HISTORY_PATH, "DATE")?;
347 date.group()?.set_u32("^+1", 0)?;
348 record.group()?
349 .set_record("DATE", Some(date.build()?))?
350 .set_record("CHARACTER_NAME", Some(self.characters.write_name(HISTORY_PATH, None)?))?
351 .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
352 .set_str("CAMPAIGN_LOCALISATION+1", "")?;
353 return record.build();
354 };
355
356 let settlement_key = index.settlement_of(region.key()).and_then(|settlement| self.settlement_keys.get(settlement.id())).cloned().unwrap_or_default();
357 localisation.group()?.set_str("^+1", &settlement_name_key(&settlement_key, region))?.unset("^+2")?;
358 let mut date = child(layout, HISTORY_PATH, "DATE")?;
359 date.group()?.set_u32("^+1", self.start_year)?;
360 record.group()?
361 .set_record("DATE", Some(date.build()?))?
362 .set_record("CHARACTER_NAME", Some(self.characters.write_name(HISTORY_PATH, commander.entry())?))?
363 .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
364 .set_str("CAMPAIGN_LOCALISATION+1", self.provinces.get(region.key()).map(String::as_str).unwrap_or_default())?;
365 record.build()
366 }
367
368 fn write_unit(&self, force: &PlannedForce, unit: &PlannedUnit, id: u32, commander: u32) -> Result<RecordNode> {
373 let layout = self.inputs.layout;
374 let stats = self.stats.get(unit.unit()).copied().unwrap_or_default();
375 let multiplier = if matches!(force.kind(), ForceKind::Army | ForceKind::Navy) { GENERAL_ACTION_POINTS_MULTIPLIER } else { 1.0 };
376 let action_points = (stats.action_points as f32 * multiplier).round() as u32;
377
378 let mut key = child(layout, UNIT_PATH, "UNIT_RECORD_KEY")?;
379 key.group()?.set_str("^+1", unit.unit())?;
380
381 let mut date = child(layout, &format!("{UNIT_PATH}/UNIT_HISTORY"), "DATE")?;
382 date.group()?.set_u32("^+1", self.start_year)?;
383 let mut history = child(layout, UNIT_PATH, "UNIT_HISTORY")?;
384 history.group()?.set_record("DATE", Some(date.build()?))?;
385
386 let mut commander_details = child(layout, UNIT_PATH, "COMMANDER_DETAILS")?;
387 commander_details.group()?.set_str("^+1", force.faction().key())?;
388
389 let mut record = layout.builder(UNIT_PATH)?;
390 record.group()?
391 .set_u32("^+1", id)?
392 .set_record("UNIT_RECORD_KEY", Some(key.build()?))?
393 .set_record("UNIT_HISTORY", Some(history.build()?))?
394 .set_record("COMMANDER_DETAILS", Some(commander_details.build()?))?
395 .set_u32("COMMANDER_DETAILS+1", stats.men * unit.soldiers() / 100)?
396 .set_u32("COMMANDER_DETAILS+2", stats.men)?
397 .set_u32("COMMANDER_DETAILS+3", action_points)?
398 .set_u32("COMMANDER_DETAILS+4", commander)?
399 .set_u32("CAMPAIGN_LOCALISATION+1", stats.action_points)?
400 .set_f32("CAMPAIGN_LOCALISATION+2", multiplier)?
401 .set_u32("CAMPAIGN_LOCALISATION+3", id.saturating_sub(1))?;
402 record.build()
403 }
404}
405
406fn is_navy(force: &PlannedForce) -> bool {
408 matches!(force.kind(), ForceKind::Navy | ForceKind::GarrisonNavy)
409}
410
411fn horde_plans(inputs: &WorldInputs, forces: &ForcePlan, late_recruitment: u32) -> Result<HashMap<u32, HordePlan>> {
419 let rows = horde_details(inputs)?.into_iter()
420 .map(|row| Ok((row.str("general")?.into_owned(), row)))
421 .collect::<Result<HashMap<_, _>>>()?;
422
423 let mut horde_factions = HashMap::new();
424 let (mut next_recruitment, mut late_recruitment, mut next_slot, mut next_building) = (1, late_recruitment, 1, 1);
425 let mut plans = HashMap::new();
426 for force in forces.forces().iter().filter(|force| force.kind() == ForceKind::Army) {
427 let faction = force.faction();
428 if !horde_factions.contains_key(faction.key()) {
429 let is_horde = faction_summary(inputs.tables, inputs.index, faction)?.horde() == Some(true);
430 horde_factions.insert(faction.key().to_owned(), is_horde);
431 }
432 if !horde_factions[faction.key()] {
433 continue;
434 }
435
436 let row = forces.characters()[force.commander()].entry().and_then(|entry| rows.get(entry.id()));
437 let mut buildings = vec![];
438 for column in HORDE_BUILDING_COLUMNS {
439 if let Some(building) = row.map(|row| row.str(column)).transpose()?.filter(|building| !building.is_empty()) {
440 buildings.push(Some((building.into_owned(), next_id(&mut next_building))));
441 }
442 }
443 buildings.resize(HORDE_BUILDING_COLUMNS.len(), None);
444
445 let first_slot_id = next_slot;
446 next_slot += u32::try_from(HORDE_BUILDING_COLUMNS.len()).unwrap_or_default();
447 plans.insert(force.id(), HordePlan {
448 recruitment_id: if row.is_some() { next_id(&mut next_recruitment) } else { next_id(&mut late_recruitment) },
449 first_slot_id,
450 buildings,
451 development: u32::try_from(row.map(|row| row.int("starting_dev_points")).transpose()?.unwrap_or_default()).unwrap_or_default() + 1,
452 government: start_pos_faction_row(inputs.tables, faction)?.str("government_type")?.into_owned(),
453 });
454 }
455 Ok(plans)
456}
457
458fn unit_stats(inputs: &WorldInputs) -> Result<HashMap<String, UnitStats>> {
460 let mut land_points = HashMap::new();
461 for row in inputs.tables.table("land_units")?.rows() {
462 land_points.insert(row.str("key")?.into_owned(), row.int("campaign_action_points")?);
463 }
464 let mut naval_points = HashMap::new();
465 for row in inputs.tables.table("naval_units")?.rows() {
466 naval_points.insert(row.str("key")?.into_owned(), row.int("campaign_action_points")?);
467 }
468
469 let mut stats = HashMap::new();
470 for row in inputs.tables.table("main_units")?.rows() {
471 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()) };
472 stats.insert(row.str("unit")?.into_owned(), UnitStats {
473 men: u32::try_from(row.int("num_men")?).unwrap_or_default(),
474 action_points: points.and_then(|points| u32::try_from(*points).ok()).unwrap_or_default(),
475 });
476 }
477 Ok(stats)
478}
479
480pub(crate) fn start_year(inputs: &WorldInputs) -> Result<u32> {
482 Ok(Calendar::new(inputs.tables, inputs.index.campaign())?.start_year())
483}