1use std::collections::HashMap;
15
16use rpfm_lib::error::Result;
17use rpfm_lib::files::esf::RecordNode;
18
19use super::map::MapRegion;
20use super::plan::CaiPlan;
21use super::world_view::WorldView;
22use super::{component, CAI_WORLD_PATH};
23use crate::startpos::layout::{child_path, StartposLayout};
24use crate::startpos::tables::StartposTables;
25use crate::startpos::world::CampaignIndex;
26
27#[cfg(test)] #[path = "factions_test.rs"] mod factions_test;
28
29pub(crate) const REBELS_WORLD_ID: u32 = 1;
31
32const REBELS_PERSONALITY: &str = "default";
34
35#[derive(Debug, Clone, Default)]
41pub struct FactionLinks {
42
43 world_id: u32,
45
46 regions: Vec<u32>,
48
49 groups: Vec<u32>,
51
52 mobiles: Vec<u32>,
54
55 characters: Vec<u32>,
57
58 capital: u32,
60
61 leader: u32,
63
64 posts: Vec<u32>,
66
67 provinces: Vec<u32>,
69
70 seas: Vec<u32>,
72
73 governorship: u32,
75
76 personality: String,
78
79 starting_personality: String,
81
82 victory_regions: Vec<u32>,
84
85 horde: Vec<u32>,
87}
88
89pub fn capitals(index: &CampaignIndex, world: &WorldView, plan: &CaiPlan, map_regions: &[MapRegion]) -> HashMap<usize, u32> {
102 world.factions().iter().zip(plan.factions())
103 .filter_map(|(faction, ids)| {
104 let capital = index.capital_of(faction.key())?;
105 let region = map_regions.iter().position(|region| region.key() == capital.key())?;
106 Some((region, ids.faction()))
107 })
108 .collect()
109}
110
111pub fn victory_factions(tables: &StartposTables, index: &CampaignIndex, world: &WorldView, plan: &CaiPlan) -> Result<HashMap<String, Vec<u32>>> {
124 let components = world.factions().iter().zip(plan.factions()).map(|(faction, ids)| (faction.key().as_str(), ids.faction())).collect::<HashMap<_, _>>();
125 let mut factions: HashMap<String, Vec<u32>> = HashMap::new();
126 for row in tables.table("start_pos_victory_conditions")?.rows() {
127 let Some(component) = index.faction_by_id(&row.str("start_pos_faction")?).and_then(|faction| components.get(faction.key().as_str())) else { continue };
128 let list = factions.entry(row.str("region")?.into_owned()).or_default();
129 if !list.contains(component) {
130 list.push(*component);
131 }
132 }
133 for list in factions.values_mut() {
134 list.sort_unstable();
135 }
136 Ok(factions)
137}
138
139pub fn faction_links(tables: &StartposTables, index: &CampaignIndex, world: &WorldView, plan: &CaiPlan, map_regions: &[MapRegion]) -> Result<Vec<FactionLinks>> {
153 let region_index = map_regions.iter().enumerate().map(|(position, region)| (region.key().as_str(), position)).collect::<HashMap<_, _>>();
154 let region_component = |key: &str| region_index.get(key).map(|position| plan.regions()[*position].id());
155 let mut personalities = HashMap::new();
156 for row in tables.table("start_pos_factions")?.rows() {
157 if let Some(faction) = index.faction_by_id(&row.str("ID")?) {
158 personalities.insert(faction.key().to_owned(), (row.str("cai_personality_group")?.into_owned(), row.str("cai_starting_personality")?.into_owned()));
159 }
160 }
161 let mut victory_regions: HashMap<String, Vec<u32>> = HashMap::new();
162 for row in tables.table("start_pos_victory_conditions")?.rows() {
163 if let (Some(faction), Some(region)) = (index.faction_by_id(&row.str("start_pos_faction")?), region_component(&row.str("region")?)) {
164 victory_regions.entry(faction.key().to_owned()).or_default().push(region);
165 }
166 }
167
168 let owned_regions = |owner: u32| -> Vec<u32> {
170 let mut regions = world.regions().iter().filter(|region| region.owner() == owner).filter_map(|region| region_index.get(region.key().as_str()).copied()).collect::<Vec<_>>();
171 regions.sort_unstable();
172 regions.into_iter().map(|position| plan.regions()[position].id()).collect()
173 };
174 let parts = |owner: u32| -> (Vec<u32>, Vec<u32>) {
175 let provinces = world.provinces().iter().zip(plan.provinces())
176 .flat_map(|(province, ids)| province.managers().iter().zip(ids.faction_provinces()))
177 .filter(|(part, _)| part.faction() == owner)
178 .map(|(_, id)| *id)
179 .collect();
180 let seas = world.seas().iter().zip(plan.faction_seas())
181 .flat_map(|(sea, ids)| sea.managers().iter().zip(ids))
182 .filter(|(part, _)| part.faction() == owner)
183 .map(|(_, id)| *id)
184 .collect();
185 (provinces, seas)
186 };
187
188 let mut links = vec![];
189 for (position, faction) in world.factions().iter().enumerate() {
190 let characters = &plan.characters()[position];
191 let regions = owned_regions(faction.id());
192 let mobiles = characters.iter().filter_map(|character| character.mobile()).collect::<Vec<_>>();
193 let leader_id = faction.posts().iter().map(|(_, holder)| *holder).find(|holder| *holder != 0);
194 let leader = leader_id.and_then(|leader| faction.characters().iter().position(|character| character.id() == leader)).map(|position| characters[position].character()).unwrap_or_default();
195 let (provinces, seas) = parts(faction.id());
196 let (personality, starting_personality) = personalities.get(faction.key()).cloned().unwrap_or_default();
197 links.push(FactionLinks {
198 world_id: faction.id(),
199 groups: if regions.is_empty() { vec![] } else { vec![0] },
200 horde: if regions.is_empty() { mobiles.to_vec() } else { vec![] },
201 regions,
202 characters: characters.iter().map(|character| character.character()).collect(),
203 mobiles,
204 capital: index.capital_of(faction.key()).and_then(|capital| region_component(capital.key())).unwrap_or_default(),
205 leader,
206 posts: plan.posts()[position].to_vec(),
207 provinces,
208 seas,
209 governorship: plan.governorships()[position],
210 personality,
211 starting_personality,
212 victory_regions: victory_regions.remove(faction.key()).unwrap_or_default(),
213 });
214 }
215
216 let regions = owned_regions(REBELS_WORLD_ID);
217 let (provinces, seas) = parts(REBELS_WORLD_ID);
218 links.push(FactionLinks {
219 world_id: REBELS_WORLD_ID,
220 groups: if regions.is_empty() { vec![] } else { vec![0] },
221 regions,
222 provinces,
223 seas,
224 personality: REBELS_PERSONALITY.to_owned(),
225 ..Default::default()
226 });
227 Ok(links)
228}
229
230pub fn write_factions(layout: &StartposLayout, plan: &CaiPlan, links: &[FactionLinks]) -> Result<RecordNode> {
242 let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_FACTIONS");
243 let faction_path = child_path(&path, "CAI_FACTION");
244
245 let mut factions = layout.builder(&path)?;
246 for (position, (ids, links)) in plan.factions().iter().zip(links).enumerate() {
247 let mut personality = layout.builder(&child_path(&faction_path, "CAI_FACTION_PERSONALITY"))?;
248 personality.group()?
249 .set_u32("^+1", ids.faction())?
250 .set_str("^+2", &links.starting_personality)?
251 .set_str("^+3", &links.personality)?;
252
253 let mut record = layout.builder(&faction_path)?;
254 record.group()?
255 .set_u32s("^+1", links.regions.to_vec())?
256 .set_u32s("^+2", if links.regions.is_empty() { vec![] } else { vec![plan.theatre()] })?
257 .set_u32s("^+3", links.groups.to_vec())?
258 .set_u32s("^+4", links.mobiles.to_vec())?
259 .set_u32s("^+5", links.characters.to_vec())?
260 .set_u32("^+6", links.capital)?
261 .set_u32("^+7", links.world_id)?
262 .set_u32s("^+8", vec![])?
263 .set_u32("^+9", ids.tree().unwrap_or_default())?
264 .set_u32s("^+10", if links.governorship == 0 { vec![] } else { vec![links.governorship] })?
265 .set_record("CAI_BDI_COMPONENT", Some(component(layout, &faction_path, ids.cai_faction(), vec![])?))?
266 .set_u32("CAI_BDI_NEW_TURN+1", links.capital)?
267 .set_u32s("CAI_BDI_NEW_TURN+2", links.victory_regions.to_vec())?
268 .set_u32("CAI_BDI_NEW_TURN+3", links.leader)?
269 .set_u32s("CAI_BDI_NEW_TURN+4", links.posts.to_vec())?
270 .set_u32s("CAI_BDI_NEW_TURN+5", links.provinces.to_vec())?
271 .set_record("CAI_FACTION_PERSONALITY", Some(personality.build()?))?
272 .set_u32s("CAI_FACTION_PERSONALITY+1", links.seas.to_vec())?
273 .set_u32("CAI_FACTION_PERSONALITY+2", (position + 1) as u32)?
274 .set_record("CAI_FACTION_HORDE_MOBILE_GROUPS", Some(write_horde_groups(layout, &faction_path, &links.horde)?))?;
275
276 factions.push_group()?
277 .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, ids.faction())?))?
278 .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, ids.faction(), vec![])?))?
279 .set_record("CAI_FACTION", Some(record.build()?))?;
280 }
281 factions.build()
282}
283
284pub fn write_technology_trees(layout: &StartposLayout, plan: &CaiPlan) -> Result<RecordNode> {
290 let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_TECHNOLOGY_TREES");
291 let mut trees = layout.builder(&path)?;
292 for ids in plan.factions() {
293 let Some(tree_id) = ids.tree() else { continue };
294 let mut tree = layout.builder(&child_path(&path, "CAI_TECHNOLOGY_TREE"))?;
295 tree.group()?.set_u32s("^+1", ids.technologies().to_vec())?;
296
297 trees.push_group()?
298 .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, tree_id, vec![])?))?
299 .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, ids.faction())?))?
300 .set_record("CAI_TECHNOLOGY_TREE", Some(tree.build()?))?;
301 }
302 trees.build()
303}
304
305pub fn write_technologies(layout: &StartposLayout, plan: &CaiPlan, technologies: &[Vec<String>]) -> Result<RecordNode> {
317 let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_TECHNOLOGIES");
318 let mut list = layout.builder(&path)?;
319 for (ids, keys) in plan.factions().iter().zip(technologies) {
320 let tree_id = ids.tree().unwrap_or_default();
321 for (id, key) in ids.technologies().iter().zip(keys) {
322 let mut technology = layout.builder(&child_path(&path, "CAI_TECHNOLOGY"))?;
323 technology.group()?
324 .set_u32("^+1", tree_id)?
325 .set_str("^+2", key)?;
326
327 list.push_group()?
328 .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, *id, vec![])?))?
329 .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, ids.faction())?))?
330 .set_record("CAI_TECHNOLOGY", Some(technology.build()?))?;
331 }
332 }
333 list.build()
334}
335
336fn write_horde_groups(layout: &StartposLayout, faction_path: &str, horde: &[u32]) -> Result<RecordNode> {
338 let path = child_path(faction_path, "CAI_FACTION_HORDE_MOBILE_GROUPS");
339 let mut groups = layout.builder(&path)?;
340 groups.group()?.set_u32("^+1", u32::from(!horde.is_empty()))?;
341 if let Some(leader) = horde.first() {
342 let mut group = layout.builder(&child_path(&path, "CAI_FACTION_HORDE_MOBILE_GROUP"))?;
343 group.group()?
344 .set_u32s("^+1", horde.to_vec())?
345 .set_u32("^+2", *leader)?;
346 groups.group()?.set_record("CAI_FACTION_HORDE_MOBILE_GROUP", Some(group.build()?))?;
347 }
348 groups.build()
349}
350
351pub(crate) fn owned_direct(layout: &StartposLayout, list_path: &str, owner: u32) -> Result<RecordNode> {
353 let mut record = layout.builder(&child_path(list_path, "OWNED_DIRECT"))?;
354 record.group()?.set_u32("^+1", owner)?;
355 record.build()
356}