Skip to main content

rpfm_server/
ceo_builder.rs

1//! CEO Builder backend logic.
2//!
3//! Contains the functions for building CEO entries, importing ceo_data.ccd,
4//! and querying trait CEOs from the Assembly Kit data.
5
6use anyhow::{anyhow, Result};
7
8use std::collections::{HashMap, HashSet};
9use std::path::PathBuf;
10use std::slice::from_ref;
11
12use rpfm_lib::files::{Container, ContainerPath, db::DB, FileType, loc::Loc, pack::Pack, RFile, RFileDecoded, table::DecodedData};
13use rpfm_lib::schema::*;
14
15use rpfm_ipc::messages::CeoEntryData;
16
17use rpfm_telemetry::*;
18
19/// Build CEO entries (armour, career, traits, loc) from structured input data.
20///
21/// This is the core logic for the `BuildCeoEntries` command, extracted to keep
22/// the background thread function manageable and to help the compiler optimize.
23pub fn build_ceo_entries(
24    pack: &mut Pack,
25    schema: &Schema,
26    entries: &[CeoEntryData],
27) -> Result<Vec<ContainerPath>> {
28        let mut added_paths: Vec<ContainerPath> = Vec::new();
29
30        // ── helpers ────────────────────────────────────────────
31        // Stable CRC32-based synthetic ID for auto_id fields.
32        // These columns exist only in AKit XML, not in binary pack format.
33        fn crc32_id(data: &[u8]) -> i64 {
34            let mut crc: u32 = 0xFFFF_FFFF;
35            for &b in data {
36                crc ^= b as u32;
37                for _ in 0..8 {
38                    if crc & 1 != 0 { crc = (crc >> 1) ^ 0xEDB8_8320; }
39                    else { crc >>= 1; }
40                }
41            }
42            (1_500_000_000u32 + (!crc % 647_000_000u32)) as i64
43        }
44
45        fn auto_id(table: &str, row_key: &str) -> i64 {
46            crc32_id(format!("{}|{}", table, row_key).as_bytes())
47        }
48
49        // Get-or-create a DB table file in the pack.
50        // Returns the path so the caller can track it.
51        fn get_or_create_db(
52            pack: &mut Pack,
53            schema: &Schema,
54            table_name: &str,
55            file_stem: &str,
56        ) -> Result<String> {
57            let path = format!("ceo_db/{}/{}", table_name, file_stem);
58            let container_path = ContainerPath::File(path.clone());
59
60            // If the file doesn't exist yet, create it.
61            if pack.files_by_paths(from_ref(&container_path), false).is_empty() {
62                let _full_table_name = format!("{}_tables", table_name.trim_end_matches("_tables"));
63                let table_name_with_suffix = if table_name.ends_with("_tables") {
64                    table_name.to_string()
65                } else {
66                    format!("{}_tables", table_name)
67                };
68
69                let def = schema.definitions_by_table_name(&table_name_with_suffix)
70                    .and_then(|defs| defs.first())
71                    .ok_or_else(|| anyhow!("No schema definition for {}", table_name_with_suffix))?;
72
73                let patches = schema.patches_for_table(&table_name_with_suffix);
74                let db = DB::new(def, patches, &table_name_with_suffix);
75                let rfile = RFile::new_from_decoded(
76                    &RFileDecoded::DB(db), 0, &path
77                );
78                pack.insert(rfile)?;
79            }
80            Ok(path)
81        }
82
83        // Insert a row into a DB table. Builds the row by matching field names
84        // from the definition against the provided HashMap of name→value strings.
85        fn insert_row(
86            pack: &mut Pack,
87            schema: &Schema,
88            table_name: &str,
89            file_stem: &str,
90            values: &std::collections::HashMap<&str, String>,
91        ) -> Result<ContainerPath> {
92            let path = get_or_create_db(pack, schema, table_name, file_stem)?;
93            let container_path = ContainerPath::File(path.clone());
94
95            let _table_name_with_suffix = if table_name.ends_with("_tables") {
96                table_name.to_string()
97            } else {
98                format!("{}_tables", table_name)
99            };
100
101            let mut files = pack.files_by_type_and_paths_mut(
102                &[FileType::DB],
103                from_ref(&container_path),
104                true,
105            );
106
107            if let Some(file) = files.first_mut() {
108                if let Ok(RFileDecoded::DB(db)) = file.decoded_mut() {
109                    let fields = db.definition().fields_processed().to_vec();
110                    let mut row: Vec<DecodedData> = Vec::new();
111
112                    for field in &fields {
113                        let fname = field.name();
114                        let raw = values.get(fname).map(|s| s.as_str()).unwrap_or("");
115
116                        let cell = match field.field_type() {
117                            FieldType::Boolean =>
118                                DecodedData::Boolean(raw == "true" || raw == "1"),
119                            FieldType::I16 =>
120                                DecodedData::I16(raw.parse().unwrap_or(0)),
121                            FieldType::I32 =>
122                                DecodedData::I32(raw.parse().unwrap_or(0)),
123                            FieldType::I64 =>
124                                DecodedData::I64(raw.parse().unwrap_or(0)),
125                            FieldType::F32 =>
126                                DecodedData::F32(raw.parse().unwrap_or(0.0)),
127                            FieldType::F64 =>
128                                DecodedData::F64(raw.parse().unwrap_or(0.0)),
129                            FieldType::OptionalI16 =>
130                                DecodedData::OptionalI16(raw.parse().unwrap_or(0)),
131                            FieldType::OptionalI32 =>
132                                DecodedData::OptionalI32(raw.parse().unwrap_or(0)),
133                            FieldType::OptionalI64 =>
134                                DecodedData::OptionalI64(raw.parse().unwrap_or(0)),
135                            FieldType::OptionalStringU8 =>
136                                DecodedData::OptionalStringU8(raw.to_string()),
137                            FieldType::OptionalStringU16 =>
138                                DecodedData::OptionalStringU16(raw.to_string()),
139                            FieldType::StringU16 =>
140                                DecodedData::StringU16(raw.to_string()),
141                            _ =>
142                                DecodedData::StringU8(raw.to_string()),
143                        };
144                        row.push(cell);
145                    }
146                    db.data_mut().push(row);
147                }
148            }
149            Ok(container_path)
150        }
151
152        // ── loc helper ─────────────────────────────────────────
153        fn insert_loc_entries(
154            pack: &mut Pack,
155            loc_path: &str,
156            entries: &[(&str, &str)], // (key, text)
157        ) -> Result<ContainerPath> {
158            let container_path = ContainerPath::File(loc_path.to_string());
159
160            if pack.files_by_paths(from_ref(&container_path), false).is_empty() {
161                let loc = Loc::new();
162                let rfile = RFile::new_from_decoded(
163                    &RFileDecoded::Loc(loc), 0, loc_path
164                );
165                pack.insert(rfile)?;
166            }
167
168            let mut files = pack.files_by_type_and_paths_mut(
169                &[FileType::Loc],
170                from_ref(&container_path),
171                true,
172            );
173
174            if let Some(file) = files.first_mut() {
175                if let Ok(RFileDecoded::Loc(loc)) = file.decoded_mut() {
176                    for (key, text) in entries {
177                        loc.data_mut().push(vec![
178                            DecodedData::StringU16(key.to_string()),
179                            DecodedData::StringU16(text.to_string()),
180                            DecodedData::Boolean(true),
181                        ]);
182                    }
183                }
184            }
185
186            Ok(container_path)
187        }
188
189        // ── macro shorthand ────────────────────────────────────
190        macro_rules! row {
191            ($($k:expr => $v:expr),* $(,)?) => {{
192                let mut m = std::collections::HashMap::new();
193                $(m.insert($k, $v.to_string());)*
194                m
195            }};
196        }
197
198        // ── file stem shared across all entries ────────────────
199        let stem = format!("ceo_{}", entries.first().map(|e| e.name.as_str()).unwrap_or("builder"));
200        let stem = stem.as_str();
201        let loc_path = format!("text/ceo/general_items/{}.loc", stem);
202
203        // ── process each entry ─────────────────────────────────
204        let mut loc_entries: Vec<(String, String)> = Vec::new();
205
206        for entry in entries {
207            let n = &entry.name;
208            let element = &entry.element;
209            let gender = &entry.gender;
210            let is_unique = entry.option == "unique";
211
212            if is_unique {
213                // ── UNIQUE PATH ────────────────────────────────
214
215                // ceos — armor
216                let p = insert_row(pack, schema, "ceos_tables", stem, &row![
217                    "key" => format!("3k_main_ancilliary_armour_{n}_armour_unique"),
218                    "exists_in_location" => "character_ceo_manager",
219                    "category" => "3k_main_ceo_category_ancillary_armour",
220                    "equipped_in_location" => "character_equipment",
221                    "priority" => "1",
222                    "turns_to_expire" => "0",
223                    "point_change_per_turn_if_inactive" => "0",
224                    "point_change_per_turn_while_active" => "0",
225                    "point_change_per_turn_while_equipped" => "0",
226                    "inheritance_chance" => "0",
227                    "can_be_looted_post_battle" => "false",
228                    "can_be_traded_in_diplomacy" => "false",
229                    "can_be_stolen" => "false",
230                    "rarity" => "unique",
231                    "can_be_unequipped" => "false",
232                    "can_be_transferred_if_equipped" => "true",
233                    "cannot_reequip_until_next_round_if_unequipped" => "true",
234                    "provides_scripted_permissions_on_spawn" => "",
235                ])?;
236                if !added_paths.contains(&p) { added_paths.push(p); }
237
238                // ceos — career/title
239                let p = insert_row(pack, schema, "ceos_tables", stem, &row![
240                    "key" => format!("3k_main_ceo_career_historical_{n}"),
241                    "exists_in_location" => "character_ceo_manager",
242                    "category" => "3k_main_ceo_category_career",
243                    "equipped_in_location" => "character_equipment",
244                    "priority" => "1",
245                    "turns_to_expire" => "0",
246                    "point_change_per_turn_if_inactive" => "0",
247                    "point_change_per_turn_while_active" => "0",
248                    "point_change_per_turn_while_equipped" => "0",
249                    "inheritance_chance" => "0",
250                    "can_be_looted_post_battle" => "false",
251                    "can_be_traded_in_diplomacy" => "false",
252                    "can_be_stolen" => "false",
253                    "rarity" => "common",
254                    "can_be_unequipped" => "false",
255                    "can_be_transferred_if_equipped" => "true",
256                    "cannot_reequip_until_next_round_if_unequipped" => "true",
257                    "provides_scripted_permissions_on_spawn" => "",
258                ])?;
259                if !added_paths.contains(&p) { added_paths.push(p); }
260
261                // ceo_groups
262                let p = insert_row(pack, schema, "ceo_groups_tables", stem, &row![
263                    "key" => format!("3k_main_ceo_group_ancillary_armour_character_specific_{n}"),
264                ])?;
265                if !added_paths.contains(&p) { added_paths.push(p); }
266
267                // ceo_group_ceos
268                let grp_key = format!("3k_main_ceo_group_ancillary_armour_character_specific_{n}");
269                let armor_key = format!("3k_main_ancilliary_armour_{n}_armour_unique");
270                let career_key_grp = format!("3k_main_ceo_career_historical_{n}");
271                // armour into character_specific group
272                let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
273                    "ceo_group" => &grp_key,
274                    "ceo" => &armor_key,
275                    "trigger_weighting" => "0.1",
276                    "auto_id" => auto_id("ceo_group_ceos", &format!("{grp_key}|{armor_key}")),
277                ])?;
278                if !added_paths.contains(&p) { added_paths.push(p); }
279                // armour into type_character_specific group
280                let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
281                    "ceo_group" => "3k_main_ceo_group_ancillary_armour_type_character_specific",
282                    "ceo" => &armor_key,
283                    "trigger_weighting" => "0.1",
284                    "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_ancillary_armour_type_character_specific|{armor_key}")),
285                ])?;
286                if !added_paths.contains(&p) { added_paths.push(p); }
287                // armour into character_all group
288                let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
289                    "ceo_group" => "3k_main_ceo_group_ancillary_armour_character_all",
290                    "ceo" => &armor_key,
291                    "trigger_weighting" => "0.1",
292                    "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_ancillary_armour_character_all|{armor_key}")),
293                ])?;
294                if !added_paths.contains(&p) { added_paths.push(p); }
295                // career into career_all group
296                let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
297                    "ceo_group" => "3k_main_ceo_group_career_all",
298                    "ceo" => &career_key_grp,
299                    "trigger_weighting" => "1",
300                    "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_career_all|{career_key_grp}")),
301                ])?;
302                if !added_paths.contains(&p) { added_paths.push(p); }
303
304                // ceo_permissions
305                let perm_key = format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}");
306                let p = insert_row(pack, schema, "ceo_permissions_tables", stem, &row![
307                    "key" => &perm_key,
308                ])?;
309                if !added_paths.contains(&p) { added_paths.push(p); }
310
311                // ceo_permissions_groups
312                let p = insert_row(pack, schema, "ceo_permissions_groups_tables", stem, &row![
313                    "permissions" => &perm_key,
314                    "group" => &grp_key,
315                    "point_gain_enabled_override" => "true",
316                    "point_gain_disabled_override" => "false",
317                    "state_active_override" => "true",
318                    "state_inactive_override" => "false",
319                    "auto_id" => auto_id("ceo_permissions_groups", &format!("{perm_key}|{grp_key}")),
320                ])?;
321                if !added_paths.contains(&p) { added_paths.push(p); }
322
323                // ceo_scripted_permissions
324                let scripted_perm_key = format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}");
325                let p = insert_row(pack, schema, "ceo_scripted_permissions_tables", stem, &row![
326                    "key" => &scripted_perm_key,
327                    "exists_in_and_provides_permission_to_location" => "character_ceo_manager",
328                ])?;
329                if !added_paths.contains(&p) { added_paths.push(p); }
330
331                // ceo_scripted_permissions_to_permissions
332                let p = insert_row(pack, schema, "ceo_scripted_permissions_to_permissions_tables", stem, &row![
333                    "scripted_permissions" => &scripted_perm_key,
334                    "permissions" => &perm_key,
335                    "auto_id" => auto_id("ceo_scripted_permissions_to_permissions", &format!("{scripted_perm_key}|{perm_key}")),
336                ])?;
337                if !added_paths.contains(&p) { added_paths.push(p); }
338
339                // ceo_initial_data_stages — two stages
340                let stage1_key = format!("3k_main_ceo_initial_data_stage_character_traits_historical_{n}");
341                let stage2_key = format!("3k_main_ceo_initial_data_character_historical_{n}_ancillaries");
342                for key in &[&stage1_key, &stage2_key] {
343                    let p = insert_row(pack, schema, "ceo_initial_data_stages_tables", stem, &row![
344                        "key" => *key,
345                    ])?;
346                    if !added_paths.contains(&p) { added_paths.push(p); }
347                }
348
349                // ceo_effect_lists — armor + career
350                let effect_list_armor = format!("3k_main_ancilliary_armour_{n}_armour_unique");
351                let effect_list_career = format!("3k_main_ceo_career_historical_{n}");
352                for key in &[&effect_list_armor, &effect_list_career] {
353                    let p = insert_row(pack, schema, "ceo_effect_lists_tables", stem, &row![
354                        "key" => *key,
355                    ])?;
356                    if !added_paths.contains(&p) { added_paths.push(p); }
357                }
358
359                // ceo_initial_datas
360                let initial_data_key = format!("3k_main_ceo_initial_data_character_historical_{n}");
361                let p = insert_row(pack, schema, "ceo_initial_datas_tables", stem, &row![
362                    "key" => &initial_data_key,
363                    "template_manager" => "character_ceo_manager",
364                ])?;
365                if !added_paths.contains(&p) { added_paths.push(p); }
366
367                // Element-specific: scripted permissions + equipments + active_ceos + to_stages
368                let armor_ceo_key = format!("3k_main_ancilliary_armour_{n}_armour_unique");
369                match element.as_str() {
370                    "metal" => {
371                        let armour_perm_key = format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}");
372                        for scripted in &[
373                            "3k_main_ceo_permissions_ancillary_weapon_character_sword_dual_enable",
374                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_dual_enable",
375                            "3k_main_ceo_permissions_ancillary_weapon_character_sword_one_handed_enable",
376                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_one_handed_enable",
377                            "3k_ytr_ceo_permissions_ancillary_weapon_character_mace_dual_enable",
378                            armour_perm_key.as_str(),
379                        ] {
380                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
381                                "initial_data_stage" => &stage2_key,
382                                "scripted_permissions" => *scripted,
383                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
384                            ])?;
385                            if !added_paths.contains(&p) { added_paths.push(p); }
386                        }
387                        for (category, equipped_ceo) in &[
388                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_double_edged_sword_common"),
389                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_grey_horse"),
390                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
391                        ] {
392                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
393                                "initial_data_stage" => &stage2_key,
394                                "category" => *category,
395                                "equipped_ceo" => *equipped_ceo,
396                                "slot_index" => "0",
397                                "target" => "character_equipment",
398                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
399                            ])?;
400                            if !added_paths.contains(&p) { added_paths.push(p); }
401                        }
402                        for active_ceo in &[
403                            "3k_main_ancillary_weapon_single_edged_sword_common",
404                            "3k_ytr_ancillary_weapon_dual_maces_common",
405                            "3k_main_ancillary_weapon_one_handed_axe_common",
406                            "3k_main_ancillary_weapon_dual_swords_common",
407                            "3k_main_ancillary_weapon_double_edged_sword_common",
408                            "3k_main_ancillary_mount_grey_horse",
409                        ] {
410                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
411                                "initial_data_stage" => &stage2_key,
412                                "active_ceo" => *active_ceo,
413                                "starting_points_delta" => "0",
414                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
415                            ])?;
416                            if !added_paths.contains(&p) { added_paths.push(p); }
417                        }
418                        // class into stage1
419                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
420                            "initial_data_stage" => &stage1_key,
421                            "active_ceo" => "3k_main_ceo_class_metal",
422                            "starting_points_delta" => "0",
423                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_metal")),
424                        ])?;
425                        if !added_paths.contains(&p) { added_paths.push(p); }
426                        // expanded: sword_and_shield only if expanded
427                        if entry.expanded {
428                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
429                                "initial_data_stage" => &stage2_key,
430                                "active_ceo" => "3k_main_ancillary_weapon_sword_and_shield_common",
431                                "starting_points_delta" => "0",
432                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|3k_main_ancillary_weapon_sword_and_shield_common")),
433                            ])?;
434                            if !added_paths.contains(&p) { added_paths.push(p); }
435                        }
436                        // armour effect: expertise_mod
437                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
438                            "effect_list" => &effect_list_armor,
439                            "effect" => "3k_main_effect_character_attribute_expertise_mod",
440                            "value" => "18",
441                            "effect_scope" => "character_to_character_own",
442                            "optional_only_in_game_mode" => "",
443                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|expertise")),
444                        ])?;
445                        if !added_paths.contains(&p) { added_paths.push(p); }
446                        for (id_stage, stage_num) in &[
447                            ("3k_main_ceo_initial_data_stage_character_childhood_metal", 17i32),
448                            ("3k_main_ceo_initial_data_equipment_permissions_unique_metal", 4i32),
449                        ] {
450                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
451                                "ceo_initial_data" => &initial_data_key,
452                                "initial_data_stage" => *id_stage,
453                                "stage" => stage_num,
454                            ])?;
455                            if !added_paths.contains(&p) { added_paths.push(p); }
456                        }
457                    },
458                    "water" => {
459                        for scripted in &[
460                            "3k_main_ceo_permissions_ancillary_weapon_character_sword_one_handed_enable",
461                            &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
462                        ] {
463                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
464                                "initial_data_stage" => &stage2_key,
465                                "scripted_permissions" => *scripted,
466                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
467                            ])?;
468                            if !added_paths.contains(&p) { added_paths.push(p); }
469                        }
470                        for (category, equipped_ceo) in &[
471                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_black_horse"),
472                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_single_edged_sword_common"),
473                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
474                        ] {
475                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
476                                "initial_data_stage" => &stage2_key,
477                                "category" => *category,
478                                "equipped_ceo" => *equipped_ceo,
479                                "slot_index" => "0",
480                                "target" => "character_equipment",
481                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
482                            ])?;
483                            if !added_paths.contains(&p) { added_paths.push(p); }
484                        }
485                        for active_ceo in &[
486                            "3k_main_ancillary_weapon_single_edged_sword_common",
487                            "3k_main_ancillary_weapon_dual_swords_common",
488                            "3k_main_ancillary_weapon_double_edged_sword_common",
489                            "3k_main_ancillary_mount_black_horse",
490                        ] {
491                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
492                                "initial_data_stage" => &stage2_key,
493                                "active_ceo" => *active_ceo,
494                                "starting_points_delta" => "0",
495                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
496                            ])?;
497                            if !added_paths.contains(&p) { added_paths.push(p); }
498                        }
499                        // armour active_ceo into stage2
500                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
501                            "initial_data_stage" => &stage2_key,
502                            "active_ceo" => &armor_ceo_key,
503                            "starting_points_delta" => "0",
504                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
505                        ])?;
506                        if !added_paths.contains(&p) { added_paths.push(p); }
507                        // class into stage1
508                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
509                            "initial_data_stage" => &stage1_key,
510                            "active_ceo" => "3k_main_ceo_class_water",
511                            "starting_points_delta" => "0",
512                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_water")),
513                        ])?;
514                        if !added_paths.contains(&p) { added_paths.push(p); }
515                        // armour effect: cunning_mod
516                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
517                            "effect_list" => &effect_list_armor,
518                            "effect" => "3k_main_effect_character_attribute_cunning_mod",
519                            "value" => "18",
520                            "effect_scope" => "character_to_character_own",
521                            "optional_only_in_game_mode" => "",
522                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|cunning")),
523                        ])?;
524                        if !added_paths.contains(&p) { added_paths.push(p); }
525                        for (id_stage, stage_num) in &[
526                            ("3k_main_ceo_initial_data_stage_character_childhood_water", 17i32),
527                            ("3k_main_ceo_initial_data_equipment_permissions_unique_water", 4i32),
528                        ] {
529                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
530                                "ceo_initial_data" => &initial_data_key,
531                                "initial_data_stage" => *id_stage,
532                                "stage" => stage_num,
533                            ])?;
534                            if !added_paths.contains(&p) { added_paths.push(p); }
535                        }
536                    },
537                    "earth" => {
538                        for scripted in &[
539                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_dual_enable",
540                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_one_handed_enable",
541                            &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
542                            "3k_main_ceo_permissions_ancillary_weapon_character_sword_one_handed_enable",
543                            "3k_main_ceo_permissions_ancillary_weapon_character_sword_dual_enable",
544                        ] {
545                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
546                                "initial_data_stage" => &stage2_key,
547                                "scripted_permissions" => *scripted,
548                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
549                            ])?;
550                            if !added_paths.contains(&p) { added_paths.push(p); }
551                        }
552                        for (category, equipped_ceo) in &[
553                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_single_edged_sword_common"),
554                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_white_horse"),
555                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
556                        ] {
557                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
558                                "initial_data_stage" => &stage2_key,
559                                "category" => *category,
560                                "equipped_ceo" => *equipped_ceo,
561                                "slot_index" => "0",
562                                "target" => "character_equipment",
563                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
564                            ])?;
565                            if !added_paths.contains(&p) { added_paths.push(p); }
566                        }
567                        for active_ceo in &[
568                            "3k_main_ancillary_weapon_one_handed_axe_common",
569                            "3k_main_ancillary_weapon_dual_swords_common",
570                            "3k_main_ancillary_mount_white_horse",
571                            "3k_main_ancillary_weapon_single_edged_sword_common",
572                            "3k_main_ancillary_weapon_double_edged_sword_common",
573                        ] {
574                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
575                                "initial_data_stage" => &stage2_key,
576                                "active_ceo" => *active_ceo,
577                                "starting_points_delta" => "0",
578                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
579                            ])?;
580                            if !added_paths.contains(&p) { added_paths.push(p); }
581                        }
582                        // armour active_ceo into stage2
583                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
584                            "initial_data_stage" => &stage2_key,
585                            "active_ceo" => &armor_ceo_key,
586                            "starting_points_delta" => "0",
587                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
588                        ])?;
589                        if !added_paths.contains(&p) { added_paths.push(p); }
590                        // class into stage1
591                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
592                            "initial_data_stage" => &stage1_key,
593                            "active_ceo" => "3k_main_ceo_class_earth",
594                            "starting_points_delta" => "0",
595                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_earth")),
596                        ])?;
597                        if !added_paths.contains(&p) { added_paths.push(p); }
598                        // expanded: sword_and_shield
599                        if entry.expanded {
600                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
601                                "initial_data_stage" => &stage2_key,
602                                "active_ceo" => "3k_main_ancillary_weapon_sword_and_shield_common",
603                                "starting_points_delta" => "0",
604                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|3k_main_ancillary_weapon_sword_and_shield_common")),
605                            ])?;
606                            if !added_paths.contains(&p) { added_paths.push(p); }
607                        }
608                        // armour effect: authority_mod
609                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
610                            "effect_list" => &effect_list_armor,
611                            "effect" => "3k_main_effect_character_attribute_authority_mod",
612                            "value" => "18",
613                            "effect_scope" => "character_to_character_own",
614                            "optional_only_in_game_mode" => "",
615                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|authority")),
616                        ])?;
617                        if !added_paths.contains(&p) { added_paths.push(p); }
618                        for (id_stage, stage_num) in &[
619                            ("3k_main_ceo_initial_data_stage_character_childhood_earth", 17i32),
620                            ("3k_main_ceo_initial_data_equipment_permissions_unique_earth", 4i32),
621                        ] {
622                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
623                                "ceo_initial_data" => &initial_data_key,
624                                "initial_data_stage" => *id_stage,
625                                "stage" => stage_num,
626                            ])?;
627                            if !added_paths.contains(&p) { added_paths.push(p); }
628                        }
629                    },
630                    "fire" => {
631                        for scripted in &[
632                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_two_handed_enable",
633                            "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_long_enable",
634                            &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
635                            "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_enable",
636                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_two_handed_enable",
637                        ] {
638                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
639                                "initial_data_stage" => &stage2_key,
640                                "scripted_permissions" => *scripted,
641                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
642                            ])?;
643                            if !added_paths.contains(&p) { added_paths.push(p); }
644                        }
645                        for (category, equipped_ceo) in &[
646                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_red_horse"),
647                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_two_handed_spear_common"),
648                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
649                        ] {
650                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
651                                "initial_data_stage" => &stage2_key,
652                                "category" => *category,
653                                "equipped_ceo" => *equipped_ceo,
654                                "slot_index" => "0",
655                                "target" => "character_equipment",
656                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
657                            ])?;
658                            if !added_paths.contains(&p) { added_paths.push(p); }
659                        }
660                        for active_ceo in &[
661                            "3k_main_ancillary_weapon_hook_sickle_sabre_common",
662                            "3k_main_ancillary_weapon_two_handed_axe_common",
663                            "3k_main_ancillary_weapon_two_handed_spear_common",
664                            "3k_main_ancillary_weapon_halberd_common",
665                            "3k_main_ancillary_mount_red_horse",
666                        ] {
667                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
668                                "initial_data_stage" => &stage2_key,
669                                "active_ceo" => *active_ceo,
670                                "starting_points_delta" => "0",
671                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
672                            ])?;
673                            if !added_paths.contains(&p) { added_paths.push(p); }
674                        }
675                        // armour active_ceo into stage2
676                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
677                            "initial_data_stage" => &stage2_key,
678                            "active_ceo" => &armor_ceo_key,
679                            "starting_points_delta" => "0",
680                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
681                        ])?;
682                        if !added_paths.contains(&p) { added_paths.push(p); }
683                        // class into stage1
684                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
685                            "initial_data_stage" => &stage1_key,
686                            "active_ceo" => "3k_main_ceo_class_fire",
687                            "starting_points_delta" => "0",
688                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_fire")),
689                        ])?;
690                        if !added_paths.contains(&p) { added_paths.push(p); }
691                        // armour effect: instinct_mod
692                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
693                            "effect_list" => &effect_list_armor,
694                            "effect" => "3k_main_effect_character_attribute_instinct_mod",
695                            "value" => "18",
696                            "effect_scope" => "character_to_character_own",
697                            "optional_only_in_game_mode" => "",
698                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|instinct")),
699                        ])?;
700                        if !added_paths.contains(&p) { added_paths.push(p); }
701                        for (id_stage, stage_num) in &[
702                            ("3k_main_ceo_initial_data_stage_character_childhood_fire", 17i32),
703                            ("3k_main_ceo_initial_data_equipment_permissions_unique_fire", 4i32),
704                        ] {
705                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
706                                "ceo_initial_data" => &initial_data_key,
707                                "initial_data_stage" => *id_stage,
708                                "stage" => stage_num,
709                            ])?;
710                            if !added_paths.contains(&p) { added_paths.push(p); }
711                        }
712                    },
713                    _ => { // wood
714                        for scripted in &[
715                            &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
716                            "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_enable",
717                            "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_long_enable",
718                            "3k_ytr_ceo_permissions_ancillary_weapon_character_staff_two_handed_enable",
719                            "3k_ytr_ceo_permissions_ancillary_weapon_character_mace_two_handed_enable",
720                        ] {
721                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
722                                "initial_data_stage" => &stage2_key,
723                                "scripted_permissions" => *scripted,
724                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
725                            ])?;
726                            if !added_paths.contains(&p) { added_paths.push(p); }
727                        }
728                        for (category, equipped_ceo) in &[
729                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_brown_horse"),
730                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_two_handed_spear_common"),
731                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
732                        ] {
733                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
734                                "initial_data_stage" => &stage2_key,
735                                "category" => *category,
736                                "equipped_ceo" => *equipped_ceo,
737                                "slot_index" => "0",
738                                "target" => "character_equipment",
739                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
740                            ])?;
741                            if !added_paths.contains(&p) { added_paths.push(p); }
742                        }
743                        for active_ceo in &[
744                            "3k_ytr_ancillary_weapon_2h_ball_mace_common",
745                            "3k_main_ancillary_weapon_hook_sickle_sabre_common",
746                            "3k_main_ancillary_mount_brown_horse",
747                            "3k_main_ancillary_weapon_two_handed_spear_common",
748                            "3k_main_ancillary_weapon_halberd_common",
749                        ] {
750                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
751                                "initial_data_stage" => &stage2_key,
752                                "active_ceo" => *active_ceo,
753                                "starting_points_delta" => "0",
754                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
755                            ])?;
756                            if !added_paths.contains(&p) { added_paths.push(p); }
757                        }
758                        // armour active_ceo into stage2
759                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
760                            "initial_data_stage" => &stage2_key,
761                            "active_ceo" => &armor_ceo_key,
762                            "starting_points_delta" => "0",
763                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
764                        ])?;
765                        if !added_paths.contains(&p) { added_paths.push(p); }
766                        // class into stage1
767                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
768                            "initial_data_stage" => &stage1_key,
769                            "active_ceo" => "3k_main_ceo_class_wood",
770                            "starting_points_delta" => "0",
771                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_wood")),
772                        ])?;
773                        if !added_paths.contains(&p) { added_paths.push(p); }
774                        // armour effect: resolve_mod
775                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
776                            "effect_list" => &effect_list_armor,
777                            "effect" => "3k_main_effect_character_attribute_resolve_mod",
778                            "value" => "18",
779                            "effect_scope" => "character_to_character_own",
780                            "optional_only_in_game_mode" => "",
781                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|resolve")),
782                        ])?;
783                        if !added_paths.contains(&p) { added_paths.push(p); }
784                        for (id_stage, stage_num) in &[
785                            ("3k_main_ceo_initial_data_stage_character_childhood_wood", 17i32),
786                            ("3k_main_ceo_initial_data_equipment_permissions_unique_wood", 4i32),
787                        ] {
788                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
789                                "ceo_initial_data" => &initial_data_key,
790                                "initial_data_stage" => *id_stage,
791                                "stage" => stage_num,
792                            ])?;
793                            if !added_paths.contains(&p) { added_paths.push(p); }
794                        }
795                    },
796                }
797                // Gender
798                let gender_stage = if gender == "male" {
799                    "3k_main_ceo_initial_data_stage_character_gender_male"
800                } else {
801                    "3k_main_ceo_initial_data_stage_character_gender_female"
802                };
803                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
804                    "ceo_initial_data" => &initial_data_key,
805                    "initial_data_stage" => gender_stage,
806                    "stage" => "13",
807                ])?;
808                if !added_paths.contains(&p) { added_paths.push(p); }
809
810                // Shared unique records after element/gender
811
812                // Link the _ancillaries stage (stage2_key) at stage order 3
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" => &stage2_key,
816                    "stage" => "3",
817                ])?;
818                if !added_paths.contains(&p) { added_paths.push(p); }
819
820                let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
821                    "initial_data_stage" => &stage1_key,
822                    "active_ceo" => &format!("3k_main_ceo_career_historical_{n}"),
823                    "starting_points_delta" => "0",
824                    "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_career_historical_{n}")),
825                ])?;
826                if !added_paths.contains(&p) { added_paths.push(p); }
827
828                for (id_stage, stage_num) in &[
829                    ("3k_main_initial_data_character_ancillaries_global", 2i32),
830                    ("3k_dlc04_ceo_initial_data_character_give_political_support_random", 21i32),
831                    ("3k_main_ceo_initial_data_stage_character_wealth_random", 15i32),
832                    ("3k_main_ceo_initial_data_stage_character_traits_shared_global_permissions", 10i32),
833                    ("3k_main_ceo_initial_data_stage_character_protagonist", 14i32),
834                ] {
835                    let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
836                        "ceo_initial_data" => &initial_data_key,
837                        "initial_data_stage" => *id_stage,
838                        "stage" => stage_num,
839                    ])?;
840                    if !added_paths.contains(&p) { added_paths.push(p); }
841                }
842                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
843                    "ceo_initial_data" => &initial_data_key,
844                    "initial_data_stage" => &stage1_key,
845                    "stage" => "11",
846                ])?;
847                if !added_paths.contains(&p) { added_paths.push(p); }
848
849                // ceo_thresholds
850                for (ceo_ref, key_val) in &[
851                    (format!("3k_main_ceo_career_historical_{n}"), format!("3k_main_ceo_career_historical_{n}")),
852                    (format!("3k_main_ancilliary_armour_{n}_armour_unique"), format!("3k_main_ancilliary_armour_{n}_armour_unique")),
853                ] {
854                    let p = insert_row(pack, schema, "ceo_thresholds_tables", stem, &row![
855                        "key" => key_val,
856                        "ceo" => ceo_ref,
857                        "point_threshold_to_activate" => "1",
858                        "point_theshold_to_destroy" => "0",
859                        "starting_points" => "1",
860                        "max_points" => "1",
861                        "resets_to_starting_points_when_deactivated" => "false",
862                    ])?;
863                    if !added_paths.contains(&p) { added_paths.push(p); }
864                }
865
866                // ceo_nodes — career
867                let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
868                    "key" => format!("3k_main_ceo_career_historical_{n}"),
869                    "ceo_effect_list" => &effect_list_career,
870                    "title" => "placeholder",
871                    "description" => "placeholder",
872                    "icon_path" => "",
873                    "opinion_topic_modifier" => "",
874                    "point_change_per_turn_if_active" => "0",
875                ])?;
876                if !added_paths.contains(&p) { added_paths.push(p); }
877                // ceo_nodes — armour (with element-specific icon)
878                let armour_icon = format!("armours/3k_main_ancillary_{}_armour_unique.png", element);
879                let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
880                    "key" => format!("3k_main_ancilliary_armour_{n}_armour_unique"),
881                    "ceo_effect_list" => &effect_list_armor,
882                    "title" => "placeholder",
883                    "description" => "placeholder",
884                    "icon_path" => &armour_icon,
885                    "opinion_topic_modifier" => "",
886                    "point_change_per_turn_if_active" => "0",
887                ])?;
888                if !added_paths.contains(&p) { added_paths.push(p); }
889
890                // ceo_threshold_nodes
891                for (node_key, threshold_key) in &[
892                    (format!("3k_main_ceo_career_historical_{n}"), format!("3k_main_ceo_career_historical_{n}")),
893                    (format!("3k_main_ancilliary_armour_{n}_armour_unique"), format!("3k_main_ancilliary_armour_{n}_armour_unique")),
894                ] {
895                    let p = insert_row(pack, schema, "ceo_threshold_nodes_tables", stem, &row![
896                        "ceo_threshold" => threshold_key,
897                        "ceo_node" => node_key,
898                        "points_threshold_to_activate_node" => "1",
899                        "can_downgrade_to_previous_node" => "false",
900                        "auto_id" => auto_id("ceo_threshold_nodes", &format!("{threshold_key}|{node_key}")),
901                    ])?;
902                    if !added_paths.contains(&p) { added_paths.push(p); }
903                }
904
905                // ceo_effect_list_to_effects — dummy subcategory for armour
906                let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
907                    "effect_list" => &effect_list_armor,
908                    "effect" => "3k_dummy_effect_ceo_subcategory_armour_unique",
909                    "value" => "0",
910                    "effect_scope" => "character_to_character_own",
911                    "optional_only_in_game_mode" => "",
912                    "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|dummy_subcategory")),
913                ])?;
914                if !added_paths.contains(&p) { added_paths.push(p); }
915
916                // ceo_effect_list_to_effects (career: wealth + lives)
917                for (effect, val) in &[
918                    ("3k_main_character_wealth", "2"),
919                    ("3k_main_effect_character_num_lives", "1"),
920                ] {
921                    let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
922                        "effect_list" => &effect_list_career,
923                        "effect" => *effect,
924                        "value" => *val,
925                        "effect_scope" => "character_to_character_own",
926                        "optional_only_in_game_mode" => "",
927                        "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_career}|{effect}")),
928                    ])?;
929                    if !added_paths.contains(&p) { added_paths.push(p); }
930                }
931
932                // Traits
933                for (trait_uuid, trait_key) in &entry.traits {
934                    let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
935                        "initial_data_stage" => &stage1_key,
936                        "active_ceo" => trait_key.as_str(),
937                        "starting_points_delta" => "0",
938                        "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{trait_key}|{trait_uuid}")),
939                    ])?;
940                    if !added_paths.contains(&p) { added_paths.push(p); }
941                }
942
943                // ceos_to_equipment_variants
944                let p = insert_row(pack, schema, "ceos_to_equipment_variants_tables", stem, &row![
945                    "ceos_key" => &format!("3k_main_ancilliary_armour_{n}_armour_unique"),
946                    "game_mode" => "",
947                    "armour" => "3k_ytr_hero_scholar_unique",
948                    "male_vmd" => "",
949                    "female_vmd" => "",
950                    "mount" => "",
951                    "primary_melee_weapon" => "",
952                    "primary_missile_weapon" => "",
953                    "shield" => "",
954                    "man_animation" => "",
955                    "mount_animation" => "",
956                    "secondary_weapon_animation" => "",
957                    "remap_general_unit_to_hero_unit" => "false",
958                    "priority" => "1",
959                    "autonomous_rider_group" => "",
960                    "ground_type_stat_effect_group" => "",
961                ])?;
962                if !added_paths.contains(&p) { added_paths.push(p); }
963
964                // ceo_template_manager_ceo_limits — limit unique armour to 1 globally
965                let p = insert_row(pack, schema, "ceo_template_manager_ceo_limits_tables", stem, &row![
966                    "ceo_to_limit" => &format!("3k_main_ancilliary_armour_{n}_armour_unique"),
967                    "template_manager" => "3k_main_ceo_template_manager_world_generic",
968                    "max_limit_that_can_exist_at_once" => "1",
969                    "scoped_limit_or_local_only_limit" => "true",
970                    "ceo_category_to_limit" => "",
971                    "ceo_node_to_limit" => "",
972                    "auto_id" => auto_id("ceo_template_manager_ceo_limits", &format!("3k_main_ancilliary_armour_{n}_armour_unique")),
973                ])?;
974                if !added_paths.contains(&p) { added_paths.push(p); }
975
976                // Loc entries for unique
977                let human = n.replace("_ironic", "")
978                    .split('_')
979                    .map(|w: &str| {
980                        let mut c = w.chars();
981                        c.next().map(|f: char| f.to_uppercase().collect::<String>() + c.as_str()).unwrap_or_default()
982                    })
983                    .collect::<Vec<_>>()
984                    .join(" ");
985                loc_entries.push((format!("ceo_nodes_title_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
986                loc_entries.push((format!("ceo_nodes_description_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
987                loc_entries.push((format!("ceo_nodes_title_3k_main_ancilliary_armour_{n}_armour_unique"), format!("{human}'s Armour")));
988                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()));
989
990            } else {
991                // ── TITLE PATH ────────────────────────────────
992
993                // ceos
994                let p = insert_row(pack, schema, "ceos_tables", stem, &row![
995                    "key" => format!("3k_main_ceo_career_historical_{n}"),
996                    "exists_in_location" => "character_ceo_manager",
997                    "category" => "3k_main_ceo_category_career",
998                    "equipped_in_location" => "character_equipment",
999                    "priority" => "1",
1000                    "turns_to_expire" => "0",
1001                    "point_change_per_turn_if_inactive" => "0",
1002                    "point_change_per_turn_while_active" => "0",
1003                    "point_change_per_turn_while_equipped" => "0",
1004                    "inheritance_chance" => "0",
1005                    "can_be_looted_post_battle" => "false",
1006                    "can_be_traded_in_diplomacy" => "false",
1007                    "can_be_stolen" => "false",
1008                    "rarity" => "common",
1009                    "can_be_unequipped" => "false",
1010                    "can_be_transferred_if_equipped" => "true",
1011                    "cannot_reequip_until_next_round_if_unequipped" => "true",
1012                    "provides_scripted_permissions_on_spawn" => "",
1013                ])?;
1014                if !added_paths.contains(&p) { added_paths.push(p); }
1015
1016                // ceo_group_ceos — career into career_all
1017                let career_key_title = format!("3k_main_ceo_career_historical_{n}");
1018                let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
1019                    "ceo_group" => "3k_main_ceo_group_career_all",
1020                    "ceo" => &career_key_title,
1021                    "trigger_weighting" => "1",
1022                    "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_career_all|{career_key_title}")),
1023                ])?;
1024                if !added_paths.contains(&p) { added_paths.push(p); }
1025
1026                let stage1_key = format!("3k_main_ceo_initial_data_stage_character_traits_historical_{n}");
1027                let p = insert_row(pack, schema, "ceo_initial_data_stages_tables", stem, &row![
1028                    "key" => &stage1_key,
1029                ])?;
1030                if !added_paths.contains(&p) { added_paths.push(p); }
1031
1032                let effect_list_key = format!("3k_main_ceo_career_historical_{n}");
1033                let p = insert_row(pack, schema, "ceo_effect_lists_tables", stem, &row![
1034                    "key" => &effect_list_key,
1035                ])?;
1036                if !added_paths.contains(&p) { added_paths.push(p); }
1037
1038                let initial_data_key = format!("3k_main_ceo_initial_data_character_historical_{n}");
1039                let p = insert_row(pack, schema, "ceo_initial_datas_tables", stem, &row![
1040                    "key" => &initial_data_key,
1041                    "template_manager" => "character_ceo_manager",
1042                ])?;
1043                if !added_paths.contains(&p) { added_paths.push(p); }
1044
1045                // Element branch (title)
1046                let (childhood_stage, equipment_stage, class_ceo, generic_stage, generic_stage_num) = match element.as_str() {
1047                    "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),
1048                    "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),
1049                    "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),
1050                    "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),
1051                    _       => ("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),
1052                };
1053
1054                for (id_stage, stage_num) in &[
1055                    (childhood_stage, 17i32),
1056                    (equipment_stage, 4i32),
1057                ] {
1058                    let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1059                        "ceo_initial_data" => &initial_data_key,
1060                        "initial_data_stage" => *id_stage,
1061                        "stage" => stage_num,
1062                    ])?;
1063                    if !added_paths.contains(&p) { added_paths.push(p); }
1064                }
1065                let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1066                    "initial_data_stage" => &stage1_key,
1067                    "active_ceo" => class_ceo,
1068                    "starting_points_delta" => "0",
1069                    "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{class_ceo}")),
1070                ])?;
1071                if !added_paths.contains(&p) { added_paths.push(p); }
1072                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1073                    "ceo_initial_data" => &initial_data_key,
1074                    "initial_data_stage" => generic_stage,
1075                    "stage" => &generic_stage_num,
1076                ])?;
1077                if !added_paths.contains(&p) { added_paths.push(p); }
1078
1079                // Gender
1080                let gender_stage = if gender == "male" {
1081                    "3k_main_ceo_initial_data_stage_character_gender_male"
1082                } else {
1083                    "3k_main_ceo_initial_data_stage_character_gender_female"
1084                };
1085                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1086                    "ceo_initial_data" => &initial_data_key,
1087                    "initial_data_stage" => gender_stage,
1088                    "stage" => "13",
1089                ])?;
1090                if !added_paths.contains(&p) { added_paths.push(p); }
1091
1092                // Generic shared (title)
1093                let career_key = format!("3k_main_ceo_career_historical_{n}");
1094                let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1095                    "initial_data_stage" => &stage1_key,
1096                    "active_ceo" => &career_key,
1097                    "starting_points_delta" => "0",
1098                    "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{career_key}")),
1099                ])?;
1100                if !added_paths.contains(&p) { added_paths.push(p); }
1101
1102                for (id_stage, stage_num) in &[
1103                    ("3k_main_initial_data_character_ancillaries_global", 2i32),
1104                    ("3k_dlc04_ceo_initial_data_character_give_political_support_random", 21i32),
1105                    ("3k_main_ceo_initial_data_stage_character_wealth_random", 15i32),
1106                    ("3k_main_ceo_initial_data_stage_character_traits_shared_global_permissions", 10i32),
1107                    ("3k_main_ceo_initial_data_stage_character_protagonist", 14i32),
1108                ] {
1109                    let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1110                        "ceo_initial_data" => &initial_data_key,
1111                        "initial_data_stage" => *id_stage,
1112                        "stage" => stage_num,
1113                    ])?;
1114                    if !added_paths.contains(&p) { added_paths.push(p); }
1115                }
1116                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1117                    "ceo_initial_data" => &initial_data_key,
1118                    "initial_data_stage" => &stage1_key,
1119                    "stage" => "11",
1120                ])?;
1121                if !added_paths.contains(&p) { added_paths.push(p); }
1122
1123                // ceo_thresholds
1124                let p = insert_row(pack, schema, "ceo_thresholds_tables", stem, &row![
1125                    "key" => &career_key,
1126                    "ceo" => &career_key,
1127                    "point_threshold_to_activate" => "1",
1128                    "point_theshold_to_destroy" => "0",
1129                    "starting_points" => "1",
1130                    "max_points" => "1",
1131                    "resets_to_starting_points_when_deactivated" => "false",
1132                ])?;
1133                if !added_paths.contains(&p) { added_paths.push(p); }
1134
1135                // ceo_nodes
1136                let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
1137                    "key" => &career_key,
1138                    "ceo_effect_list" => &effect_list_key,
1139                    "title" => "placeholder",
1140                    "description" => "placeholder",
1141                    "icon_path" => "",
1142                    "opinion_topic_modifier" => "",
1143                    "point_change_per_turn_if_active" => "0",
1144                ])?;
1145                if !added_paths.contains(&p) { added_paths.push(p); }
1146
1147                // ceo_threshold_nodes
1148                let p = insert_row(pack, schema, "ceo_threshold_nodes_tables", stem, &row![
1149                    "ceo_threshold" => &career_key,
1150                    "ceo_node" => &career_key,
1151                    "points_threshold_to_activate_node" => "1",
1152                    "can_downgrade_to_previous_node" => "false",
1153                    "auto_id" => auto_id("ceo_threshold_nodes", &format!("{career_key}|{career_key}")),
1154                ])?;
1155                if !added_paths.contains(&p) { added_paths.push(p); }
1156
1157                // ceo_effect_list_to_effects
1158                for (effect, val) in &[
1159                    ("3k_main_character_wealth", "2"),
1160                    ("3k_main_effect_character_num_lives", "1"),
1161                ] {
1162                    let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
1163                        "effect_list" => &effect_list_key,
1164                        "effect" => *effect,
1165                        "value" => *val,
1166                        "effect_scope" => "character_to_character_own",
1167                        "optional_only_in_game_mode" => "",
1168                        "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_key}|{effect}")),
1169                    ])?;
1170                    if !added_paths.contains(&p) { added_paths.push(p); }
1171                }
1172
1173                // Traits
1174                for (trait_uuid, trait_key) in &entry.traits {
1175                    let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1176                        "initial_data_stage" => &stage1_key,
1177                        "active_ceo" => trait_key.as_str(),
1178                        "starting_points_delta" => "0",
1179                        "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{trait_key}|{trait_uuid}")),
1180                    ])?;
1181                    if !added_paths.contains(&p) { added_paths.push(p); }
1182                }
1183
1184                // Loc entries for title
1185                loc_entries.push((format!("ceo_nodes_title_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
1186                loc_entries.push((format!("ceo_nodes_description_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
1187            }
1188        }
1189
1190        // ── Write loc file ──────────────────────────────────────
1191        if !loc_entries.is_empty() {
1192            let loc_pairs: Vec<(&str, &str)> = loc_entries.iter()
1193                .map(|(k, v)| (k.as_str(), v.as_str()))
1194                .collect();
1195            let p = insert_loc_entries(pack, &loc_path, &loc_pairs)?;
1196            if !added_paths.contains(&p) { added_paths.push(p); }
1197        }
1198
1199        Ok(added_paths)
1200}
1201
1202/// Fetch all trait CEOs from the Assembly Kit data, resolving display names via loc.
1203pub fn get_trait_ceos(deps: &rpfm_extensions::dependencies::Dependencies) -> Vec<(String, String)> {
1204    let ak_tables = deps.asskit_only_db_tables();
1205    let mut trait_ceos: Vec<(String, String)> = Vec::new();
1206
1207    let trait_categories: HashSet<&str> = [
1208        "3k_main_ceo_category_traits_personality",
1209        "3k_main_ceo_category_traits_physical",
1210    ].iter().copied().collect();
1211
1212    info!("GetTraitCeos: AK tables available: {}", ak_tables.len());
1213
1214    // Helper to read rows from an AK table by name, extracting two columns.
1215    fn ak_lookup_pairs(ak_tables: &HashMap<String, DB>, table_name: &str, col_a: &str, col_b: &str) -> Vec<(String, String)> {
1216        let mut result = Vec::new();
1217        if let Some(db) = ak_tables.get(table_name) {
1218            let fields = db.definition().fields_processed();
1219            let a_idx = fields.iter().position(|f| f.name() == col_a);
1220            let b_idx = fields.iter().position(|f| f.name() == col_b);
1221            if let (Some(ai), Some(bi)) = (a_idx, b_idx) {
1222                for row in db.data().iter() {
1223                    result.push((row[ai].data_to_string().to_string(), row[bi].data_to_string().to_string()));
1224                }
1225            }
1226        }
1227        result
1228    }
1229
1230    // Step 1: Get all CEO keys that belong to trait categories from AK.
1231    if let Some(ceos_db) = ak_tables.get("ceos_tables") {
1232        let fields = ceos_db.definition().fields_processed();
1233        let key_idx = fields.iter().position(|f| f.name() == "key");
1234        let cat_idx = fields.iter().position(|f| f.name() == "category");
1235
1236        if let (Some(ki), Some(ci)) = (key_idx, cat_idx) {
1237            for row in ceos_db.data().iter() {
1238                let category = row[ci].data_to_string();
1239                if trait_categories.contains(&*category) {
1240                    let ceo_key = row[ki].data_to_string().to_string();
1241                    trait_ceos.push((ceo_key, String::new()));
1242                }
1243            }
1244        }
1245    }
1246
1247    info!("GetTraitCeos: found {} trait CEOs from AK ceos_tables", trait_ceos.len());
1248
1249    // Step 2: Walk ceos -> ceo_thresholds -> ceo_threshold_nodes -> ceo_nodes for display names.
1250    let ceo_to_threshold: HashMap<String, String> =
1251        ak_lookup_pairs(ak_tables, "ceo_thresholds_tables", "ceo", "key")
1252            .into_iter().collect();
1253
1254    let threshold_to_node: HashMap<String, String> =
1255        ak_lookup_pairs(ak_tables, "ceo_threshold_nodes_tables", "ceo_threshold", "ceo_node")
1256            .into_iter().collect();
1257
1258    let node_to_title: HashMap<String, String> =
1259        ak_lookup_pairs(ak_tables, "ceo_nodes_tables", "key", "title")
1260            .into_iter().collect();
1261
1262    // Build loc lookup from dependencies (game files have the loc data).
1263    let mut loc_lookup: HashMap<String, String> = HashMap::new();
1264    if let Ok(loc_files) = deps.loc_data(true, true) {
1265        for rfile in &loc_files {
1266            if let Ok(RFileDecoded::Loc(loc)) = rfile.decoded() {
1267                for row in loc.data().iter() {
1268                    if row.len() >= 2 {
1269                        let loc_key = row[0].data_to_string().to_string();
1270                        let loc_val = row[1].data_to_string().to_string();
1271                        loc_lookup.insert(loc_key, loc_val);
1272                    }
1273                }
1274            }
1275        }
1276    }
1277
1278    info!("GetTraitCeos: loc_lookup has {} entries, node_to_title has {} entries", loc_lookup.len(), node_to_title.len());
1279
1280    // Step 3: Resolve display names.
1281    for (ceo_key, display_name) in &mut trait_ceos {
1282        // Chain: ceo_key -> threshold -> node -> title (loc key) -> loc text
1283        let resolved = ceo_to_threshold.get(ceo_key.as_str())
1284            .and_then(|thresh| threshold_to_node.get(thresh))
1285            .and_then(|node| {
1286                // Try the title field value as a direct loc key
1287                node_to_title.get(node).and_then(|title_key| {
1288                    loc_lookup.get(title_key)
1289                        .or_else(|| {
1290                            // Try constructing "ceo_nodes_title_{node_key}"
1291                            let constructed = format!("ceo_nodes_title_{}", node);
1292                            loc_lookup.get(&constructed)
1293                        })
1294                })
1295            });
1296
1297        if let Some(name) = resolved {
1298            *display_name = name.clone();
1299        } else {
1300            // Fallback: humanize the key
1301            *display_name = ceo_key
1302                .replace("3k_main_ceo_trait_", "")
1303                .replace("3k_dlc", "")
1304                .replace("3k_ytr_ceo_trait_", "")
1305                .replace('_', " ");
1306        }
1307    }
1308
1309    // Sort by display name for the UI.
1310    trait_ceos.sort_by(|a, b| a.1.cmp(&b.1));
1311
1312    info!("GetTraitCeos: returning {} traits", trait_ceos.len());
1313
1314    trait_ceos
1315}
1316
1317/// Import ceo_data.ccd into the pack after BOB has run.
1318pub fn build_ceo_post(pack: &mut Pack, akit_path: &str) -> Result<Vec<ContainerPath>> {
1319    let ceo_ccd_path = PathBuf::from(akit_path)
1320        .join(r"working_data\campaigns\ceo_data.ccd");
1321
1322    if !ceo_ccd_path.exists() {
1323        return Err(anyhow!("ceo_data.ccd not found. Make sure BOB ran successfully."));
1324    }
1325
1326    let raw_bytes = std::fs::read(&ceo_ccd_path)
1327        .map_err(|e| anyhow!("Failed to read ceo_data.ccd: {e}"))?;
1328
1329    let mut rfile = RFile::new_from_vec(&raw_bytes, FileType::Unknown, 0, "campaigns/ceo_data.ccd");
1330    let _ = rfile.guess_file_type();
1331    match pack.insert(rfile) {
1332        Ok(Some(path)) => Ok(vec![path]),
1333        Ok(None) => Ok(vec![]),
1334        Err(e) => Err(anyhow!("{}", e)),
1335    }
1336}