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::WORLD_PATH;
47
48pub mod compare;
49mod characters;
50mod factions;
51mod missions;
52mod regions;
53
54#[cfg(test)] pub(crate) mod extract_test;
55
56const ROOT_PATH: &str = "CAMPAIGN_STARTPOS";
58
59const PREOPEN_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_PREOPEN_MAP_INFO";
61
62const ENV_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV";
64
65const MODEL_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL";
67
68const FACTION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION";
70
71pub const EXTRACTION_TABLES: [&str; 2] = ["character_trait_levels", "seasons"];
73
74const DESCRIPTION_KEY_PREFIX: &str = "start_pos_factions_description_";
76
77#[derive(Debug, Clone, Default, Getters)]
83pub struct ExtractedTables {
84
85 #[getset(get = "pub")]
87 campaign: String,
88
89 tables: BTreeMap<String, TableRows>,
91
92 #[getset(get = "pub")]
94 victory_objectives: Vec<VictoryObjectives>,
95}
96
97#[derive(Debug, Clone, Default, PartialEq, Getters)]
99#[getset(get = "pub")]
100pub struct TableRows {
101
102 columns: Vec<&'static str>,
104
105 rows: Vec<Vec<String>>,
107}
108
109struct Source<'a> {
111
112 layout: &'a StartposLayout,
114
115 campaign: String,
117
118 preopen: GroupView<'a>,
120
121 calendar: GroupView<'a>,
123
124 start_date: GroupView<'a>,
126
127 factions: Vec<FactionSource<'a>>,
129
130 regions: Vec<GroupView<'a>>,
132
133 provinces: Vec<GroupView<'a>>,
135
136 world: GroupView<'a>,
138
139 cai_factions: Vec<GroupView<'a>>,
141}
142
143struct FactionSource<'a> {
145
146 group: GroupView<'a>,
148
149 info: Option<GroupView<'a>>,
151
152 id: u32,
154
155 key: String,
157
158 table_id: String,
160}
161
162#[derive(Debug, Default)]
164struct Ids {
165 last: u64,
166}
167
168pub fn extract_tables(startpos: &ESF, layout: &StartposLayout, tables: &StartposTables) -> Result<ExtractedTables> {
188 let source = Source::read(startpos, layout)?;
189 let mut ids = Ids::default();
190 let mut extracted = ExtractedTables { campaign: source.campaign.to_owned(), tables: BTreeMap::new(), victory_objectives: missions::extract(&source)? };
191
192 extracted.insert("start_pos_calendars", factions::calendar(&source, tables)?);
193 extracted.insert("start_pos_factions", factions::factions(&source)?);
194 extracted.insert("start_pos_technologies", factions::technologies(&source)?);
195 extracted.insert("start_pos_victory_conditions", factions::victory_conditions(&source));
196 extracted.insert("start_pos_diplomacy", factions::diplomacy(&source, &mut ids)?);
197
198 let regions = regions::extract(&source, &mut ids)?;
199 let characters = characters::extract(&source, tables, ®ions, &mut ids)?;
200 for (name, rows) in regions.tables.into_iter().chain(characters) {
201 extracted.insert(name, rows);
202 }
203 Ok(extracted)
204}
205
206impl ExtractedTables {
207
208 pub fn table(&self, name: &str) -> Option<&TableRows> {
210 self.tables.get(name)
211 }
212
213 pub fn tables(&self) -> impl Iterator<Item = (&str, &TableRows)> {
215 self.tables.iter().map(|(name, rows)| (name.as_str(), rows))
216 }
217
218 pub fn victory_objectives_script(&self) -> String {
220 write_victory_objectives(&self.victory_objectives)
221 }
222
223 pub fn to_dbs(&self, schema: &Schema) -> Result<Vec<DB>> {
234 self.tables.iter().map(|(name, rows)| {
235 let table_name = format!("{name}_tables");
236 let definition = schema.definitions_by_table_name(&table_name)
237 .and_then(|definitions| definitions.iter().max_by_key(|definition| *definition.version()))
238 .ok_or_else(|| extraction_error(&format!("the schema has no definition for {table_name}")))?;
239
240 let mut db = DB::new(definition, None, &table_name);
241 let fields = definition.fields_processed();
242 let positions = rows.columns.iter()
243 .map(|column| fields.iter().position(|field| field.name() == *column))
244 .collect::<Vec<_>>();
245
246 let mut data = Vec::with_capacity(rows.rows.len());
247 for row in &rows.rows {
248 let mut new_row = db.new_row();
249 for (position, value) in positions.iter().zip(row) {
250 if let Some(position) = *position {
251 new_row[position] = DecodedData::new_from_type_and_string(fields[position].field_type(), value)?;
252 }
253 }
254 data.push(new_row);
255 }
256 db.set_data(&data)?;
257 Ok(db)
258 }).collect()
259 }
260
261 pub fn to_tables(&self, schema: &Schema) -> Result<StartposTables> {
267 let mut tables = StartposTables::new();
268 for db in self.to_dbs(schema)? {
269 tables.add_db(&db)?;
270 }
271 Ok(tables)
272 }
273
274 fn insert(&mut self, name: &str, rows: TableRows) {
276 self.tables.insert(name.to_owned(), rows);
277 }
278}
279
280impl TableRows {
281
282 fn new(columns: &[&'static str]) -> Self {
284 Self { columns: columns.to_vec(), rows: vec![] }
285 }
286
287 fn push(&mut self, row: Vec<String>) {
289 debug_assert_eq!(row.len(), self.columns.len(), "a row of the extracted tables doesn't fit its columns");
290 self.rows.push(row);
291 }
292}
293
294impl<'a> Source<'a> {
295
296 fn read(startpos: &'a ESF, layout: &'a StartposLayout) -> Result<Self> {
298 let NodeType::Record(root) = startpos.root_node() else { return Err(extraction_error("the startpos has no root record")) };
299 let root = layout.read_single(ROOT_PATH, root)?;
300 let preopen = layout.read_single(PREOPEN_PATH, required(&root, "CAMPAIGN_PREOPEN_MAP_INFO")?)?;
301 let env = layout.read_single(ENV_PATH, required(&root, "CAMPAIGN_ENV")?)?;
302 let model = layout.read_single(MODEL_PATH, required(&env, "CAMPAIGN_MODEL")?)?;
303 let world = layout.read_single(WORLD_PATH, required(&model, "WORLD")?)?;
304
305 let calendar_path = child_path(MODEL_PATH, "CAMPAIGN_CALENDAR");
306 let calendar = layout.read_single(&calendar_path, required(&model, "CAMPAIGN_CALENDAR")?)?;
307 let start_date = layout.read_single(&child_path(&calendar_path, "DATE"), required(&calendar, "DATE")?)?;
308
309 let infos_path = child_path(PREOPEN_PATH, "FACTION_INFOS");
310 let infos = match preopen.record("FACTION_INFOS") {
311 Some(record) => layout.read(&infos_path, record)?,
312 None => vec![],
313 };
314 let mut infos = infos.into_iter()
315 .filter_map(|info| Some((info.str("^+1")?.to_owned(), info)))
316 .collect::<HashMap<_, _>>();
317
318 let factions = layout.read_items(&child_path(WORLD_PATH, "FACTION_ARRAY"), &world, "FACTION_ARRAY", "FACTION")?.into_iter()
319 .map(|(_, group)| {
320 let key = group.str("^+2").unwrap_or_default().to_owned();
321 let info = infos.remove(&key);
322 let table_id = info.as_ref()
323 .and_then(|info| info.str("^+5"))
324 .and_then(|description| description.strip_prefix(DESCRIPTION_KEY_PREFIX))
325 .unwrap_or_default()
326 .to_owned();
327 FactionSource { id: group.u32("^+1").unwrap_or_default(), group, info, key, table_id }
328 })
329 .collect();
330
331 let region_manager_path = child_path(WORLD_PATH, "REGION_MANAGER");
332 let region_manager = layout.read_single(®ion_manager_path, required(&world, "REGION_MANAGER")?)?;
333 let regions = layout.read_items(&child_path(®ion_manager_path, "REGIONS_ARRAY"), ®ion_manager, "REGIONS_ARRAY", "REGION")?.into_iter()
334 .map(|(_, region)| region)
335 .collect();
336
337 let province_manager_path = child_path(WORLD_PATH, "PROVINCE_MANAGER");
338 let province_manager = layout.read_single(&province_manager_path, required(&world, "PROVINCE_MANAGER")?)?;
339 let provinces = layout.read_items(&child_path(&province_manager_path, "PROVINCE_ARRAY"), &province_manager, "PROVINCE_ARRAY", "PROVINCE")?.into_iter()
340 .map(|(_, province)| province)
341 .collect();
342
343 let cai_interface_path = child_path(MODEL_PATH, "CAI_INTERFACE");
344 let mut cai_factions = vec![];
345 if let Some(interface) = model.record("CAI_INTERFACE") {
346 let interface = layout.read_single(&cai_interface_path, interface)?;
347 if let Some(cai_world) = interface.record("CAI_WORLD") {
348 let cai_world = layout.read_single(CAI_WORLD_PATH, cai_world)?;
349 let factions_path = child_path(CAI_WORLD_PATH, "CAI_WORLD_FACTIONS");
350 cai_factions = layout.read_items(&factions_path, &cai_world, "CAI_WORLD_FACTIONS", "CAI_FACTION")?.into_iter()
351 .map(|(_, faction)| faction)
352 .collect();
353 }
354 }
355
356 Ok(Self {
357 layout,
358 campaign: preopen.str("DATE+1").unwrap_or_default().to_owned(),
359 preopen,
360 calendar,
361 start_date,
362 factions,
363 regions,
364 provinces,
365 world,
366 cai_factions,
367 })
368 }
369
370 fn start_year(&self) -> u32 {
372 self.start_date.u32("^+1").unwrap_or_default()
373 }
374
375 fn faction(&self, id: u32) -> Option<&FactionSource<'a>> {
377 self.factions.iter().find(|faction| faction.id == id)
378 }
379
380 fn faction_by_key(&self, key: &str) -> Option<&FactionSource<'a>> {
382 self.factions.iter().find(|faction| faction.key == key)
383 }
384
385 fn faction_record(&self, faction: &FactionSource<'a>, key: &str) -> Result<Option<GroupView<'a>>> {
387 faction.group.record(key)
388 .map(|record| self.layout.read_single(&child_path(FACTION_PATH, key), record))
389 .transpose()
390 }
391}
392
393impl Ids {
394
395 fn next(&mut self) -> String {
397 self.last += 1;
398 self.last.to_string()
399 }
400}
401
402fn required<'a>(group: &GroupView<'a>, key: &str) -> Result<&'a RecordNode> {
404 group.record(key).ok_or_else(|| extraction_error(&format!("missing {key}")))
405}
406
407fn extraction_error(message: &str) -> RLibError {
409 RLibError::StartposExtractionError(message.to_owned())
410}