rpfm_extensions/startpos/world/writer/
mod.rs1use std::collections::{HashMap, HashSet};
29
30use rpfm_lib::error::{RLibError, Result};
31use rpfm_lib::files::esf::RecordNode;
32
33use self::armies::ArmyWriter;
34use self::characters::CharacterWriter;
35use self::factions::FactionContext;
36use self::forces::ForcePlan;
37use self::government::PostPlan;
38use self::sight::SightPlan;
39use super::{CampaignIndex, CharacterEntry, FactionEntry, RegionEntry};
40use super::diplomacy::Diplomacy;
41use crate::startpos::layout::{child_path, RecordBuilder, StartposLayout};
42use crate::startpos::region_map::RegionMap;
43use crate::startpos::settlements::SettlementLayout;
44use crate::startpos::tables::{Row, StartposTables};
45
46pub mod armies;
47pub mod characters;
48pub mod factions;
49pub mod forces;
50pub mod government;
51pub mod provinces;
52pub mod regions;
53pub mod sight;
54pub mod slaves;
55pub mod spying;
56
57#[cfg(test)] pub(crate) mod writer_test;
58
59pub const WORLD_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD";
61
62pub(crate) const HORDE_BUILDING_COLUMNS: [&str; 11] = [
64 "primary_building", "secondary_building_1", "secondary_building_2", "secondary_building_3", "secondary_building_4",
65 "secondary_building_5", "secondary_building_6", "secondary_building_7", "secondary_building_8",
66 "secondary_building_9", "secondary_building_10",
67];
68
69pub const REBEL_FACTION_ID: u32 = 1;
71
72#[derive(Debug, Clone, Copy)]
78pub struct WorldInputs<'a> {
79
80 pub tables: &'a StartposTables,
82
83 pub index: &'a CampaignIndex,
85
86 pub layout: &'a StartposLayout,
88
89 pub settlements: &'a [SettlementLayout],
91
92 pub region_map: &'a RegionMap,
94
95 pub legacy_overrides: &'a HashSet<String>,
98
99 pub trading: Option<&'a HashSet<(String, String)>>,
102
103 pub game: &'a str,
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct WorldPlan<'a> {
110
111 factions: Vec<&'a FactionEntry>,
113
114 faction_ids: HashMap<String, u32>,
116
117 regions: Vec<&'a RegionEntry>,
119
120 region_ids: HashMap<String, u32>,
122
123 character_orders: HashMap<String, Vec<&'a CharacterEntry>>,
126}
127
128pub fn write_world(inputs: &WorldInputs) -> Result<RecordNode> {
142 write_world_with_plan(inputs, &WorldPlan::new(inputs.index))
143}
144
145pub fn write_world_with_plan<'a>(inputs: &WorldInputs<'a>, plan: &WorldPlan<'a>) -> Result<RecordNode> {
156 let forces = ForcePlan::new(inputs.tables, inputs.index, plan)?;
157 let residences = regions::settlement_residences(inputs, plan)?;
158
159 let posts = PostPlan::new(inputs, plan)?;
160 let sight = SightPlan::new(inputs, plan);
161 let characters = CharacterWriter::new(inputs, plan, &forces, &posts, &sight)?;
162 let provinces = provinces::write_province_and_sea_managers(inputs, plan, &sight)?;
163 let armies = ArmyWriter::new(inputs, &forces, &characters, &residences, provinces.next_recruitment)?;
164 let levy_pools = factions::plan_levy_pools(inputs, plan, &provinces.province_ids, provinces.next_pool)?;
165 let diplomacy = Diplomacy::new(inputs.tables, inputs.index, inputs.region_map, inputs.trading)?;
166 let context = FactionContext { armies: &armies, characters: &characters, posts: &posts, levy_pools: &levy_pools, diplomacy: &diplomacy, sight: &sight };
167
168 let mut world = inputs.layout.builder(WORLD_PATH)?;
169 world.group()?
170 .set_record("FACTION_ARRAY", Some(factions::write_faction_array(inputs, plan, &context)?))?
171 .set_record("REBEL_FACTION", Some(factions::write_rebel_faction(inputs.layout, plan)?))?
172 .set_record("REGION_MANAGER", Some(regions::write_region_manager(inputs, plan, &forces, &sight)?))?
173 .set_record("PROVINCE_MANAGER", Some(provinces.provinces))?
174 .set_record("SEA_REGION_MANAGER", Some(provinces.seas))?
175 .set_record("FAMILY_TREE", Some(characters.write_family_tree()?))?
176 .set_record("SLAVES", Some(slaves::write_slaves(inputs, plan)?))?
177 .set_record("SPYING_ARRAY", Some(spying::write_spying_array(inputs, plan)?))?;
178 world.build()
179}
180
181impl<'a> WorldPlan<'a> {
182
183 pub fn new(index: &'a CampaignIndex) -> Self {
185 Self::with_orders(index.factions_in_startpos_order(), index.regions().iter().collect())
186 }
187
188 pub fn with_orders(factions: Vec<&'a FactionEntry>, regions: Vec<&'a RegionEntry>) -> Self {
195 let faction_ids = factions.iter().zip(REBEL_FACTION_ID + 1..).map(|(faction, id)| (faction.key().to_owned(), id)).collect();
196 let region_ids = regions.iter().zip(1..).map(|(region, id)| (region.key().to_owned(), id)).collect();
197 Self { factions, faction_ids, regions, region_ids, character_orders: HashMap::new() }
198 }
199
200 pub fn set_character_order(&mut self, faction: &str, characters: Vec<&'a CharacterEntry>) {
208 self.character_orders.insert(faction.to_owned(), characters);
209 }
210
211 pub fn character_order(&self, faction: &str) -> Option<&[&'a CharacterEntry]> {
213 self.character_orders.get(faction).map(Vec::as_slice)
214 }
215
216 pub fn factions(&self) -> &[&'a FactionEntry] {
218 &self.factions
219 }
220
221 pub fn regions(&self) -> &[&'a RegionEntry] {
223 &self.regions
224 }
225
226 pub fn region_id(&self, region: &str) -> Option<u32> {
228 self.region_ids.get(region).copied()
229 }
230
231 pub fn faction_id(&self, faction: &str) -> Option<u32> {
233 self.faction_ids.get(faction).copied()
234 }
235}
236
237pub(crate) fn child<'a>(layout: &'a StartposLayout, parent: &str, key: &str) -> Result<RecordBuilder<'a>> {
239 layout.builder(&child_path(parent, key))
240}
241
242pub(crate) fn start_pos_faction_row<'a>(tables: &'a StartposTables, faction: &FactionEntry) -> Result<Row<'a>> {
244 for row in tables.table("start_pos_factions")?.rows() {
245 if row.str("ID")? == *faction.id() {
246 return Ok(row);
247 }
248 }
249 Err(RLibError::StartposInvalidValue("start_pos_factions".to_owned(), "ID".to_owned(), faction.id().to_owned()))
250}
251
252pub(crate) fn horde_details<'a>(inputs: &WorldInputs<'a>) -> Result<Vec<Row<'a>>> {
254 let characters = inputs.index.characters().iter().map(|character| character.id().as_str()).collect::<HashSet<_>>();
255 let mut rows = vec![];
256 for row in inputs.tables.table("start_pos_horde_details")?.rows() {
257 if characters.contains(row.str("general")?.as_ref()) {
258 rows.push(row);
259 }
260 }
261 Ok(rows)
262}
263
264pub(crate) fn next_id(counter: &mut u32) -> u32 {
266 let id = *counter;
267 *counter += 1;
268 id
269}