1use std::collections::HashMap;
14
15use rpfm_lib::error::Result;
16use rpfm_lib::files::esf::{NodeType, RecordNode};
17
18use super::armies::ArmyWriter;
19use super::characters::CharacterWriter;
20use super::government::{write_character_posts, PostPlan};
21use super::provinces::write_mercenary_pool;
22use super::sight::{write_shroud, SightPlan};
23use super::{child, next_id, start_pos_faction_row, WorldInputs, WorldPlan, WORLD_PATH};
24use crate::startpos::cai::technologies::faction_technologies;
25use crate::startpos::layout::{child_path, StartposLayout};
26use crate::startpos::vision::Vision;
27use crate::startpos::world::agents::recruitment_pool_agents;
28use crate::startpos::world::diplomacy::Diplomacy;
29use crate::startpos::world::government::political_parties;
30use crate::startpos::world::pools::{faction_mercenary_pool, levy_pools};
31use crate::startpos::world::FactionEntry;
32use crate::startpos::world::factions::{faction_flags, faction_summary, FactionFlag};
33
34const FACTION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION";
36
37pub(crate) const FACTION_KEY_FIELDS: [&str; 2] = ["^+2", "CAMPAIGN_EFFECT_BUNDLES+1"];
39
40const SCREEN_NAME_PREFIX: &str = "factions_screen_name_";
42
43const EMPTY_MEMORY_HONOUR: i64 = 129;
45
46const EVENT_COUNTERS: usize = 151;
49
50const NOMADIC_CULTURE: &str = "att_cult_nomadic";
52
53const DEFAULT_MEMORY: [u8; 22] = [0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
55
56pub struct FactionContext<'c, 'a, 'b> {
62
63 pub armies: &'c ArmyWriter<'a, 'b>,
65
66 pub characters: &'c CharacterWriter<'a, 'b>,
68
69 pub posts: &'c PostPlan,
71
72 pub levy_pools: &'c [PlannedLevyPool],
74
75 pub diplomacy: &'c Diplomacy,
77
78 pub sight: &'c SightPlan,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct PlannedLevyPool {
85
86 id: u32,
88
89 overlord: String,
91
92 subject: u32,
94
95 province: u32,
97}
98
99pub fn write_rebel_faction(layout: &StartposLayout, plan: &WorldPlan) -> Result<RecordNode> {
109 let path = child_path(WORLD_PATH, "REBEL_FACTION");
110 let region_count = plan.regions().iter().filter(|region| region.owner().is_none()).count();
111 let mut faction = layout.builder(&child_path(&path, "FACTION"))?;
112 faction.group()?.set_u32("FACTION_FLAG_AND_COLOURS@2+14", u32::try_from(region_count).unwrap_or(u32::MAX))?;
113
114 let mut rebels = layout.builder(&path)?;
115 rebels.group()?.set_record("FACTION", Some(faction.build()?))?;
116 rebels.build()
117}
118
119pub fn write_faction_array(inputs: &WorldInputs, plan: &WorldPlan, context: &FactionContext) -> Result<RecordNode> {
125 let mut array = child(inputs.layout, WORLD_PATH, "FACTION_ARRAY")?;
126 for faction in plan.factions() {
127 array.push_group()?.set_record("FACTION", Some(write_faction(inputs, plan, context, faction)?))?;
128 }
129 array.build()
130}
131
132pub fn plan_levy_pools(inputs: &WorldInputs, plan: &WorldPlan, province_ids: &HashMap<String, u32>, first_pool: u32) -> Result<Vec<PlannedLevyPool>> {
146
147 if inputs.layout.record(&format!("{FACTION_PATH}/LEVY_MERCENARY_MANAGER/LEVY_POOLS_BLOCK/LEVY_MERCENARY_POOL")).is_none() {
149 return Ok(vec![]);
150 }
151 let mut pools = levy_pools(inputs.tables, inputs.index)?.into_iter()
152 .map(|pool| PlannedLevyPool {
153 id: 0,
154 subject: plan.faction_id(pool.subject()).unwrap_or_default(),
155 province: province_ids.get(pool.province()).copied().unwrap_or_default(),
156 overlord: pool.overlord().to_owned(),
157 })
158 .collect::<Vec<_>>();
159 pools.sort_by_key(|pool| (pool.subject, pool.province));
160
161 let mut next_pool = first_pool;
162 for pool in &mut pools {
163 pool.id = next_id(&mut next_pool);
164 }
165 Ok(pools)
166}
167
168pub fn write_faction(inputs: &WorldInputs, plan: &WorldPlan, context: &FactionContext, faction: &FactionEntry) -> Result<RecordNode> {
174 let layout = inputs.layout;
175 let key = faction.key();
176 let row = start_pos_faction_row(inputs.tables, faction)?;
177 let summary = faction_summary(inputs.tables, inputs.index, faction)?;
178 let faction_id = |key: &Option<String>| key.as_deref().and_then(|key| plan.faction_id(key)).unwrap_or_default();
179
180 let mut setup = child(layout, FACTION_PATH, "CAMPAIGN_PLAYER_SETUP")?;
181 setup.group()?
182 .set_str("CAMPAIGN_PLAYER_SETUP_INGAME_MODIFIABLES+1", key)?
183 .set_bool("CAMPAIGN_PLAYER_SETUP_INGAME_MODIFIABLES+3", faction.playable())?;
184
185 let mut economics = child(layout, FACTION_PATH, "FACTION_ECONOMICS")?;
186 economics.group()?.set_i32("history+1", i32::try_from(row.int("treasury")?).unwrap_or(i32::MAX))?;
187
188 let posts = write_character_posts(inputs, context.posts, key, |id| context.characters.runtime_id(id))?;
189 let mut government = child(layout, FACTION_PATH, "GOVERNMENT")?;
190 government.group()?
191 .set_str("^+2", &row.str("government_type")?)?
192 .set_record("CHARACTER_POSTS", Some(posts))?;
193
194 let mut religion = child(layout, FACTION_PATH, "FACTION_RELIGION_MANAGER")?;
195 religion.group()?.set_str("^+1", &row.str("state_religion")?)?;
196
197 let mut localisation = child(layout, FACTION_PATH, "CAMPAIGN_LOCALISATION")?;
198 localisation.group()?.set_str("^+1", &format!("{SCREEN_NAME_PREFIX}{key}"))?.unset("^+2")?;
199
200 let [flag, republican_flag] = faction_flags(inputs.tables, key)?;
201 let honour = row.int("honour")?;
202 let memory = if honour >= EMPTY_MEMORY_HONOUR { vec![0; DEFAULT_MEMORY.len()] } else { DEFAULT_MEMORY.to_vec() };
203
204 let mut horde = None;
206 if let Some(is_horde) = summary.horde().filter(|_| layout.record(&child_path(FACTION_PATH, "FACTION_HORDE_MANAGER")).is_some()) {
207 let mut manager = child(layout, FACTION_PATH, "FACTION_HORDE_MANAGER")?;
208 manager.group()?.set_i32("^+1", i32::from(is_horde))?;
209 horde = Some(manager.build()?);
210 }
211
212 let mission_manager = faction.playable().then(|| layout.default_record(&format!("{FACTION_PATH}/CAMPAIGN_MISSION_MANAGER"))).transpose()?;
213 let shroud = if faction.playable() { Some(write_shroud(layout, FACTION_PATH, context.sight, &faction_vision(inputs, context, faction)?)?) } else { None };
214
215 let faction_layout = layout.record(FACTION_PATH);
217 let key_field = FACTION_KEY_FIELDS.into_iter().find(|field| faction_layout.is_some_and(|record| record.has_slot(field))).unwrap_or(FACTION_KEY_FIELDS[0]);
218 let log_shift = usize::from(faction_layout.is_some_and(|record| record.has_slot("FACTION_ACTION_LOG_BLOCK+13")));
219 let mut record = layout.builder(FACTION_PATH)?;
220 record.group()?
221 .set_u32("^+1", plan.faction_id(key).unwrap_or_default())?
222 .set_str(key_field, key)?
223 .set_record("CAMPAIGN_PLAYER_SETUP", Some(setup.build()?))?
224 .set_record("FACTION_ECONOMICS", Some(economics.build()?))?
225 .set_record("GOVERNMENT", Some(government.build()?))?
226 .set_record("FACTION_HORDE_MANAGER", horde)?
227 .set_bool("GOVERNMENT+3", row.bool("is_major")?)?
228 .set_record("FACTION_RELIGION_MANAGER", Some(religion.build()?))?
229 .set_record("CHARACTER_ARRAY", Some(context.characters.write_character_array(faction)?))?
230 .set_record("FACTION_TECHNOLOGY_MANAGER", Some(write_technologies(inputs, faction)?))?
231 .set_record("ARMY_ARRAY", Some(context.armies.write_army_array(faction)?))?
232 .set_bool("MORGUE+1", faction.playable())?
233 .set_strings("MORGUE+2", summary.victory_regions().to_vec())?
234 .set_u32("MORGUE+3", summary.capital().as_deref().and_then(|capital| plan.region_id(capital)).unwrap_or_default())?
235 .set_u32("MORGUE+4", summary.capital().as_deref().and_then(|capital| plan.region_id(capital)).unwrap_or_default())?
236 .set_bool("PRESTIGE+1", summary.without_characters())?
237 .set_str("PRESTIGE+6", &row.str("cdir_military_generator_config")?)?
238 .set_record("FACTION_FLAG_AND_COLOURS", Some(write_flag(inputs, FACTION_PATH, &flag)?))?
239 .set_record("FACTION_FLAG_AND_COLOURS@2", Some(write_flag(inputs, FACTION_PATH, &republican_flag)?))?
240 .set_u32("FACTION_FLAG_AND_COLOURS@2+1", faction_id(summary.client_state_of()))?
241 .set_u32("FACTION_FLAG_AND_COLOURS@2+2", faction_id(summary.vassal_of()))?
242 .set_bool("FACTION_FLAG_AND_COLOURS@2+5", summary.has_war_or_vassal())?
243 .set_u32("FACTION_FLAG_AND_COLOURS@2+14", u32::try_from(summary.region_count()).unwrap_or(u32::MAX))?
244 .set_bytes("FACTION_FLAG_AND_COLOURS@2+21", memory)?
245 .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
246 .set_strings("FACTION_ACTION_LOG_BLOCK+2", summary.subjects().to_vec())?
247 .set_record("FACTION_MERCENARY_POOL", Some(write_faction_mercenary_pool(inputs, plan, faction)?))?
248 .set_record("LEVY_MERCENARY_MANAGER", Some(write_levy_pools(inputs, context.levy_pools, faction)?))?
249 .set_record("CHARACTER_RECRUITMENT_POOL_MANAGER", Some(write_recruitment_pools(inputs, faction)?))?
250 .set_record("CAMPAIGN_POLITICS", Some(write_politics(inputs, faction)?))?
251 .set_record("OLD_DIPLOMACY_MANAGER", Some(write_diplomacy(inputs, plan, context.diplomacy, faction)?))?
252 .set_record("MILITARY_FORCE_LEGACY_NAMES_AND_EMBLEMS", Some(context.armies.write_used_legacies(faction)?))?
253 .set_record("CAMPAIGN_MISSION_MANAGER", mission_manager)?
254 .set_record("CAMPAIGN_SHROUD", shroud)?
255 .set_u32s("FACTION_FLAG_AND_COLOURS@2+19", vec![0; EVENT_COUNTERS])?
256 .set_u32(&format!("FACTION_ACTION_LOG_BLOCK+{}", 11 + log_shift), context.characters.progenitor_family_id(faction))?
257 .set_bool(&format!("FACTION_ACTION_LOG_BLOCK+{}", 12 + log_shift), *faction.culture() != NOMADIC_CULTURE)?;
258
259 record.build()
260}
261
262fn faction_vision(inputs: &WorldInputs, context: &FactionContext, faction: &FactionEntry) -> Result<Vision> {
264 let mut vision = Vision::default();
265 for region in inputs.index.regions_of(faction.key()) {
266 for area in [context.sight.region(region.key()), context.sight.settlement(region.key())].into_iter().flatten() {
267 vision.extend(area);
268 }
269 }
270 for character in context.characters.characters_of(faction) {
271 if let Some(area) = context.characters.vision(character)? {
272 vision.extend(&area);
273 }
274 }
275 Ok(vision)
276}
277
278fn write_technologies(inputs: &WorldInputs, faction: &FactionEntry) -> Result<RecordNode> {
282 let path = format!("{FACTION_PATH}/FACTION_TECHNOLOGY_MANAGER");
283 let tree = faction_technologies(inputs.tables, inputs.index.campaign(), faction)?;
284 let mut researched = child(inputs.layout, &path, "RESEARCHED_TECHS")?;
285 for row in inputs.tables.table("start_pos_technologies")?.rows() {
286 let technology = row.str("technology")?;
287 if row.str("faction")? == *faction.id() && tree.iter().any(|known| *known == technology) {
288 researched.push_group()?
289 .set_str("^+1", &technology)?
290 .set_bool("^+2", true)?;
291 }
292 }
293
294 let mut record = inputs.layout.builder(&path)?;
295 record.group()?
296 .set_str("^+1", faction.key())?
297 .set_record("RESEARCHED_TECHS", Some(researched.build()?))?;
298 record.build()
299}
300
301fn write_faction_mercenary_pool(inputs: &WorldInputs, plan: &WorldPlan, faction: &FactionEntry) -> Result<RecordNode> {
303 let path = format!("{FACTION_PATH}/FACTION_MERCENARY_POOL");
304 let units = faction_mercenary_pool(inputs.tables, faction.key())?;
305 let mut record = inputs.layout.builder(&path)?;
306 record.group()?
307 .set_u32("^+1", plan.faction_id(faction.key()).unwrap_or_default())?
308 .set_record("MERCENARY_POOL", Some(write_mercenary_pool(inputs, &path, &units)?))?;
309 record.build()
310}
311
312fn write_levy_pools(inputs: &WorldInputs, pools: &[PlannedLevyPool], faction: &FactionEntry) -> Result<RecordNode> {
314 let path = format!("{FACTION_PATH}/LEVY_MERCENARY_MANAGER");
315 let pool_path = format!("{path}/LEVY_POOLS_BLOCK/LEVY_MERCENARY_POOL");
316 let mut block = child(inputs.layout, &path, "LEVY_POOLS_BLOCK")?;
317 for pool in pools.iter().filter(|pool| pool.overlord == *faction.key()) {
318 let mut record = inputs.layout.builder(&pool_path)?;
319 record.group()?
320 .set_u32("^+1", pool.id)?
321 .set_record("MERCENARY_POOL", Some(write_mercenary_pool(inputs, &pool_path, &[])?))?
322 .set_u32("MERCENARY_POOL+1", pool.subject)?
323 .set_u32("MERCENARY_POOL+2", pool.province)?;
324 block.push_group()?.set_record("LEVY_MERCENARY_POOL", Some(record.build()?))?;
325 }
326
327 let mut manager = inputs.layout.builder(&path)?;
328 manager.group()?.set_record("LEVY_POOLS_BLOCK", Some(block.build()?))?;
329 manager.build()
330}
331
332fn write_recruitment_pools(inputs: &WorldInputs, faction: &FactionEntry) -> Result<RecordNode> {
334 let path = format!("{FACTION_PATH}/CHARACTER_RECRUITMENT_POOL_MANAGER");
335 let mut block = child(inputs.layout, &path, "CHARACTER_RECRUITMENT_POOL_BLOCK")?;
336 for agent in recruitment_pool_agents(inputs.tables, faction)? {
337 block.push_group()?.set_str("^+1", &agent)?;
338 }
339
340 let mut manager = inputs.layout.builder(&path)?;
341 manager.group()?.set_record("CHARACTER_RECRUITMENT_POOL_BLOCK", Some(block.build()?))?;
342 manager.build()
343}
344
345fn write_diplomacy(inputs: &WorldInputs, plan: &WorldPlan, diplomacy: &Diplomacy, faction: &FactionEntry) -> Result<RecordNode> {
348 let path = format!("{FACTION_PATH}/OLD_DIPLOMACY_MANAGER");
349 let relationship_path = format!("{path}/DIPLOMACY_RELATIONSHIPS_ARRAY/OLD_DIPLOMACY_RELATIONSHIP");
350 let mut array = child(inputs.layout, &path, "DIPLOMACY_RELATIONSHIPS_ARRAY")?;
351 for other in plan.factions().iter().filter(|other| other.key() != faction.key()) {
352 let relationship = diplomacy.relationship(faction.key(), other.key());
353 let other_id = plan.faction_id(other.key()).unwrap_or_default();
354
355 let mut allied = child(inputs.layout, &relationship_path, "ALLIED_IN_WAR_AGAINST")?;
356 for (enemy, defensive_ally) in relationship.allied_against() {
357 allied.push_group()?
358 .set_u32("^+1", plan.faction_id(enemy).unwrap_or_default())?
359 .set_i32("^+2", if *defensive_ally { -1 } else { 0 })?;
360 }
361
362 let (vassal_value, master_value) = relationship.vassal_values();
363 let mut record = inputs.layout.builder(&relationship_path)?;
364 record.group()?
365 .set_u32("^+1", other_id)?
366 .set_bool("^+2", relationship.trade())?
367 .set_i32("^+3", if relationship.allied_against().is_empty() { 0 } else { -1 })?
368 .set_str("^+4", relationship.stance())?
369 .set_i32("^+8", vassal_value)?
370 .set_i32("^+9", master_value)?
371 .set_record("ALLIED_IN_WAR_AGAINST", Some(allied.build()?))?
372 .set_bool("HOSTAGES+2", relationship.known())?
373 .set_bool("HOSTAGES+3", relationship.non_aggression())?
374 .set_i32("HOSTAGES+4", if relationship.military_access() { -1 } else { 0 })?;
375 array.push_group()?
376 .set_u32("^+1", other_id)?
377 .set_record("OLD_DIPLOMACY_RELATIONSHIP", Some(record.build()?))?;
378 }
379
380 let mut manager = inputs.layout.builder(&path)?;
381 manager.group()?.set_record("DIPLOMACY_RELATIONSHIPS_ARRAY", Some(array.build()?))?;
382 manager.build()
383}
384
385fn write_politics(inputs: &WorldInputs, faction: &FactionEntry) -> Result<RecordNode> {
387 let path = format!("{FACTION_PATH}/CAMPAIGN_POLITICS");
388 let parties_path = format!("{path}/POLITICAL_PARTIES");
389 let parties = political_parties(inputs.tables, faction.key())?;
390 let mut array = child(inputs.layout, &path, "POLITICAL_PARTIES")?;
391
392 let party_path = child_path(&parties_path, "CAMPAIGN_POLITICAL_PARTY");
394 let has_power = inputs.layout.record(&party_path).and_then(|layout| layout.leaf_default("^+2")).is_some_and(|default| matches!(default, NodeType::U32(_)));
395 for party in &parties {
396 let mut record = child(inputs.layout, &parties_path, "CAMPAIGN_POLITICAL_PARTY")?;
397 let group = record.group()?;
398 group.set_str("^+1", party.key())?;
399 if has_power {
400 group.set_u32("^+2", party.power())?;
401 }
402 array.push_group()?
403 .set_str("^+1", party.key())?
404 .set_record("CAMPAIGN_POLITICAL_PARTY", Some(record.build()?))?;
405 }
406
407 let mut politics = inputs.layout.builder(&path)?;
408 politics.group()?
409 .set_record("POLITICAL_PARTIES", Some(array.build()?))?
410 .set_bool("POLITICAL_PARTIES+1", !parties.is_empty())?;
411 politics.build()
412}
413
414pub(super) fn write_flag(inputs: &WorldInputs, parent: &str, flag: &FactionFlag) -> Result<RecordNode> {
417 let mut record = child(inputs.layout, parent, "FACTION_FLAG_AND_COLOURS")?;
418 let group = record.group()?;
419 group.set_str("^+1", flag.path())?;
420 for (index, value) in flag.colours().iter().flatten().enumerate() {
421 group.set_u32(&format!("^+{}", index + 2), u32::from(*value))?;
422 }
423 record.build()
424}