1use anyhow::{anyhow, Result};
7
8use std::collections::{BTreeMap, HashMap, HashSet};
9use std::path::{Path, PathBuf};
10use std::process::{Command as SysCommand, Stdio};
11use std::slice::from_ref;
12use std::time::Instant;
13
14use rpfm_lib::files::{Container, ContainerPath, db::DB, DecodeableExtraData, FileType, loc::Loc, pack::Pack, RFile, RFileDecoded, table::DecodedData};
15use rpfm_lib::schema::*;
16
17use rpfm_ipc::api::tools::CeoEntryData;
18
19use rpfm_telemetry::*;
20
21pub fn build_ceo_entries(
26 pack: &mut Pack,
27 schema: &Schema,
28 entries: &[CeoEntryData],
29) -> Result<Vec<ContainerPath>> {
30 let mut added_paths: Vec<ContainerPath> = Vec::new();
31
32 fn crc32_id(data: &[u8]) -> i64 {
36 let mut crc: u32 = 0xFFFF_FFFF;
37 for &b in data {
38 crc ^= b as u32;
39 for _ in 0..8 {
40 if crc & 1 != 0 { crc = (crc >> 1) ^ 0xEDB8_8320; }
41 else { crc >>= 1; }
42 }
43 }
44 (1_500_000_000u32 + (!crc % 647_000_000u32)) as i64
45 }
46
47 fn auto_id(table: &str, row_key: &str) -> i64 {
48 crc32_id(format!("{}|{}", table, row_key).as_bytes())
49 }
50
51 fn get_or_create_db(
54 pack: &mut Pack,
55 schema: &Schema,
56 table_name: &str,
57 file_stem: &str,
58 ) -> Result<String> {
59 let path = format!("ceo_db/{}/{}", table_name, file_stem);
60 let container_path = ContainerPath::File(path.clone());
61
62 if pack.files_by_paths(from_ref(&container_path), false).is_empty() {
64 let _full_table_name = format!("{}_tables", table_name.trim_end_matches("_tables"));
65 let table_name_with_suffix = if table_name.ends_with("_tables") {
66 table_name.to_string()
67 } else {
68 format!("{}_tables", table_name)
69 };
70
71 let def = schema.definitions_by_table_name(&table_name_with_suffix)
72 .and_then(|defs| defs.first())
73 .ok_or_else(|| anyhow!("No schema definition for {}", table_name_with_suffix))?;
74
75 let patches = schema.patches_for_table(&table_name_with_suffix);
76 let db = DB::new(def, patches, &table_name_with_suffix);
77 let rfile = RFile::new_from_decoded(
78 &RFileDecoded::DB(db), 0, &path
79 );
80 pack.insert(rfile)?;
81 }
82 Ok(path)
83 }
84
85 fn insert_row(
88 pack: &mut Pack,
89 schema: &Schema,
90 table_name: &str,
91 file_stem: &str,
92 values: &std::collections::HashMap<&str, String>,
93 ) -> Result<ContainerPath> {
94 let path = get_or_create_db(pack, schema, table_name, file_stem)?;
95 let container_path = ContainerPath::File(path.clone());
96
97 let _table_name_with_suffix = if table_name.ends_with("_tables") {
98 table_name.to_string()
99 } else {
100 format!("{}_tables", table_name)
101 };
102
103 let mut files = pack.files_by_type_and_paths_mut(
104 &[FileType::DB],
105 from_ref(&container_path),
106 true,
107 );
108
109 if let Some(file) = files.first_mut() {
110 if let Ok(RFileDecoded::DB(db)) = file.decoded_mut() {
111 let fields = db.definition().fields_processed().to_vec();
112 let mut row: Vec<DecodedData> = Vec::new();
113
114 for field in &fields {
115 let fname = field.name();
116 let raw = values.get(fname).map(|s| s.as_str()).unwrap_or("");
117
118 let cell = match field.field_type() {
119 FieldType::Boolean =>
120 DecodedData::Boolean(raw == "true" || raw == "1"),
121 FieldType::I16 =>
122 DecodedData::I16(raw.parse().unwrap_or(0)),
123 FieldType::I32 =>
124 DecodedData::I32(raw.parse().unwrap_or(0)),
125 FieldType::I64 =>
126 DecodedData::I64(raw.parse().unwrap_or(0)),
127 FieldType::F32 =>
128 DecodedData::F32(raw.parse().unwrap_or(0.0)),
129 FieldType::F64 =>
130 DecodedData::F64(raw.parse().unwrap_or(0.0)),
131 FieldType::OptionalI16 =>
132 DecodedData::OptionalI16(raw.parse().unwrap_or(0)),
133 FieldType::OptionalI32 =>
134 DecodedData::OptionalI32(raw.parse().unwrap_or(0)),
135 FieldType::OptionalI64 =>
136 DecodedData::OptionalI64(raw.parse().unwrap_or(0)),
137 FieldType::OptionalStringU8 =>
138 DecodedData::OptionalStringU8(raw.to_string()),
139 FieldType::OptionalStringU16 =>
140 DecodedData::OptionalStringU16(raw.to_string()),
141 FieldType::StringU16 =>
142 DecodedData::StringU16(raw.to_string()),
143 _ =>
144 DecodedData::StringU8(raw.to_string()),
145 };
146 row.push(cell);
147 }
148 db.data_mut().push(row);
149 }
150 }
151 Ok(container_path)
152 }
153
154 fn insert_loc_entries(
156 pack: &mut Pack,
157 loc_path: &str,
158 entries: &[(&str, &str)], ) -> Result<ContainerPath> {
160 let container_path = ContainerPath::File(loc_path.to_string());
161
162 if pack.files_by_paths(from_ref(&container_path), false).is_empty() {
163 let loc = Loc::new();
164 let rfile = RFile::new_from_decoded(
165 &RFileDecoded::Loc(loc), 0, loc_path
166 );
167 pack.insert(rfile)?;
168 }
169
170 let mut files = pack.files_by_type_and_paths_mut(
171 &[FileType::Loc],
172 from_ref(&container_path),
173 true,
174 );
175
176 if let Some(file) = files.first_mut() {
177 if let Ok(RFileDecoded::Loc(loc)) = file.decoded_mut() {
178 for (key, text) in entries {
179 loc.data_mut().push(vec![
180 DecodedData::StringU16(key.to_string()),
181 DecodedData::StringU16(text.to_string()),
182 DecodedData::Boolean(true),
183 ]);
184 }
185 }
186 }
187
188 Ok(container_path)
189 }
190
191 macro_rules! row {
193 ($($k:expr => $v:expr),* $(,)?) => {{
194 let mut m = std::collections::HashMap::new();
195 $(m.insert($k, $v.to_string());)*
196 m
197 }};
198 }
199
200 let stem = format!("ceo_{}", entries.first().map(|e| e.name.as_str()).unwrap_or("builder"));
202 let stem = stem.as_str();
203 let loc_path = format!("text/ceo/general_items/{}.loc", stem);
204
205 let mut loc_entries: Vec<(String, String)> = Vec::new();
207
208 for entry in entries {
209 let n = &entry.name;
210 let element = &entry.element;
211 let gender = &entry.gender;
212 let is_unique = entry.option == "unique";
213
214 if is_unique {
215 let p = insert_row(pack, schema, "ceos_tables", stem, &row![
219 "key" => format!("3k_main_ancilliary_armour_{n}_armour_unique"),
220 "exists_in_location" => "character_ceo_manager",
221 "category" => "3k_main_ceo_category_ancillary_armour",
222 "equipped_in_location" => "character_equipment",
223 "priority" => "1",
224 "turns_to_expire" => "0",
225 "point_change_per_turn_if_inactive" => "0",
226 "point_change_per_turn_while_active" => "0",
227 "point_change_per_turn_while_equipped" => "0",
228 "inheritance_chance" => "0",
229 "can_be_looted_post_battle" => "false",
230 "can_be_traded_in_diplomacy" => "false",
231 "can_be_stolen" => "false",
232 "rarity" => "unique",
233 "can_be_unequipped" => "false",
234 "can_be_transferred_if_equipped" => "true",
235 "cannot_reequip_until_next_round_if_unequipped" => "true",
236 "provides_scripted_permissions_on_spawn" => "",
237 ])?;
238 if !added_paths.contains(&p) { added_paths.push(p); }
239
240 let p = insert_row(pack, schema, "ceos_tables", stem, &row![
242 "key" => format!("3k_main_ceo_career_historical_{n}"),
243 "exists_in_location" => "character_ceo_manager",
244 "category" => "3k_main_ceo_category_career",
245 "equipped_in_location" => "character_equipment",
246 "priority" => "1",
247 "turns_to_expire" => "0",
248 "point_change_per_turn_if_inactive" => "0",
249 "point_change_per_turn_while_active" => "0",
250 "point_change_per_turn_while_equipped" => "0",
251 "inheritance_chance" => "0",
252 "can_be_looted_post_battle" => "false",
253 "can_be_traded_in_diplomacy" => "false",
254 "can_be_stolen" => "false",
255 "rarity" => "common",
256 "can_be_unequipped" => "false",
257 "can_be_transferred_if_equipped" => "true",
258 "cannot_reequip_until_next_round_if_unequipped" => "true",
259 "provides_scripted_permissions_on_spawn" => "",
260 ])?;
261 if !added_paths.contains(&p) { added_paths.push(p); }
262
263 let p = insert_row(pack, schema, "ceo_groups_tables", stem, &row![
265 "key" => format!("3k_main_ceo_group_ancillary_armour_character_specific_{n}"),
266 ])?;
267 if !added_paths.contains(&p) { added_paths.push(p); }
268
269 let grp_key = format!("3k_main_ceo_group_ancillary_armour_character_specific_{n}");
271 let armor_key = format!("3k_main_ancilliary_armour_{n}_armour_unique");
272 let career_key_grp = format!("3k_main_ceo_career_historical_{n}");
273 let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
275 "ceo_group" => &grp_key,
276 "ceo" => &armor_key,
277 "trigger_weighting" => "0.1",
278 "auto_id" => auto_id("ceo_group_ceos", &format!("{grp_key}|{armor_key}")),
279 ])?;
280 if !added_paths.contains(&p) { added_paths.push(p); }
281 let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
283 "ceo_group" => "3k_main_ceo_group_ancillary_armour_type_character_specific",
284 "ceo" => &armor_key,
285 "trigger_weighting" => "0.1",
286 "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_ancillary_armour_type_character_specific|{armor_key}")),
287 ])?;
288 if !added_paths.contains(&p) { added_paths.push(p); }
289 let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
291 "ceo_group" => "3k_main_ceo_group_ancillary_armour_character_all",
292 "ceo" => &armor_key,
293 "trigger_weighting" => "0.1",
294 "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_ancillary_armour_character_all|{armor_key}")),
295 ])?;
296 if !added_paths.contains(&p) { added_paths.push(p); }
297 let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
299 "ceo_group" => "3k_main_ceo_group_career_all",
300 "ceo" => &career_key_grp,
301 "trigger_weighting" => "1",
302 "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_career_all|{career_key_grp}")),
303 ])?;
304 if !added_paths.contains(&p) { added_paths.push(p); }
305
306 let perm_key = format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}");
308 let p = insert_row(pack, schema, "ceo_permissions_tables", stem, &row![
309 "key" => &perm_key,
310 ])?;
311 if !added_paths.contains(&p) { added_paths.push(p); }
312
313 let p = insert_row(pack, schema, "ceo_permissions_groups_tables", stem, &row![
315 "permissions" => &perm_key,
316 "group" => &grp_key,
317 "point_gain_enabled_override" => "true",
318 "point_gain_disabled_override" => "false",
319 "state_active_override" => "true",
320 "state_inactive_override" => "false",
321 "auto_id" => auto_id("ceo_permissions_groups", &format!("{perm_key}|{grp_key}")),
322 ])?;
323 if !added_paths.contains(&p) { added_paths.push(p); }
324
325 let scripted_perm_key = format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}");
327 let p = insert_row(pack, schema, "ceo_scripted_permissions_tables", stem, &row![
328 "key" => &scripted_perm_key,
329 "exists_in_and_provides_permission_to_location" => "character_ceo_manager",
330 ])?;
331 if !added_paths.contains(&p) { added_paths.push(p); }
332
333 let p = insert_row(pack, schema, "ceo_scripted_permissions_to_permissions_tables", stem, &row![
335 "scripted_permissions" => &scripted_perm_key,
336 "permissions" => &perm_key,
337 "auto_id" => auto_id("ceo_scripted_permissions_to_permissions", &format!("{scripted_perm_key}|{perm_key}")),
338 ])?;
339 if !added_paths.contains(&p) { added_paths.push(p); }
340
341 let stage1_key = format!("3k_main_ceo_initial_data_stage_character_traits_historical_{n}");
343 let stage2_key = format!("3k_main_ceo_initial_data_character_historical_{n}_ancillaries");
344 for key in &[&stage1_key, &stage2_key] {
345 let p = insert_row(pack, schema, "ceo_initial_data_stages_tables", stem, &row![
346 "key" => *key,
347 ])?;
348 if !added_paths.contains(&p) { added_paths.push(p); }
349 }
350
351 let effect_list_armor = format!("3k_main_ancilliary_armour_{n}_armour_unique");
353 let effect_list_career = format!("3k_main_ceo_career_historical_{n}");
354 for key in &[&effect_list_armor, &effect_list_career] {
355 let p = insert_row(pack, schema, "ceo_effect_lists_tables", stem, &row![
356 "key" => *key,
357 ])?;
358 if !added_paths.contains(&p) { added_paths.push(p); }
359 }
360
361 let initial_data_key = format!("3k_main_ceo_initial_data_character_historical_{n}");
363 let p = insert_row(pack, schema, "ceo_initial_datas_tables", stem, &row![
364 "key" => &initial_data_key,
365 "template_manager" => "character_ceo_manager",
366 ])?;
367 if !added_paths.contains(&p) { added_paths.push(p); }
368
369 let armor_ceo_key = format!("3k_main_ancilliary_armour_{n}_armour_unique");
371 match element.as_str() {
372 "metal" => {
373 let armour_perm_key = format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}");
374 for scripted in &[
375 "3k_main_ceo_permissions_ancillary_weapon_character_sword_dual_enable",
376 "3k_main_ceo_permissions_ancillary_weapon_character_axe_dual_enable",
377 "3k_main_ceo_permissions_ancillary_weapon_character_sword_one_handed_enable",
378 "3k_main_ceo_permissions_ancillary_weapon_character_axe_one_handed_enable",
379 "3k_ytr_ceo_permissions_ancillary_weapon_character_mace_dual_enable",
380 armour_perm_key.as_str(),
381 ] {
382 let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
383 "initial_data_stage" => &stage2_key,
384 "scripted_permissions" => *scripted,
385 "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
386 ])?;
387 if !added_paths.contains(&p) { added_paths.push(p); }
388 }
389 for (category, equipped_ceo) in &[
390 ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_double_edged_sword_common"),
391 ("3k_main_ceo_category_ancillary_mount", "3k_main_ancillary_mount_grey_horse"),
392 ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
393 ] {
394 let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
395 "initial_data_stage" => &stage2_key,
396 "category" => *category,
397 "equipped_ceo" => *equipped_ceo,
398 "slot_index" => "0",
399 "target" => "character_equipment",
400 "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
401 ])?;
402 if !added_paths.contains(&p) { added_paths.push(p); }
403 }
404 for active_ceo in &[
405 "3k_main_ancillary_weapon_single_edged_sword_common",
406 "3k_ytr_ancillary_weapon_dual_maces_common",
407 "3k_main_ancillary_weapon_one_handed_axe_common",
408 "3k_main_ancillary_weapon_dual_swords_common",
409 "3k_main_ancillary_weapon_double_edged_sword_common",
410 "3k_main_ancillary_mount_grey_horse",
411 ] {
412 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
413 "initial_data_stage" => &stage2_key,
414 "active_ceo" => *active_ceo,
415 "starting_points_delta" => "0",
416 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
417 ])?;
418 if !added_paths.contains(&p) { added_paths.push(p); }
419 }
420 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
422 "initial_data_stage" => &stage1_key,
423 "active_ceo" => "3k_main_ceo_class_metal",
424 "starting_points_delta" => "0",
425 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_metal")),
426 ])?;
427 if !added_paths.contains(&p) { added_paths.push(p); }
428 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
430 "initial_data_stage" => &stage2_key,
431 "active_ceo" => &armor_ceo_key,
432 "starting_points_delta" => "0",
433 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
434 ])?;
435 if !added_paths.contains(&p) { added_paths.push(p); }
436 if entry.expanded {
438 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
439 "initial_data_stage" => &stage2_key,
440 "active_ceo" => "3k_main_ancillary_weapon_sword_and_shield_common",
441 "starting_points_delta" => "0",
442 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|3k_main_ancillary_weapon_sword_and_shield_common")),
443 ])?;
444 if !added_paths.contains(&p) { added_paths.push(p); }
445 }
446 let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
448 "effect_list" => &effect_list_armor,
449 "effect" => "3k_main_effect_character_attribute_expertise_mod",
450 "value" => "18",
451 "effect_scope" => "character_to_character_own",
452 "optional_only_in_game_mode" => "",
453 "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|expertise")),
454 ])?;
455 if !added_paths.contains(&p) { added_paths.push(p); }
456 for (id_stage, stage_num) in &[
457 ("3k_main_ceo_initial_data_stage_character_childhood_metal", 17i32),
458 ("3k_main_ceo_initial_data_equipment_permissions_unique_metal", 4i32),
459 ] {
460 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
461 "ceo_initial_data" => &initial_data_key,
462 "initial_data_stage" => *id_stage,
463 "stage" => stage_num,
464 ])?;
465 if !added_paths.contains(&p) { added_paths.push(p); }
466 }
467 },
468 "water" => {
469 for scripted in &[
470 "3k_main_ceo_permissions_ancillary_weapon_character_sword_one_handed_enable",
471 &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
472 ] {
473 let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
474 "initial_data_stage" => &stage2_key,
475 "scripted_permissions" => *scripted,
476 "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
477 ])?;
478 if !added_paths.contains(&p) { added_paths.push(p); }
479 }
480 for (category, equipped_ceo) in &[
481 ("3k_main_ceo_category_ancillary_mount", "3k_main_ancillary_mount_black_horse"),
482 ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_single_edged_sword_common"),
483 ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
484 ] {
485 let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
486 "initial_data_stage" => &stage2_key,
487 "category" => *category,
488 "equipped_ceo" => *equipped_ceo,
489 "slot_index" => "0",
490 "target" => "character_equipment",
491 "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
492 ])?;
493 if !added_paths.contains(&p) { added_paths.push(p); }
494 }
495 for active_ceo in &[
496 "3k_main_ancillary_weapon_single_edged_sword_common",
497 "3k_main_ancillary_weapon_dual_swords_common",
498 "3k_main_ancillary_weapon_double_edged_sword_common",
499 "3k_main_ancillary_mount_black_horse",
500 ] {
501 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
502 "initial_data_stage" => &stage2_key,
503 "active_ceo" => *active_ceo,
504 "starting_points_delta" => "0",
505 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
506 ])?;
507 if !added_paths.contains(&p) { added_paths.push(p); }
508 }
509 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
511 "initial_data_stage" => &stage2_key,
512 "active_ceo" => &armor_ceo_key,
513 "starting_points_delta" => "0",
514 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
515 ])?;
516 if !added_paths.contains(&p) { added_paths.push(p); }
517 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
519 "initial_data_stage" => &stage1_key,
520 "active_ceo" => "3k_main_ceo_class_water",
521 "starting_points_delta" => "0",
522 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_water")),
523 ])?;
524 if !added_paths.contains(&p) { added_paths.push(p); }
525 let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
527 "effect_list" => &effect_list_armor,
528 "effect" => "3k_main_effect_character_attribute_cunning_mod",
529 "value" => "18",
530 "effect_scope" => "character_to_character_own",
531 "optional_only_in_game_mode" => "",
532 "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|cunning")),
533 ])?;
534 if !added_paths.contains(&p) { added_paths.push(p); }
535 for (id_stage, stage_num) in &[
536 ("3k_main_ceo_initial_data_stage_character_childhood_water", 17i32),
537 ("3k_main_ceo_initial_data_equipment_permissions_unique_water", 4i32),
538 ] {
539 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
540 "ceo_initial_data" => &initial_data_key,
541 "initial_data_stage" => *id_stage,
542 "stage" => stage_num,
543 ])?;
544 if !added_paths.contains(&p) { added_paths.push(p); }
545 }
546 },
547 "earth" => {
548 for scripted in &[
549 "3k_main_ceo_permissions_ancillary_weapon_character_axe_dual_enable",
550 "3k_main_ceo_permissions_ancillary_weapon_character_axe_one_handed_enable",
551 &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
552 "3k_main_ceo_permissions_ancillary_weapon_character_sword_one_handed_enable",
553 "3k_main_ceo_permissions_ancillary_weapon_character_sword_dual_enable",
554 ] {
555 let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
556 "initial_data_stage" => &stage2_key,
557 "scripted_permissions" => *scripted,
558 "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
559 ])?;
560 if !added_paths.contains(&p) { added_paths.push(p); }
561 }
562 for (category, equipped_ceo) in &[
563 ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_single_edged_sword_common"),
564 ("3k_main_ceo_category_ancillary_mount", "3k_main_ancillary_mount_white_horse"),
565 ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
566 ] {
567 let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
568 "initial_data_stage" => &stage2_key,
569 "category" => *category,
570 "equipped_ceo" => *equipped_ceo,
571 "slot_index" => "0",
572 "target" => "character_equipment",
573 "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
574 ])?;
575 if !added_paths.contains(&p) { added_paths.push(p); }
576 }
577 for active_ceo in &[
578 "3k_main_ancillary_weapon_one_handed_axe_common",
579 "3k_main_ancillary_weapon_dual_swords_common",
580 "3k_main_ancillary_mount_white_horse",
581 "3k_main_ancillary_weapon_single_edged_sword_common",
582 "3k_main_ancillary_weapon_double_edged_sword_common",
583 ] {
584 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
585 "initial_data_stage" => &stage2_key,
586 "active_ceo" => *active_ceo,
587 "starting_points_delta" => "0",
588 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
589 ])?;
590 if !added_paths.contains(&p) { added_paths.push(p); }
591 }
592 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
594 "initial_data_stage" => &stage2_key,
595 "active_ceo" => &armor_ceo_key,
596 "starting_points_delta" => "0",
597 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
598 ])?;
599 if !added_paths.contains(&p) { added_paths.push(p); }
600 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
602 "initial_data_stage" => &stage1_key,
603 "active_ceo" => "3k_main_ceo_class_earth",
604 "starting_points_delta" => "0",
605 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_earth")),
606 ])?;
607 if !added_paths.contains(&p) { added_paths.push(p); }
608 if entry.expanded {
610 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
611 "initial_data_stage" => &stage2_key,
612 "active_ceo" => "3k_main_ancillary_weapon_sword_and_shield_common",
613 "starting_points_delta" => "0",
614 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|3k_main_ancillary_weapon_sword_and_shield_common")),
615 ])?;
616 if !added_paths.contains(&p) { added_paths.push(p); }
617 }
618 let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
620 "effect_list" => &effect_list_armor,
621 "effect" => "3k_main_effect_character_attribute_authority_mod",
622 "value" => "18",
623 "effect_scope" => "character_to_character_own",
624 "optional_only_in_game_mode" => "",
625 "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|authority")),
626 ])?;
627 if !added_paths.contains(&p) { added_paths.push(p); }
628 for (id_stage, stage_num) in &[
629 ("3k_main_ceo_initial_data_stage_character_childhood_earth", 17i32),
630 ("3k_main_ceo_initial_data_equipment_permissions_unique_earth", 4i32),
631 ] {
632 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
633 "ceo_initial_data" => &initial_data_key,
634 "initial_data_stage" => *id_stage,
635 "stage" => stage_num,
636 ])?;
637 if !added_paths.contains(&p) { added_paths.push(p); }
638 }
639 },
640 "fire" => {
641 for scripted in &[
642 "3k_main_ceo_permissions_ancillary_weapon_character_axe_two_handed_enable",
643 "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_long_enable",
644 &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
645 "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_enable",
646 "3k_main_ceo_permissions_ancillary_weapon_character_axe_two_handed_enable",
647 ] {
648 let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
649 "initial_data_stage" => &stage2_key,
650 "scripted_permissions" => *scripted,
651 "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
652 ])?;
653 if !added_paths.contains(&p) { added_paths.push(p); }
654 }
655 for (category, equipped_ceo) in &[
656 ("3k_main_ceo_category_ancillary_mount", "3k_main_ancillary_mount_red_horse"),
657 ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_two_handed_spear_common"),
658 ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
659 ] {
660 let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
661 "initial_data_stage" => &stage2_key,
662 "category" => *category,
663 "equipped_ceo" => *equipped_ceo,
664 "slot_index" => "0",
665 "target" => "character_equipment",
666 "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
667 ])?;
668 if !added_paths.contains(&p) { added_paths.push(p); }
669 }
670 for active_ceo in &[
671 "3k_main_ancillary_weapon_hook_sickle_sabre_common",
672 "3k_main_ancillary_weapon_two_handed_axe_common",
673 "3k_main_ancillary_weapon_two_handed_spear_common",
674 "3k_main_ancillary_weapon_halberd_common",
675 "3k_main_ancillary_mount_red_horse",
676 ] {
677 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
678 "initial_data_stage" => &stage2_key,
679 "active_ceo" => *active_ceo,
680 "starting_points_delta" => "0",
681 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
682 ])?;
683 if !added_paths.contains(&p) { added_paths.push(p); }
684 }
685 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
687 "initial_data_stage" => &stage2_key,
688 "active_ceo" => &armor_ceo_key,
689 "starting_points_delta" => "0",
690 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
691 ])?;
692 if !added_paths.contains(&p) { added_paths.push(p); }
693 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
695 "initial_data_stage" => &stage1_key,
696 "active_ceo" => "3k_main_ceo_class_fire",
697 "starting_points_delta" => "0",
698 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_fire")),
699 ])?;
700 if !added_paths.contains(&p) { added_paths.push(p); }
701 let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
703 "effect_list" => &effect_list_armor,
704 "effect" => "3k_main_effect_character_attribute_instinct_mod",
705 "value" => "18",
706 "effect_scope" => "character_to_character_own",
707 "optional_only_in_game_mode" => "",
708 "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|instinct")),
709 ])?;
710 if !added_paths.contains(&p) { added_paths.push(p); }
711 for (id_stage, stage_num) in &[
712 ("3k_main_ceo_initial_data_stage_character_childhood_fire", 17i32),
713 ("3k_main_ceo_initial_data_equipment_permissions_unique_fire", 4i32),
714 ] {
715 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
716 "ceo_initial_data" => &initial_data_key,
717 "initial_data_stage" => *id_stage,
718 "stage" => stage_num,
719 ])?;
720 if !added_paths.contains(&p) { added_paths.push(p); }
721 }
722 },
723 _ => { for scripted in &[
725 &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
726 "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_enable",
727 "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_long_enable",
728 "3k_ytr_ceo_permissions_ancillary_weapon_character_staff_two_handed_enable",
729 "3k_ytr_ceo_permissions_ancillary_weapon_character_mace_two_handed_enable",
730 ] {
731 let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
732 "initial_data_stage" => &stage2_key,
733 "scripted_permissions" => *scripted,
734 "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
735 ])?;
736 if !added_paths.contains(&p) { added_paths.push(p); }
737 }
738 for (category, equipped_ceo) in &[
739 ("3k_main_ceo_category_ancillary_mount", "3k_main_ancillary_mount_brown_horse"),
740 ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_two_handed_spear_common"),
741 ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
742 ] {
743 let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
744 "initial_data_stage" => &stage2_key,
745 "category" => *category,
746 "equipped_ceo" => *equipped_ceo,
747 "slot_index" => "0",
748 "target" => "character_equipment",
749 "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
750 ])?;
751 if !added_paths.contains(&p) { added_paths.push(p); }
752 }
753 for active_ceo in &[
754 "3k_ytr_ancillary_weapon_2h_ball_mace_common",
755 "3k_main_ancillary_weapon_hook_sickle_sabre_common",
756 "3k_main_ancillary_mount_brown_horse",
757 "3k_main_ancillary_weapon_two_handed_spear_common",
758 "3k_main_ancillary_weapon_halberd_common",
759 ] {
760 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
761 "initial_data_stage" => &stage2_key,
762 "active_ceo" => *active_ceo,
763 "starting_points_delta" => "0",
764 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
765 ])?;
766 if !added_paths.contains(&p) { added_paths.push(p); }
767 }
768 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
770 "initial_data_stage" => &stage2_key,
771 "active_ceo" => &armor_ceo_key,
772 "starting_points_delta" => "0",
773 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
774 ])?;
775 if !added_paths.contains(&p) { added_paths.push(p); }
776 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
778 "initial_data_stage" => &stage1_key,
779 "active_ceo" => "3k_main_ceo_class_wood",
780 "starting_points_delta" => "0",
781 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_wood")),
782 ])?;
783 if !added_paths.contains(&p) { added_paths.push(p); }
784 let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
786 "effect_list" => &effect_list_armor,
787 "effect" => "3k_main_effect_character_attribute_resolve_mod",
788 "value" => "18",
789 "effect_scope" => "character_to_character_own",
790 "optional_only_in_game_mode" => "",
791 "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|resolve")),
792 ])?;
793 if !added_paths.contains(&p) { added_paths.push(p); }
794 for (id_stage, stage_num) in &[
795 ("3k_main_ceo_initial_data_stage_character_childhood_wood", 17i32),
796 ("3k_main_ceo_initial_data_equipment_permissions_unique_wood", 4i32),
797 ] {
798 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
799 "ceo_initial_data" => &initial_data_key,
800 "initial_data_stage" => *id_stage,
801 "stage" => stage_num,
802 ])?;
803 if !added_paths.contains(&p) { added_paths.push(p); }
804 }
805 },
806 }
807 let gender_stage = if gender == "male" {
809 "3k_main_ceo_initial_data_stage_character_gender_male"
810 } else {
811 "3k_main_ceo_initial_data_stage_character_gender_female"
812 };
813 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
814 "ceo_initial_data" => &initial_data_key,
815 "initial_data_stage" => gender_stage,
816 "stage" => "13",
817 ])?;
818 if !added_paths.contains(&p) { added_paths.push(p); }
819
820 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
824 "ceo_initial_data" => &initial_data_key,
825 "initial_data_stage" => &stage2_key,
826 "stage" => "3",
827 ])?;
828 if !added_paths.contains(&p) { added_paths.push(p); }
829
830 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
831 "initial_data_stage" => &stage1_key,
832 "active_ceo" => &format!("3k_main_ceo_career_historical_{n}"),
833 "starting_points_delta" => "0",
834 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_career_historical_{n}")),
835 ])?;
836 if !added_paths.contains(&p) { added_paths.push(p); }
837
838 for (id_stage, stage_num) in &[
839 ("3k_main_initial_data_character_ancillaries_global", 2i32),
840 ("3k_dlc04_ceo_initial_data_character_give_political_support_random", 21i32),
841 ("3k_main_ceo_initial_data_stage_character_wealth_random", 15i32),
842 ("3k_main_ceo_initial_data_stage_character_traits_shared_global_permissions", 10i32),
843 ("3k_main_ceo_initial_data_stage_character_protagonist", 14i32),
844 ] {
845 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
846 "ceo_initial_data" => &initial_data_key,
847 "initial_data_stage" => *id_stage,
848 "stage" => stage_num,
849 ])?;
850 if !added_paths.contains(&p) { added_paths.push(p); }
851 }
852 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
853 "ceo_initial_data" => &initial_data_key,
854 "initial_data_stage" => &stage1_key,
855 "stage" => "11",
856 ])?;
857 if !added_paths.contains(&p) { added_paths.push(p); }
858
859 for (ceo_ref, key_val) in &[
861 (format!("3k_main_ceo_career_historical_{n}"), format!("3k_main_ceo_career_historical_{n}")),
862 (format!("3k_main_ancilliary_armour_{n}_armour_unique"), format!("3k_main_ancilliary_armour_{n}_armour_unique")),
863 ] {
864 let p = insert_row(pack, schema, "ceo_thresholds_tables", stem, &row![
865 "key" => key_val,
866 "ceo" => ceo_ref,
867 "point_threshold_to_activate" => "1",
868 "point_theshold_to_destroy" => "0",
869 "starting_points" => "1",
870 "max_points" => "1",
871 "resets_to_starting_points_when_deactivated" => "false",
872 ])?;
873 if !added_paths.contains(&p) { added_paths.push(p); }
874 }
875
876 let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
878 "key" => format!("3k_main_ceo_career_historical_{n}"),
879 "ceo_effect_list" => &effect_list_career,
880 "title" => "placeholder",
881 "description" => "placeholder",
882 "icon_path" => "",
883 "opinion_topic_modifier" => "",
884 "point_change_per_turn_if_active" => "0",
885 ])?;
886 if !added_paths.contains(&p) { added_paths.push(p); }
887 let armour_icon = format!("armours/3k_main_ancillary_{}_armour_unique.png", element);
889 let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
890 "key" => format!("3k_main_ancilliary_armour_{n}_armour_unique"),
891 "ceo_effect_list" => &effect_list_armor,
892 "title" => "placeholder",
893 "description" => "placeholder",
894 "icon_path" => &armour_icon,
895 "opinion_topic_modifier" => "",
896 "point_change_per_turn_if_active" => "0",
897 ])?;
898 if !added_paths.contains(&p) { added_paths.push(p); }
899
900 for (node_key, threshold_key) in &[
902 (format!("3k_main_ceo_career_historical_{n}"), format!("3k_main_ceo_career_historical_{n}")),
903 (format!("3k_main_ancilliary_armour_{n}_armour_unique"), format!("3k_main_ancilliary_armour_{n}_armour_unique")),
904 ] {
905 let p = insert_row(pack, schema, "ceo_threshold_nodes_tables", stem, &row![
906 "ceo_threshold" => threshold_key,
907 "ceo_node" => node_key,
908 "points_threshold_to_activate_node" => "1",
909 "can_downgrade_to_previous_node" => "false",
910 "auto_id" => auto_id("ceo_threshold_nodes", &format!("{threshold_key}|{node_key}")),
911 ])?;
912 if !added_paths.contains(&p) { added_paths.push(p); }
913 }
914
915 let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
917 "effect_list" => &effect_list_armor,
918 "effect" => "3k_dummy_effect_ceo_subcategory_armour_unique",
919 "value" => "0",
920 "effect_scope" => "character_to_character_own",
921 "optional_only_in_game_mode" => "",
922 "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|dummy_subcategory")),
923 ])?;
924 if !added_paths.contains(&p) { added_paths.push(p); }
925
926 for (effect, val) in &[
928 ("3k_main_character_wealth", "2"),
929 ("3k_main_effect_character_num_lives", "1"),
930 ] {
931 let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
932 "effect_list" => &effect_list_career,
933 "effect" => *effect,
934 "value" => *val,
935 "effect_scope" => "character_to_character_own",
936 "optional_only_in_game_mode" => "",
937 "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_career}|{effect}")),
938 ])?;
939 if !added_paths.contains(&p) { added_paths.push(p); }
940 }
941
942 for (trait_uuid, trait_key) in &entry.traits {
944 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
945 "initial_data_stage" => &stage1_key,
946 "active_ceo" => trait_key.as_str(),
947 "starting_points_delta" => "0",
948 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{trait_key}|{trait_uuid}")),
949 ])?;
950 if !added_paths.contains(&p) { added_paths.push(p); }
951 }
952
953 let p = insert_row(pack, schema, "ceos_to_equipment_variants_tables", stem, &row![
955 "ceos_key" => &format!("3k_main_ancilliary_armour_{n}_armour_unique"),
956 "game_mode" => "",
957 "armour" => "3k_ytr_hero_scholar_unique",
958 "male_vmd" => "",
959 "female_vmd" => "",
960 "mount" => "",
961 "primary_melee_weapon" => "",
962 "primary_missile_weapon" => "",
963 "shield" => "",
964 "man_animation" => "",
965 "mount_animation" => "",
966 "secondary_weapon_animation" => "",
967 "remap_general_unit_to_hero_unit" => "false",
968 "priority" => "1",
969 "autonomous_rider_group" => "",
970 "ground_type_stat_effect_group" => "",
971 ])?;
972 if !added_paths.contains(&p) { added_paths.push(p); }
973
974 let p = insert_row(pack, schema, "ceo_template_manager_ceo_limits_tables", stem, &row![
976 "ceo_to_limit" => &format!("3k_main_ancilliary_armour_{n}_armour_unique"),
977 "template_manager" => "3k_main_ceo_template_manager_world_generic",
978 "max_limit_that_can_exist_at_once" => "1",
979 "scoped_limit_or_local_only_limit" => "true",
980 "ceo_category_to_limit" => "",
981 "ceo_node_to_limit" => "",
982 "auto_id" => auto_id("ceo_template_manager_ceo_limits", &format!("3k_main_ancilliary_armour_{n}_armour_unique")),
983 ])?;
984 if !added_paths.contains(&p) { added_paths.push(p); }
985
986 let human = n.replace("_ironic", "")
988 .split('_')
989 .map(|w: &str| {
990 let mut c = w.chars();
991 c.next().map(|f: char| f.to_uppercase().collect::<String>() + c.as_str()).unwrap_or_default()
992 })
993 .collect::<Vec<_>>()
994 .join(" ");
995 loc_entries.push((format!("ceo_nodes_title_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
996 loc_entries.push((format!("ceo_nodes_description_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
997 loc_entries.push((format!("ceo_nodes_title_3k_main_ancilliary_armour_{n}_armour_unique"), format!("{human}'s Armour")));
998 loc_entries.push((format!("ceo_nodes_description_3k_main_ancilliary_armour_{n}_armour_unique"), "The perfect weight and fit, tailored for this warrior of class and distinction.".into()));
999
1000 } else {
1001 let p = insert_row(pack, schema, "ceos_tables", stem, &row![
1005 "key" => format!("3k_main_ceo_career_historical_{n}"),
1006 "exists_in_location" => "character_ceo_manager",
1007 "category" => "3k_main_ceo_category_career",
1008 "equipped_in_location" => "character_equipment",
1009 "priority" => "1",
1010 "turns_to_expire" => "0",
1011 "point_change_per_turn_if_inactive" => "0",
1012 "point_change_per_turn_while_active" => "0",
1013 "point_change_per_turn_while_equipped" => "0",
1014 "inheritance_chance" => "0",
1015 "can_be_looted_post_battle" => "false",
1016 "can_be_traded_in_diplomacy" => "false",
1017 "can_be_stolen" => "false",
1018 "rarity" => "common",
1019 "can_be_unequipped" => "false",
1020 "can_be_transferred_if_equipped" => "true",
1021 "cannot_reequip_until_next_round_if_unequipped" => "true",
1022 "provides_scripted_permissions_on_spawn" => "",
1023 ])?;
1024 if !added_paths.contains(&p) { added_paths.push(p); }
1025
1026 let career_key_title = format!("3k_main_ceo_career_historical_{n}");
1028 let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
1029 "ceo_group" => "3k_main_ceo_group_career_all",
1030 "ceo" => &career_key_title,
1031 "trigger_weighting" => "1",
1032 "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_career_all|{career_key_title}")),
1033 ])?;
1034 if !added_paths.contains(&p) { added_paths.push(p); }
1035
1036 let stage1_key = format!("3k_main_ceo_initial_data_stage_character_traits_historical_{n}");
1037 let p = insert_row(pack, schema, "ceo_initial_data_stages_tables", stem, &row![
1038 "key" => &stage1_key,
1039 ])?;
1040 if !added_paths.contains(&p) { added_paths.push(p); }
1041
1042 let effect_list_key = format!("3k_main_ceo_career_historical_{n}");
1043 let p = insert_row(pack, schema, "ceo_effect_lists_tables", stem, &row![
1044 "key" => &effect_list_key,
1045 ])?;
1046 if !added_paths.contains(&p) { added_paths.push(p); }
1047
1048 let initial_data_key = format!("3k_main_ceo_initial_data_character_historical_{n}");
1049 let p = insert_row(pack, schema, "ceo_initial_datas_tables", stem, &row![
1050 "key" => &initial_data_key,
1051 "template_manager" => "character_ceo_manager",
1052 ])?;
1053 if !added_paths.contains(&p) { added_paths.push(p); }
1054
1055 let (childhood_stage, equipment_stage, class_ceo, generic_stage, generic_stage_num) = match element.as_str() {
1057 "metal" => ("3k_main_ceo_initial_data_stage_character_childhood_metal", "3k_main_ceo_initial_data_equipment_permissions_title_metal", "3k_main_ceo_class_metal", "3k_main_ceo_initial_data_character_generic_metal_ancillaries_01", 3i32),
1058 "earth" => ("3k_main_ceo_initial_data_stage_character_childhood_earth", "3k_main_ceo_initial_data_equipment_permissions_title_earth", "3k_main_ceo_class_earth", "3k_main_ceo_initial_data_character_generic_earth_ancillaries_01", 3i32),
1059 "water" => ("3k_main_ceo_initial_data_stage_character_childhood_water", "3k_main_ceo_initial_data_equipment_permissions_title_water", "3k_main_ceo_class_water", "3k_main_ceo_initial_data_character_generic_water_ancillaries_01", 3i32),
1060 "fire" => ("3k_main_ceo_initial_data_stage_character_childhood_fire", "3k_main_ceo_initial_data_equipment_permissions_title_fire", "3k_main_ceo_class_fire", "3k_main_ceo_initial_data_character_generic_fire_ancillaries_01", 3i32),
1061 _ => ("3k_main_ceo_initial_data_stage_character_childhood_wood", "3k_main_ceo_initial_data_equipment_permissions_title_wood", "3k_main_ceo_class_wood", "3k_main_ceo_initial_data_character_generic_wood_ancillaries_01", 3i32),
1062 };
1063
1064 for (id_stage, stage_num) in &[
1065 (childhood_stage, 17i32),
1066 (equipment_stage, 4i32),
1067 ] {
1068 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1069 "ceo_initial_data" => &initial_data_key,
1070 "initial_data_stage" => *id_stage,
1071 "stage" => stage_num,
1072 ])?;
1073 if !added_paths.contains(&p) { added_paths.push(p); }
1074 }
1075 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1076 "initial_data_stage" => &stage1_key,
1077 "active_ceo" => class_ceo,
1078 "starting_points_delta" => "0",
1079 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{class_ceo}")),
1080 ])?;
1081 if !added_paths.contains(&p) { added_paths.push(p); }
1082 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1083 "ceo_initial_data" => &initial_data_key,
1084 "initial_data_stage" => generic_stage,
1085 "stage" => &generic_stage_num,
1086 ])?;
1087 if !added_paths.contains(&p) { added_paths.push(p); }
1088
1089 let gender_stage = if gender == "male" {
1091 "3k_main_ceo_initial_data_stage_character_gender_male"
1092 } else {
1093 "3k_main_ceo_initial_data_stage_character_gender_female"
1094 };
1095 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1096 "ceo_initial_data" => &initial_data_key,
1097 "initial_data_stage" => gender_stage,
1098 "stage" => "13",
1099 ])?;
1100 if !added_paths.contains(&p) { added_paths.push(p); }
1101
1102 let career_key = format!("3k_main_ceo_career_historical_{n}");
1104 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1105 "initial_data_stage" => &stage1_key,
1106 "active_ceo" => &career_key,
1107 "starting_points_delta" => "0",
1108 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{career_key}")),
1109 ])?;
1110 if !added_paths.contains(&p) { added_paths.push(p); }
1111
1112 for (id_stage, stage_num) in &[
1113 ("3k_main_initial_data_character_ancillaries_global", 2i32),
1114 ("3k_dlc04_ceo_initial_data_character_give_political_support_random", 21i32),
1115 ("3k_main_ceo_initial_data_stage_character_wealth_random", 15i32),
1116 ("3k_main_ceo_initial_data_stage_character_traits_shared_global_permissions", 10i32),
1117 ("3k_main_ceo_initial_data_stage_character_protagonist", 14i32),
1118 ] {
1119 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1120 "ceo_initial_data" => &initial_data_key,
1121 "initial_data_stage" => *id_stage,
1122 "stage" => stage_num,
1123 ])?;
1124 if !added_paths.contains(&p) { added_paths.push(p); }
1125 }
1126 let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1127 "ceo_initial_data" => &initial_data_key,
1128 "initial_data_stage" => &stage1_key,
1129 "stage" => "11",
1130 ])?;
1131 if !added_paths.contains(&p) { added_paths.push(p); }
1132
1133 let p = insert_row(pack, schema, "ceo_thresholds_tables", stem, &row![
1135 "key" => &career_key,
1136 "ceo" => &career_key,
1137 "point_threshold_to_activate" => "1",
1138 "point_theshold_to_destroy" => "0",
1139 "starting_points" => "1",
1140 "max_points" => "1",
1141 "resets_to_starting_points_when_deactivated" => "false",
1142 ])?;
1143 if !added_paths.contains(&p) { added_paths.push(p); }
1144
1145 let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
1147 "key" => &career_key,
1148 "ceo_effect_list" => &effect_list_key,
1149 "title" => "placeholder",
1150 "description" => "placeholder",
1151 "icon_path" => "",
1152 "opinion_topic_modifier" => "",
1153 "point_change_per_turn_if_active" => "0",
1154 ])?;
1155 if !added_paths.contains(&p) { added_paths.push(p); }
1156
1157 let p = insert_row(pack, schema, "ceo_threshold_nodes_tables", stem, &row![
1159 "ceo_threshold" => &career_key,
1160 "ceo_node" => &career_key,
1161 "points_threshold_to_activate_node" => "1",
1162 "can_downgrade_to_previous_node" => "false",
1163 "auto_id" => auto_id("ceo_threshold_nodes", &format!("{career_key}|{career_key}")),
1164 ])?;
1165 if !added_paths.contains(&p) { added_paths.push(p); }
1166
1167 for (effect, val) in &[
1169 ("3k_main_character_wealth", "2"),
1170 ("3k_main_effect_character_num_lives", "1"),
1171 ] {
1172 let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
1173 "effect_list" => &effect_list_key,
1174 "effect" => *effect,
1175 "value" => *val,
1176 "effect_scope" => "character_to_character_own",
1177 "optional_only_in_game_mode" => "",
1178 "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_key}|{effect}")),
1179 ])?;
1180 if !added_paths.contains(&p) { added_paths.push(p); }
1181 }
1182
1183 for (trait_uuid, trait_key) in &entry.traits {
1185 let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1186 "initial_data_stage" => &stage1_key,
1187 "active_ceo" => trait_key.as_str(),
1188 "starting_points_delta" => "0",
1189 "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{trait_key}|{trait_uuid}")),
1190 ])?;
1191 if !added_paths.contains(&p) { added_paths.push(p); }
1192 }
1193
1194 loc_entries.push((format!("ceo_nodes_title_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
1196 loc_entries.push((format!("ceo_nodes_description_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
1197 }
1198 }
1199
1200 if !loc_entries.is_empty() {
1202 let loc_pairs: Vec<(&str, &str)> = loc_entries.iter()
1203 .map(|(k, v)| (k.as_str(), v.as_str()))
1204 .collect();
1205 let p = insert_loc_entries(pack, &loc_path, &loc_pairs)?;
1206 if !added_paths.contains(&p) { added_paths.push(p); }
1207 }
1208
1209 Ok(added_paths)
1210}
1211
1212pub fn get_trait_ceos(deps: &rpfm_extensions::dependencies::Dependencies) -> Vec<(String, String)> {
1214 let ak_tables = deps.asskit_only_db_tables();
1215 let mut trait_ceos: Vec<(String, String)> = Vec::new();
1216
1217 let trait_categories: HashSet<&str> = [
1218 "3k_main_ceo_category_traits_personality",
1219 "3k_main_ceo_category_traits_physical",
1220 ].iter().copied().collect();
1221
1222 info!("GetTraitCeos: AK tables available: {}", ak_tables.len());
1223
1224 fn ak_lookup_pairs(ak_tables: &HashMap<String, DB>, table_name: &str, col_a: &str, col_b: &str) -> Vec<(String, String)> {
1226 let mut result = Vec::new();
1227 if let Some(db) = ak_tables.get(table_name) {
1228 let fields = db.definition().fields_processed();
1229 let a_idx = fields.iter().position(|f| f.name() == col_a);
1230 let b_idx = fields.iter().position(|f| f.name() == col_b);
1231 if let (Some(ai), Some(bi)) = (a_idx, b_idx) {
1232 for row in db.data().iter() {
1233 result.push((row[ai].data_to_string().to_string(), row[bi].data_to_string().to_string()));
1234 }
1235 }
1236 }
1237 result
1238 }
1239
1240 if let Some(ceos_db) = ak_tables.get("ceos_tables") {
1242 let fields = ceos_db.definition().fields_processed();
1243 let key_idx = fields.iter().position(|f| f.name() == "key");
1244 let cat_idx = fields.iter().position(|f| f.name() == "category");
1245
1246 if let (Some(ki), Some(ci)) = (key_idx, cat_idx) {
1247 for row in ceos_db.data().iter() {
1248 let category = row[ci].data_to_string();
1249 if trait_categories.contains(&*category) {
1250 let ceo_key = row[ki].data_to_string().to_string();
1251 trait_ceos.push((ceo_key, String::new()));
1252 }
1253 }
1254 }
1255 }
1256
1257 info!("GetTraitCeos: found {} trait CEOs from AK ceos_tables", trait_ceos.len());
1258
1259 let ceo_to_threshold: HashMap<String, String> =
1261 ak_lookup_pairs(ak_tables, "ceo_thresholds_tables", "ceo", "key")
1262 .into_iter().collect();
1263
1264 let threshold_to_node: HashMap<String, String> =
1265 ak_lookup_pairs(ak_tables, "ceo_threshold_nodes_tables", "ceo_threshold", "ceo_node")
1266 .into_iter().collect();
1267
1268 let node_to_title: HashMap<String, String> =
1269 ak_lookup_pairs(ak_tables, "ceo_nodes_tables", "key", "title")
1270 .into_iter().collect();
1271
1272 let mut loc_lookup: HashMap<String, String> = HashMap::new();
1274 if let Ok(loc_files) = deps.loc_data(true, true) {
1275 for rfile in &loc_files {
1276 if let Ok(RFileDecoded::Loc(loc)) = rfile.decoded() {
1277 for row in loc.data().iter() {
1278 if row.len() >= 2 {
1279 let loc_key = row[0].data_to_string().to_string();
1280 let loc_val = row[1].data_to_string().to_string();
1281 loc_lookup.insert(loc_key, loc_val);
1282 }
1283 }
1284 }
1285 }
1286 }
1287
1288 info!("GetTraitCeos: loc_lookup has {} entries, node_to_title has {} entries", loc_lookup.len(), node_to_title.len());
1289
1290 for (ceo_key, display_name) in &mut trait_ceos {
1292 let resolved = ceo_to_threshold.get(ceo_key.as_str())
1294 .and_then(|thresh| threshold_to_node.get(thresh))
1295 .and_then(|node| {
1296 node_to_title.get(node).and_then(|title_key| {
1298 loc_lookup.get(title_key)
1299 .or_else(|| {
1300 let constructed = format!("ceo_nodes_title_{}", node);
1302 loc_lookup.get(&constructed)
1303 })
1304 })
1305 });
1306
1307 if let Some(name) = resolved {
1308 *display_name = name.clone();
1309 } else {
1310 *display_name = ceo_key
1312 .replace("3k_main_ceo_trait_", "")
1313 .replace("3k_dlc", "")
1314 .replace("3k_ytr_ceo_trait_", "")
1315 .replace('_', " ");
1316 }
1317 }
1318
1319 trait_ceos.sort_by(|a, b| a.1.cmp(&b.1));
1321
1322 info!("GetTraitCeos: returning {} traits", trait_ceos.len());
1323
1324 trait_ceos
1325}
1326
1327const CEO_BOB_TABLES: [&str; 61] = [
1329 "ceo_active_permissions_tables",
1330 "ceo_anti_ceo_pairs_tables",
1331 "ceo_can_equip_requirements_tables",
1332 "ceo_categories_tables",
1333 "ceo_effect_list_to_effects_tables",
1334 "ceo_effect_lists_tables",
1335 "ceo_equipment_category_managers_tables",
1336 "ceo_equipment_manager_all_possible_ceos_tables",
1337 "ceo_equipment_manager_campaign_lookups_tables",
1338 "ceo_equipment_manager_to_category_managers_tables",
1339 "ceo_equipment_manager_types_tables",
1340 "ceo_equipment_managers_tables",
1341 "ceo_equipped_set_bonus_ceos_tables",
1342 "ceo_equipped_set_bonus_effect_bundles_tables",
1343 "ceo_equipped_set_bonuses_tables",
1344 "ceo_equipped_set_bonuses_to_incident_junctions_tables",
1345 "ceo_event_feed_categories_tables",
1346 "ceo_group_ceos_tables",
1347 "ceo_group_spawners_tables",
1348 "ceo_groups_tables",
1349 "ceo_initial_data_active_ceos_tables",
1350 "ceo_initial_data_active_spawners_tables",
1351 "ceo_initial_data_equipments_tables",
1352 "ceo_initial_data_scripted_permissions_tables",
1353 "ceo_initial_data_stages_tables",
1354 "ceo_initial_data_to_stages_tables",
1355 "ceo_initial_data_triggers_tables",
1356 "ceo_initial_datas_tables",
1357 "ceo_location_enums_tables",
1358 "ceo_nodes_tables",
1359 "ceo_permissions_groups_tables",
1360 "ceo_permissions_tables",
1361 "ceo_post_battle_loot_chances_tables",
1362 "ceo_rarities_tables",
1363 "ceo_scripted_permissions_tables",
1364 "ceo_scripted_permissions_to_permissions_tables",
1365 "ceo_set_items_tables",
1366 "ceo_sets_tables",
1367 "ceo_spawner_can_spawn_requirements_tables",
1368 "ceo_spawners_tables",
1369 "ceo_template_manager_all_possible_ceos_tables",
1370 "ceo_template_manager_campaign_lookups_tables",
1371 "ceo_template_manager_ceo_limits_tables",
1372 "ceo_template_manager_ceo_spawn_limits_tables",
1373 "ceo_template_manager_supported_categories_tables",
1374 "ceo_template_manager_types_tables",
1375 "ceo_template_managers_tables",
1376 "ceo_threshold_nodes_tables",
1377 "ceo_thresholds_tables",
1378 "ceo_to_target_ceo_junctions_tables",
1379 "ceo_to_target_factions_tables",
1380 "ceo_to_target_junction_reasons_tables",
1381 "ceo_to_target_province_junctions_tables",
1382 "ceo_to_ui_display_junctions_tables",
1383 "ceo_trigger_behaviour_enums_tables",
1384 "ceo_trigger_target_requirements_tables",
1385 "ceo_trigger_targets_tables",
1386 "ceo_trigger_to_trigger_targets_tables",
1387 "ceo_triggers_tables",
1388 "ceos_tables",
1389 "ceos_to_equipment_variants_tables",
1390];
1391
1392const CEO_REQUIRED_TABLES: [&str; 5] = [
1394 "ceos_tables",
1395 "ceo_nodes_tables",
1396 "ceo_thresholds_tables",
1397 "ceo_threshold_nodes_tables",
1398 "ceo_initial_datas_tables",
1399];
1400
1401const BOB_CONFIG_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
1403 <bob_configuration><processors><processor>Campaign</processor></processors>
1404 <directories/><global_rules/><retail>0</retail><silent>1</silent>
1405 <get_latest>0</get_latest><connect_db>0</connect_db>
1406 <merge_for_checkin_mode>2</merge_for_checkin_mode>
1407 <selected_files><entry><working>/campaigns/ceo_data.ccd</entry></selected_files>
1408 </bob_configuration>"#;
1409
1410pub fn build_ceo(pack: &mut Pack, schema: &Option<Schema>, akit_root: &Path, bob_exe: &Path) -> Result<()> {
1427 let bob_dir = bob_exe.parent().ok_or_else(|| anyhow!("Invalid BOB path"))?;
1428 let raw_db = akit_root.join(r"raw_data\db");
1429 let ceo_ccd = akit_root.join(r"working_data\campaigns\ceo_data.ccd");
1430
1431 if ceo_ccd.exists() {
1432 std::fs::copy(&ceo_ccd, ceo_ccd.with_extension("ccd.bak1"))
1433 .map_err(|e| anyhow!("Failed to backup ceo_data.ccd: {e}"))?;
1434 }
1435
1436 let xml_backups = backup_ceo_xmls(&raw_db)?;
1437 let result = export_ceo_tables(pack, schema, &raw_db)
1438 .and_then(|_| run_bob(bob_exe, bob_dir, &ceo_ccd));
1439
1440 for (orig, bak) in &xml_backups {
1441 let _ = std::fs::rename(bak, orig);
1442 }
1443
1444 result
1445}
1446
1447fn backup_ceo_xmls(raw_db: &Path) -> Result<Vec<(PathBuf, PathBuf)>> {
1453 let mut xml_backups = vec![];
1454 if !raw_db.exists() {
1455 return Ok(xml_backups);
1456 }
1457
1458 let entries = std::fs::read_dir(raw_db).map_err(|e| anyhow!("Failed to read raw_data/db: {e}"))?;
1459 for entry in entries.filter_map(|e| e.ok()) {
1460 let file_name = entry.file_name().to_string_lossy().to_lowercase();
1461 if file_name.starts_with("ceo") && file_name.ends_with(".xml") {
1462 let orig = entry.path();
1463 let bak = orig.with_extension("xml.bak");
1464 if std::fs::copy(&orig, &bak).is_ok() {
1465 xml_backups.push((orig, bak));
1466 }
1467 }
1468 }
1469
1470 Ok(xml_backups)
1471}
1472
1473fn export_ceo_tables(pack: &mut Pack, schema: &Option<Schema>, raw_db: &Path) -> Result<()> {
1477 let ceo_table_paths = pack.files()
1478 .keys()
1479 .filter(|path| {
1480 let mut parts = path.splitn(3, '/');
1481 parts.next() == Some("ceo_db") && parts.next().is_some_and(|folder| CEO_BOB_TABLES.contains(&folder))
1482 })
1483 .cloned()
1484 .collect::<Vec<String>>();
1485
1486 if ceo_table_paths.is_empty() {
1487 return Err(anyhow!("No CEO tables found in the pack (only the ceo_db/ folder is scanned). \
1488 Import CEO tables from the Assembly Kit, or move any CEO tables under db/ to ceo_db/."));
1489 }
1490
1491 let present_folders = ceo_table_paths.iter()
1492 .filter_map(|path| path.split('/').nth(1))
1493 .collect::<HashSet<&str>>();
1494 let missing = CEO_REQUIRED_TABLES.iter()
1495 .filter(|table| !present_folders.contains(**table))
1496 .map(|table| format!(" - {table}"))
1497 .collect::<Vec<_>>();
1498
1499 if !missing.is_empty() {
1500 return Err(anyhow!("The following required CEO tables are missing from the pack:\n{}\n\n\
1501 Import them from the Assembly Kit generate them.", missing.join("\n")));
1502 }
1503
1504 let mut decode_extra_data = DecodeableExtraData::default();
1505 decode_extra_data.set_schema(schema.as_ref());
1506 let decode_extra_data = Some(decode_extra_data);
1507
1508 let mut xml_groups: BTreeMap<String, Vec<String>> = BTreeMap::new();
1510 for table_path in &ceo_table_paths {
1511 if let Some(folder) = table_path.split('/').nth(1) {
1512 let xml_name = format!("{}.xml", folder.strip_suffix("_tables").unwrap_or(folder));
1513 xml_groups.entry(xml_name).or_default().push(table_path.clone());
1514 }
1515 }
1516
1517 let mut export_errors = vec![];
1518 for (xml_name, table_paths) in &xml_groups {
1519 let xml_path = raw_db.join(xml_name);
1520 let table_tag = xml_name.trim_end_matches(".xml");
1521 let xsd_name = xml_name.replace(".xml", ".xsd");
1522
1523 let mut xml = format!(
1524 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n\
1525 <dataroot xmlns:od=\"urn:schemas-microsoft-com:officedata\" \
1526 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \
1527 xsi:noNamespaceSchemaLocation=\"{xsd_name}\" \
1528 export_time=\"\" revision=\"0\" export_branch=\"\" export_user=\"rpfm\">\r\n\
1529 <edit_uuid>00000000-0000-0000-0000-000000000000</edit_uuid>\r\n"
1530 );
1531
1532 for table_path in table_paths {
1533 let Some(rfile) = pack.files_mut().get_mut(table_path.as_str()) else { continue };
1534 let _ = rfile.load();
1535 let _ = rfile.decode(&decode_extra_data, true, false);
1536
1537 let Ok(RFileDecoded::DB(table)) = rfile.decoded() else {
1538 export_errors.push(format!("Could not decode {table_path}"));
1539 continue;
1540 };
1541
1542 let fields = table.definition().fields_processed();
1543 for row in table.data().iter() {
1544 let mut field_pairs = fields.iter()
1545 .zip(row.iter())
1546 .map(|(field, value)| (field.name().to_owned(), xml_escaped_value(value)))
1547 .collect::<Vec<_>>();
1548 field_pairs.sort_by(|a, b| a.0.cmp(&b.0));
1549
1550 xml.push_str(&format!("<{table_tag}>\r\n"));
1551 for (field_name, value) in &field_pairs {
1552 xml.push_str(&format!("<{field_name}>{value}</{field_name}>\r\n"));
1553 }
1554 xml.push_str(&format!("</{table_tag}>\r\n"));
1555 }
1556 }
1557
1558 xml.push_str("</dataroot>\r\n");
1559
1560 if let Err(e) = std::fs::write(&xml_path, xml.as_bytes()) {
1561 export_errors.push(format!("Failed to write {}: {e}", xml_path.display()));
1562 }
1563 }
1564
1565 if export_errors.is_empty() {
1566 Ok(())
1567 } else {
1568 Err(anyhow!("Export errors:\n{}", export_errors.join("\n")))
1569 }
1570}
1571
1572fn xml_escaped_value(value: &DecodedData) -> String {
1574 let value = match value {
1575 DecodedData::Boolean(b) => if *b { "1".to_owned() } else { "0".to_owned() },
1576 DecodedData::I16(v) => v.to_string(),
1577 DecodedData::I32(v) => v.to_string(),
1578 DecodedData::I64(v) => v.to_string(),
1579 DecodedData::OptionalI16(v) => v.to_string(),
1580 DecodedData::OptionalI32(v) => v.to_string(),
1581 DecodedData::OptionalI64(v) => v.to_string(),
1582 DecodedData::F32(v) => v.to_string(),
1583 DecodedData::F64(v) => v.to_string(),
1584 DecodedData::StringU8(s) | DecodedData::StringU16(s) |
1585 DecodedData::OptionalStringU8(s) | DecodedData::OptionalStringU16(s) |
1586 DecodedData::ColourRGB(s) => s.clone(),
1587 _ => String::new(),
1588 };
1589
1590 value.replace('&', "&")
1591 .replace('<', "<")
1592 .replace('>', ">")
1593 .replace('"', """)
1594}
1595
1596fn run_bob(bob_exe: &Path, bob_dir: &Path, ceo_ccd: &Path) -> Result<()> {
1604 let cfg_path = bob_dir.join("BOB/default_configuration.xml");
1605
1606 if let Some(cfg_dir) = cfg_path.parent() {
1609 std::fs::create_dir_all(cfg_dir)
1610 .map_err(|e| anyhow!("Failed to create BOB config directory {}: {e}", cfg_dir.display()))?;
1611 }
1612
1613 let cfg_backup = bob_dir.join("BOB/default_configuration.xml.rpfm_bak");
1614 let cfg_existed = cfg_path.exists();
1615 if cfg_existed {
1616 std::fs::rename(&cfg_path, &cfg_backup)
1617 .map_err(|e| anyhow!("Failed to backup BOB config {}: {e}", cfg_path.display()))?;
1618 }
1619
1620 let restore_config = || {
1621 let _ = std::fs::remove_file(&cfg_path);
1622 if cfg_existed {
1623 let _ = std::fs::rename(&cfg_backup, &cfg_path);
1624 }
1625 };
1626
1627 if let Err(e) = std::fs::write(&cfg_path, BOB_CONFIG_XML) {
1628 restore_config();
1629 return Err(anyhow!("Failed to write BOB config {}: {e}", cfg_path.display()));
1630 }
1631
1632 info!("[BuildCeo] launching BOB: {} (cwd {})", bob_exe.display(), bob_dir.display());
1633 let bob_start = Instant::now();
1634
1635 let status = SysCommand::new(bob_exe)
1638 .current_dir(bob_dir)
1639 .stdin(Stdio::null())
1640 .status();
1641
1642 restore_config();
1643
1644 let status = status.map_err(|e| {
1645 info!("[BuildCeo] BOB spawn failed after {:?}: {e}", bob_start.elapsed());
1646 anyhow!("Failed to launch BOB: {e}")
1647 })?;
1648
1649 info!("[BuildCeo] BOB exited after {:?}, exit={:?}", bob_start.elapsed(), status.code());
1650
1651 if ceo_ccd.exists() {
1654 Ok(())
1655 } else {
1656 Err(anyhow!("BOB finished (exit {:?}) but ceo_data.ccd was not generated. \
1657 This usually means BOB hit an error in the exported tables.", status.code()))
1658 }
1659}
1660
1661pub fn build_ceo_post(pack: &mut Pack, akit_path: &str) -> Result<Vec<ContainerPath>> {
1663 let ceo_ccd_path = PathBuf::from(akit_path)
1664 .join(r"working_data\campaigns\ceo_data.ccd");
1665
1666 if !ceo_ccd_path.exists() {
1667 return Err(anyhow!("ceo_data.ccd not found. Make sure BOB ran successfully."));
1668 }
1669
1670 let raw_bytes = std::fs::read(&ceo_ccd_path)
1671 .map_err(|e| anyhow!("Failed to read ceo_data.ccd: {e}"))?;
1672
1673 let mut rfile = RFile::new_from_vec(&raw_bytes, FileType::Unknown, 0, "campaigns/ceo_data.ccd");
1674 let _ = rfile.guess_file_type();
1675 match pack.insert(rfile) {
1676 Ok(Some(path)) => Ok(vec![path]),
1677 Ok(None) => Ok(vec![]),
1678 Err(e) => Err(anyhow!("{}", e)),
1679 }
1680}