rpfm_extensions/startpos/extract/
mod.rs1use getset::Getters;
33
34use std::collections::{BTreeMap, HashMap};
35
36use rpfm_lib::error::{RLibError, Result};
37use rpfm_lib::files::db::DB;
38use rpfm_lib::files::esf::{NodeType, RecordNode, ESF};
39use rpfm_lib::files::table::DecodedData;
40use rpfm_lib::schema::Schema;
41
42use super::cai::CAI_WORLD_PATH;
43use super::layout::{child_path, GroupView, StartposLayout};
44use super::missions::{write_victory_objectives, VictoryObjectives};
45use super::tables::StartposTables;
46use super::world::writer::factions::FACTION_KEY_FIELDS;
47use super::world::writer::WORLD_PATH;
48
49pub mod compare;
50mod characters;
51mod factions;
52mod missions;
53mod regions;
54
55#[cfg(test)] pub(crate) mod extract_test;
56
57const ROOT_PATH: &str = "CAMPAIGN_STARTPOS";
59
60const PREOPEN_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_PREOPEN_MAP_INFO";
62
63const ENV_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV";
65
66const MODEL_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL";
68
69const FACTION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION";
71
72pub const EXTRACTION_TABLES: [&str; 2] = ["character_trait_levels", "seasons"];
74
75const DESCRIPTION_KEY_PREFIX: &str = "start_pos_factions_description_";
77
78#[derive(Debug, Clone, Default, Getters)]
84pub struct ExtractedTables {
85
86 #[getset(get = "pub")]
88 campaign: String,
89
90 tables: BTreeMap<String, TableRows>,
92
93 #[getset(get = "pub")]
95 victory_objectives: Vec<VictoryObjectives>,
96}
97
98#[derive(Debug, Clone, Default, PartialEq, Getters)]
100#[getset(get = "pub")]
101pub struct TableRows {
102
103 columns: Vec<&'static str>,
105
106 rows: Vec<Vec<String>>,
108}
109
110struct Source<'a> {
112
113 layout: &'a StartposLayout,
115
116 campaign: String,
118
119 preopen: GroupView<'a>,
121
122 calendar: GroupView<'a>,
124
125 start_date: GroupView<'a>,
127
128 factions: Vec<FactionSource<'a>>,
130
131 regions: Vec<GroupView<'a>>,
133
134 provinces: Vec<GroupView<'a>>,
136
137 world: GroupView<'a>,
139
140 cai_factions: Vec<GroupView<'a>>,
142}
143
144struct FactionSource<'a> {
146
147 group: GroupView<'a>,
149
150 info: Option<GroupView<'a>>,
152
153 id: u32,
155
156 key: String,
158
159 table_id: String,
161}
162
163#[derive(Debug, Default)]
165struct Ids {
166 last: u64,
167}
168
169pub fn extract_tables(startpos: &ESF, layout: &StartposLayout, tables: &StartposTables) -> Result<ExtractedTables> {
189 let source = Source::read(startpos, layout)?;
190 let mut ids = Ids::default();
191 let mut extracted = ExtractedTables { campaign: source.campaign.to_owned(), tables: BTreeMap::new(), victory_objectives: missions::extract(&source)? };
192
193 extracted.insert("start_pos_calendars", factions::calendar(&source, tables)?);
194 extracted.insert("start_pos_factions", factions::factions(&source)?);
195 extracted.insert("start_pos_technologies", factions::technologies(&source)?);
196 extracted.insert("start_pos_victory_conditions", factions::victory_conditions(&source));
197 extracted.insert("start_pos_diplomacy", factions::diplomacy(&source, &mut ids)?);
198
199 let regions = regions::extract(&source, &mut ids)?;
200 let characters = characters::extract(&source, tables, ®ions, &mut ids)?;
201 for (name, rows) in regions.tables.into_iter().chain(characters) {
202 extracted.insert(name, rows);
203 }
204 Ok(extracted)
205}
206
207impl ExtractedTables {
208
209 pub fn table(&self, name: &str) -> Option<&TableRows> {
211 self.tables.get(name)
212 }
213
214 pub fn tables(&self) -> impl Iterator<Item = (&str, &TableRows)> {
216 self.tables.iter().map(|(name, rows)| (name.as_str(), rows))
217 }
218
219 pub fn victory_objectives_script(&self) -> String {
221 write_victory_objectives(&self.victory_objectives)
222 }
223
224 pub fn to_dbs(&self, schema: &Schema) -> Result<Vec<DB>> {
235 self.tables.iter().map(|(name, rows)| {
236 let table_name = format!("{name}_tables");
237 let definition = schema.definitions_by_table_name(&table_name)
238 .and_then(|definitions| definitions.iter().max_by_key(|definition| *definition.version()))
239 .ok_or_else(|| extraction_error(&format!("the schema has no definition for {table_name}")))?;
240
241 let mut db = DB::new(definition, None, &table_name);
242 let fields = definition.fields_processed();
243 let positions = rows.columns.iter()
244 .map(|column| fields.iter().position(|field| field.name() == *column))
245 .collect::<Vec<_>>();
246
247 let mut data = Vec::with_capacity(rows.rows.len());
248 for row in &rows.rows {
249 let mut new_row = db.new_row();
250 for (position, value) in positions.iter().zip(row) {
251 if let Some(position) = *position {
252 new_row[position] = DecodedData::new_from_type_and_string(fields[position].field_type(), value)?;
253 }
254 }
255 data.push(new_row);
256 }
257 db.set_data(&data)?;
258 Ok(db)
259 }).collect()
260 }
261
262 pub fn to_tables(&self, schema: &Schema) -> Result<StartposTables> {
268 let mut tables = StartposTables::new();
269 for db in self.to_dbs(schema)? {
270 tables.add_db(&db)?;
271 }
272 Ok(tables)
273 }
274
275 fn insert(&mut self, name: &str, rows: TableRows) {
277 self.tables.insert(name.to_owned(), rows);
278 }
279}
280
281impl TableRows {
282
283 fn new(columns: &[&'static str]) -> Self {
285 Self { columns: columns.to_vec(), rows: vec![] }
286 }
287
288 fn push(&mut self, row: Vec<String>) {
290 debug_assert_eq!(row.len(), self.columns.len(), "a row of the extracted tables doesn't fit its columns");
291 self.rows.push(row);
292 }
293}
294
295impl<'a> Source<'a> {
296
297 fn read(startpos: &'a ESF, layout: &'a StartposLayout) -> Result<Self> {
299 let NodeType::Record(root) = startpos.root_node() else { return Err(extraction_error("the startpos has no root record")) };
300 let root = layout.read_single(ROOT_PATH, root)?;
301 let preopen = layout.read_single(PREOPEN_PATH, required(&root, "CAMPAIGN_PREOPEN_MAP_INFO")?)?;
302 let env = layout.read_single(ENV_PATH, required(&root, "CAMPAIGN_ENV")?)?;
303 let model = layout.read_single(MODEL_PATH, required(&env, "CAMPAIGN_MODEL")?)?;
304 let world = layout.read_single(WORLD_PATH, required(&model, "WORLD")?)?;
305
306 let calendar_path = child_path(MODEL_PATH, "CAMPAIGN_CALENDAR");
307 let calendar = layout.read_single(&calendar_path, required(&model, "CAMPAIGN_CALENDAR")?)?;
308 let start_date = layout.read_single(&child_path(&calendar_path, "DATE"), required(&calendar, "DATE")?)?;
309
310 let infos_path = child_path(PREOPEN_PATH, "FACTION_INFOS");
311 let infos = match preopen.record("FACTION_INFOS") {
312 Some(record) => layout.read(&infos_path, record)?,
313 None => vec![],
314 };
315 let mut infos = infos.into_iter()
316 .filter_map(|info| Some((info.str("^+1")?.to_owned(), info)))
317 .collect::<HashMap<_, _>>();
318
319 let factions = layout.read_items(&child_path(WORLD_PATH, "FACTION_ARRAY"), &world, "FACTION_ARRAY", "FACTION")?.into_iter()
320 .map(|(_, group)| {
321 let key = FACTION_KEY_FIELDS.iter().find_map(|field| group.str(field)).unwrap_or_default().to_owned();
322 let info = infos.remove(&key);
323 let table_id = info.as_ref()
324 .and_then(|info| info.str("^+5"))
325 .and_then(|description| description.strip_prefix(DESCRIPTION_KEY_PREFIX))
326 .unwrap_or_default()
327 .to_owned();
328 FactionSource { id: group.u32("^+1").unwrap_or_default(), group, info, key, table_id }
329 })
330 .collect();
331
332 let region_manager_path = child_path(WORLD_PATH, "REGION_MANAGER");
333 let region_manager = layout.read_single(®ion_manager_path, required(&world, "REGION_MANAGER")?)?;
334 let regions = layout.read_items(&child_path(®ion_manager_path, "REGIONS_ARRAY"), ®ion_manager, "REGIONS_ARRAY", "REGION")?.into_iter()
335 .map(|(_, region)| region)
336 .collect();
337
338 let province_manager_path = child_path(WORLD_PATH, "PROVINCE_MANAGER");
339 let province_manager = layout.read_single(&province_manager_path, required(&world, "PROVINCE_MANAGER")?)?;
340 let provinces = layout.read_items(&child_path(&province_manager_path, "PROVINCE_ARRAY"), &province_manager, "PROVINCE_ARRAY", "PROVINCE")?.into_iter()
341 .map(|(_, province)| province)
342 .collect();
343
344 let cai_interface_path = child_path(MODEL_PATH, "CAI_INTERFACE");
345 let mut cai_factions = vec![];
346 if let Some(interface) = model.record("CAI_INTERFACE") {
347 let interface = layout.read_single(&cai_interface_path, interface)?;
348 if let Some(cai_world) = interface.record("CAI_WORLD") {
349 let cai_world = layout.read_single(CAI_WORLD_PATH, cai_world)?;
350 let factions_path = child_path(CAI_WORLD_PATH, "CAI_WORLD_FACTIONS");
351 cai_factions = layout.read_items(&factions_path, &cai_world, "CAI_WORLD_FACTIONS", "CAI_FACTION")?.into_iter()
352 .map(|(_, faction)| faction)
353 .collect();
354 }
355 }
356
357 Ok(Self {
358 layout,
359 campaign: preopen.str("DATE+1").unwrap_or_default().to_owned(),
360 preopen,
361 calendar,
362 start_date,
363 factions,
364 regions,
365 provinces,
366 world,
367 cai_factions,
368 })
369 }
370
371 fn start_year(&self) -> u32 {
373 self.start_date.u32("^+1").unwrap_or_default()
374 }
375
376 fn faction(&self, id: u32) -> Option<&FactionSource<'a>> {
378 self.factions.iter().find(|faction| faction.id == id)
379 }
380
381 fn faction_by_key(&self, key: &str) -> Option<&FactionSource<'a>> {
383 self.factions.iter().find(|faction| faction.key == key)
384 }
385
386 fn faction_record(&self, faction: &FactionSource<'a>, key: &str) -> Result<Option<GroupView<'a>>> {
388 faction.group.record(key)
389 .map(|record| self.layout.read_single(&child_path(FACTION_PATH, key), record))
390 .transpose()
391 }
392}
393
394impl Ids {
395
396 fn next(&mut self) -> String {
398 self.last += 1;
399 self.last.to_string()
400 }
401}
402
403fn required<'a>(group: &GroupView<'a>, key: &str) -> Result<&'a RecordNode> {
405 group.record(key).ok_or_else(|| extraction_error(&format!("missing {key}")))
406}
407
408fn extraction_error(message: &str) -> RLibError {
410 RLibError::StartposExtractionError(message.to_owned())
411}