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                        // armour active_ceo into stage2
427                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
428                            "initial_data_stage" => &stage2_key,
429                            "active_ceo" => &armor_ceo_key,
430                            "starting_points_delta" => "0",
431                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
432                        ])?;
433                        if !added_paths.contains(&p) { added_paths.push(p); }
434                        // expanded: sword_and_shield only if expanded
435                        if entry.expanded {
436                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
437                                "initial_data_stage" => &stage2_key,
438                                "active_ceo" => "3k_main_ancillary_weapon_sword_and_shield_common",
439                                "starting_points_delta" => "0",
440                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|3k_main_ancillary_weapon_sword_and_shield_common")),
441                            ])?;
442                            if !added_paths.contains(&p) { added_paths.push(p); }
443                        }
444                        // armour effect: expertise_mod
445                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
446                            "effect_list" => &effect_list_armor,
447                            "effect" => "3k_main_effect_character_attribute_expertise_mod",
448                            "value" => "18",
449                            "effect_scope" => "character_to_character_own",
450                            "optional_only_in_game_mode" => "",
451                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|expertise")),
452                        ])?;
453                        if !added_paths.contains(&p) { added_paths.push(p); }
454                        for (id_stage, stage_num) in &[
455                            ("3k_main_ceo_initial_data_stage_character_childhood_metal", 17i32),
456                            ("3k_main_ceo_initial_data_equipment_permissions_unique_metal", 4i32),
457                        ] {
458                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
459                                "ceo_initial_data" => &initial_data_key,
460                                "initial_data_stage" => *id_stage,
461                                "stage" => stage_num,
462                            ])?;
463                            if !added_paths.contains(&p) { added_paths.push(p); }
464                        }
465                    },
466                    "water" => {
467                        for scripted in &[
468                            "3k_main_ceo_permissions_ancillary_weapon_character_sword_one_handed_enable",
469                            &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
470                        ] {
471                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
472                                "initial_data_stage" => &stage2_key,
473                                "scripted_permissions" => *scripted,
474                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
475                            ])?;
476                            if !added_paths.contains(&p) { added_paths.push(p); }
477                        }
478                        for (category, equipped_ceo) in &[
479                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_black_horse"),
480                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_single_edged_sword_common"),
481                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
482                        ] {
483                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
484                                "initial_data_stage" => &stage2_key,
485                                "category" => *category,
486                                "equipped_ceo" => *equipped_ceo,
487                                "slot_index" => "0",
488                                "target" => "character_equipment",
489                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
490                            ])?;
491                            if !added_paths.contains(&p) { added_paths.push(p); }
492                        }
493                        for active_ceo in &[
494                            "3k_main_ancillary_weapon_single_edged_sword_common",
495                            "3k_main_ancillary_weapon_dual_swords_common",
496                            "3k_main_ancillary_weapon_double_edged_sword_common",
497                            "3k_main_ancillary_mount_black_horse",
498                        ] {
499                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
500                                "initial_data_stage" => &stage2_key,
501                                "active_ceo" => *active_ceo,
502                                "starting_points_delta" => "0",
503                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
504                            ])?;
505                            if !added_paths.contains(&p) { added_paths.push(p); }
506                        }
507                        // armour active_ceo into stage2
508                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
509                            "initial_data_stage" => &stage2_key,
510                            "active_ceo" => &armor_ceo_key,
511                            "starting_points_delta" => "0",
512                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
513                        ])?;
514                        if !added_paths.contains(&p) { added_paths.push(p); }
515                        // class into stage1
516                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
517                            "initial_data_stage" => &stage1_key,
518                            "active_ceo" => "3k_main_ceo_class_water",
519                            "starting_points_delta" => "0",
520                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_water")),
521                        ])?;
522                        if !added_paths.contains(&p) { added_paths.push(p); }
523                        // armour effect: cunning_mod
524                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
525                            "effect_list" => &effect_list_armor,
526                            "effect" => "3k_main_effect_character_attribute_cunning_mod",
527                            "value" => "18",
528                            "effect_scope" => "character_to_character_own",
529                            "optional_only_in_game_mode" => "",
530                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|cunning")),
531                        ])?;
532                        if !added_paths.contains(&p) { added_paths.push(p); }
533                        for (id_stage, stage_num) in &[
534                            ("3k_main_ceo_initial_data_stage_character_childhood_water", 17i32),
535                            ("3k_main_ceo_initial_data_equipment_permissions_unique_water", 4i32),
536                        ] {
537                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
538                                "ceo_initial_data" => &initial_data_key,
539                                "initial_data_stage" => *id_stage,
540                                "stage" => stage_num,
541                            ])?;
542                            if !added_paths.contains(&p) { added_paths.push(p); }
543                        }
544                    },
545                    "earth" => {
546                        for scripted in &[
547                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_dual_enable",
548                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_one_handed_enable",
549                            &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
550                            "3k_main_ceo_permissions_ancillary_weapon_character_sword_one_handed_enable",
551                            "3k_main_ceo_permissions_ancillary_weapon_character_sword_dual_enable",
552                        ] {
553                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
554                                "initial_data_stage" => &stage2_key,
555                                "scripted_permissions" => *scripted,
556                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
557                            ])?;
558                            if !added_paths.contains(&p) { added_paths.push(p); }
559                        }
560                        for (category, equipped_ceo) in &[
561                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_single_edged_sword_common"),
562                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_white_horse"),
563                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
564                        ] {
565                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
566                                "initial_data_stage" => &stage2_key,
567                                "category" => *category,
568                                "equipped_ceo" => *equipped_ceo,
569                                "slot_index" => "0",
570                                "target" => "character_equipment",
571                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
572                            ])?;
573                            if !added_paths.contains(&p) { added_paths.push(p); }
574                        }
575                        for active_ceo in &[
576                            "3k_main_ancillary_weapon_one_handed_axe_common",
577                            "3k_main_ancillary_weapon_dual_swords_common",
578                            "3k_main_ancillary_mount_white_horse",
579                            "3k_main_ancillary_weapon_single_edged_sword_common",
580                            "3k_main_ancillary_weapon_double_edged_sword_common",
581                        ] {
582                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
583                                "initial_data_stage" => &stage2_key,
584                                "active_ceo" => *active_ceo,
585                                "starting_points_delta" => "0",
586                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
587                            ])?;
588                            if !added_paths.contains(&p) { added_paths.push(p); }
589                        }
590                        // armour active_ceo into stage2
591                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
592                            "initial_data_stage" => &stage2_key,
593                            "active_ceo" => &armor_ceo_key,
594                            "starting_points_delta" => "0",
595                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
596                        ])?;
597                        if !added_paths.contains(&p) { added_paths.push(p); }
598                        // class into stage1
599                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
600                            "initial_data_stage" => &stage1_key,
601                            "active_ceo" => "3k_main_ceo_class_earth",
602                            "starting_points_delta" => "0",
603                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_earth")),
604                        ])?;
605                        if !added_paths.contains(&p) { added_paths.push(p); }
606                        // expanded: sword_and_shield
607                        if entry.expanded {
608                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
609                                "initial_data_stage" => &stage2_key,
610                                "active_ceo" => "3k_main_ancillary_weapon_sword_and_shield_common",
611                                "starting_points_delta" => "0",
612                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|3k_main_ancillary_weapon_sword_and_shield_common")),
613                            ])?;
614                            if !added_paths.contains(&p) { added_paths.push(p); }
615                        }
616                        // armour effect: authority_mod
617                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
618                            "effect_list" => &effect_list_armor,
619                            "effect" => "3k_main_effect_character_attribute_authority_mod",
620                            "value" => "18",
621                            "effect_scope" => "character_to_character_own",
622                            "optional_only_in_game_mode" => "",
623                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|authority")),
624                        ])?;
625                        if !added_paths.contains(&p) { added_paths.push(p); }
626                        for (id_stage, stage_num) in &[
627                            ("3k_main_ceo_initial_data_stage_character_childhood_earth", 17i32),
628                            ("3k_main_ceo_initial_data_equipment_permissions_unique_earth", 4i32),
629                        ] {
630                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
631                                "ceo_initial_data" => &initial_data_key,
632                                "initial_data_stage" => *id_stage,
633                                "stage" => stage_num,
634                            ])?;
635                            if !added_paths.contains(&p) { added_paths.push(p); }
636                        }
637                    },
638                    "fire" => {
639                        for scripted in &[
640                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_two_handed_enable",
641                            "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_long_enable",
642                            &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
643                            "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_enable",
644                            "3k_main_ceo_permissions_ancillary_weapon_character_axe_two_handed_enable",
645                        ] {
646                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
647                                "initial_data_stage" => &stage2_key,
648                                "scripted_permissions" => *scripted,
649                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
650                            ])?;
651                            if !added_paths.contains(&p) { added_paths.push(p); }
652                        }
653                        for (category, equipped_ceo) in &[
654                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_red_horse"),
655                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_two_handed_spear_common"),
656                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
657                        ] {
658                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
659                                "initial_data_stage" => &stage2_key,
660                                "category" => *category,
661                                "equipped_ceo" => *equipped_ceo,
662                                "slot_index" => "0",
663                                "target" => "character_equipment",
664                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
665                            ])?;
666                            if !added_paths.contains(&p) { added_paths.push(p); }
667                        }
668                        for active_ceo in &[
669                            "3k_main_ancillary_weapon_hook_sickle_sabre_common",
670                            "3k_main_ancillary_weapon_two_handed_axe_common",
671                            "3k_main_ancillary_weapon_two_handed_spear_common",
672                            "3k_main_ancillary_weapon_halberd_common",
673                            "3k_main_ancillary_mount_red_horse",
674                        ] {
675                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
676                                "initial_data_stage" => &stage2_key,
677                                "active_ceo" => *active_ceo,
678                                "starting_points_delta" => "0",
679                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
680                            ])?;
681                            if !added_paths.contains(&p) { added_paths.push(p); }
682                        }
683                        // armour active_ceo into stage2
684                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
685                            "initial_data_stage" => &stage2_key,
686                            "active_ceo" => &armor_ceo_key,
687                            "starting_points_delta" => "0",
688                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
689                        ])?;
690                        if !added_paths.contains(&p) { added_paths.push(p); }
691                        // class into stage1
692                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
693                            "initial_data_stage" => &stage1_key,
694                            "active_ceo" => "3k_main_ceo_class_fire",
695                            "starting_points_delta" => "0",
696                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_fire")),
697                        ])?;
698                        if !added_paths.contains(&p) { added_paths.push(p); }
699                        // armour effect: instinct_mod
700                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
701                            "effect_list" => &effect_list_armor,
702                            "effect" => "3k_main_effect_character_attribute_instinct_mod",
703                            "value" => "18",
704                            "effect_scope" => "character_to_character_own",
705                            "optional_only_in_game_mode" => "",
706                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|instinct")),
707                        ])?;
708                        if !added_paths.contains(&p) { added_paths.push(p); }
709                        for (id_stage, stage_num) in &[
710                            ("3k_main_ceo_initial_data_stage_character_childhood_fire", 17i32),
711                            ("3k_main_ceo_initial_data_equipment_permissions_unique_fire", 4i32),
712                        ] {
713                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
714                                "ceo_initial_data" => &initial_data_key,
715                                "initial_data_stage" => *id_stage,
716                                "stage" => stage_num,
717                            ])?;
718                            if !added_paths.contains(&p) { added_paths.push(p); }
719                        }
720                    },
721                    _ => { // wood
722                        for scripted in &[
723                            &format!("3k_main_ceo_permissions_ancillary_armour_character_specific_{n}"),
724                            "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_enable",
725                            "3k_main_ceo_permissions_ancillary_weapon_character_spear_two_handed_long_enable",
726                            "3k_ytr_ceo_permissions_ancillary_weapon_character_staff_two_handed_enable",
727                            "3k_ytr_ceo_permissions_ancillary_weapon_character_mace_two_handed_enable",
728                        ] {
729                            let p = insert_row(pack, schema, "ceo_initial_data_scripted_permissions_tables", stem, &row![
730                                "initial_data_stage" => &stage2_key,
731                                "scripted_permissions" => *scripted,
732                                "auto_id" => auto_id("ceo_initial_data_scripted_permissions", &format!("{stage2_key}|{scripted}")),
733                            ])?;
734                            if !added_paths.contains(&p) { added_paths.push(p); }
735                        }
736                        for (category, equipped_ceo) in &[
737                            ("3k_main_ceo_category_ancillary_mount",  "3k_main_ancillary_mount_brown_horse"),
738                            ("3k_main_ceo_category_ancillary_weapon", "3k_main_ancillary_weapon_two_handed_spear_common"),
739                            ("3k_main_ceo_category_ancillary_armour", armor_ceo_key.as_str()),
740                        ] {
741                            let p = insert_row(pack, schema, "ceo_initial_data_equipments_tables", stem, &row![
742                                "initial_data_stage" => &stage2_key,
743                                "category" => *category,
744                                "equipped_ceo" => *equipped_ceo,
745                                "slot_index" => "0",
746                                "target" => "character_equipment",
747                                "auto_id" => auto_id("ceo_initial_data_equipments", &format!("{stage2_key}|{equipped_ceo}")),
748                            ])?;
749                            if !added_paths.contains(&p) { added_paths.push(p); }
750                        }
751                        for active_ceo in &[
752                            "3k_ytr_ancillary_weapon_2h_ball_mace_common",
753                            "3k_main_ancillary_weapon_hook_sickle_sabre_common",
754                            "3k_main_ancillary_mount_brown_horse",
755                            "3k_main_ancillary_weapon_two_handed_spear_common",
756                            "3k_main_ancillary_weapon_halberd_common",
757                        ] {
758                            let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
759                                "initial_data_stage" => &stage2_key,
760                                "active_ceo" => *active_ceo,
761                                "starting_points_delta" => "0",
762                                "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{active_ceo}")),
763                            ])?;
764                            if !added_paths.contains(&p) { added_paths.push(p); }
765                        }
766                        // armour active_ceo into stage2
767                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
768                            "initial_data_stage" => &stage2_key,
769                            "active_ceo" => &armor_ceo_key,
770                            "starting_points_delta" => "0",
771                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage2_key}|{armor_ceo_key}")),
772                        ])?;
773                        if !added_paths.contains(&p) { added_paths.push(p); }
774                        // class into stage1
775                        let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
776                            "initial_data_stage" => &stage1_key,
777                            "active_ceo" => "3k_main_ceo_class_wood",
778                            "starting_points_delta" => "0",
779                            "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_class_wood")),
780                        ])?;
781                        if !added_paths.contains(&p) { added_paths.push(p); }
782                        // armour effect: resolve_mod
783                        let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
784                            "effect_list" => &effect_list_armor,
785                            "effect" => "3k_main_effect_character_attribute_resolve_mod",
786                            "value" => "18",
787                            "effect_scope" => "character_to_character_own",
788                            "optional_only_in_game_mode" => "",
789                            "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|resolve")),
790                        ])?;
791                        if !added_paths.contains(&p) { added_paths.push(p); }
792                        for (id_stage, stage_num) in &[
793                            ("3k_main_ceo_initial_data_stage_character_childhood_wood", 17i32),
794                            ("3k_main_ceo_initial_data_equipment_permissions_unique_wood", 4i32),
795                        ] {
796                            let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
797                                "ceo_initial_data" => &initial_data_key,
798                                "initial_data_stage" => *id_stage,
799                                "stage" => stage_num,
800                            ])?;
801                            if !added_paths.contains(&p) { added_paths.push(p); }
802                        }
803                    },
804                }
805                // Gender
806                let gender_stage = if gender == "male" {
807                    "3k_main_ceo_initial_data_stage_character_gender_male"
808                } else {
809                    "3k_main_ceo_initial_data_stage_character_gender_female"
810                };
811                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
812                    "ceo_initial_data" => &initial_data_key,
813                    "initial_data_stage" => gender_stage,
814                    "stage" => "13",
815                ])?;
816                if !added_paths.contains(&p) { added_paths.push(p); }
817
818                // Shared unique records after element/gender
819
820                // Link the _ancillaries stage (stage2_key) at stage order 3
821                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
822                    "ceo_initial_data" => &initial_data_key,
823                    "initial_data_stage" => &stage2_key,
824                    "stage" => "3",
825                ])?;
826                if !added_paths.contains(&p) { added_paths.push(p); }
827
828                let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
829                    "initial_data_stage" => &stage1_key,
830                    "active_ceo" => &format!("3k_main_ceo_career_historical_{n}"),
831                    "starting_points_delta" => "0",
832                    "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|3k_main_ceo_career_historical_{n}")),
833                ])?;
834                if !added_paths.contains(&p) { added_paths.push(p); }
835
836                for (id_stage, stage_num) in &[
837                    ("3k_main_initial_data_character_ancillaries_global", 2i32),
838                    ("3k_dlc04_ceo_initial_data_character_give_political_support_random", 21i32),
839                    ("3k_main_ceo_initial_data_stage_character_wealth_random", 15i32),
840                    ("3k_main_ceo_initial_data_stage_character_traits_shared_global_permissions", 10i32),
841                    ("3k_main_ceo_initial_data_stage_character_protagonist", 14i32),
842                ] {
843                    let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
844                        "ceo_initial_data" => &initial_data_key,
845                        "initial_data_stage" => *id_stage,
846                        "stage" => stage_num,
847                    ])?;
848                    if !added_paths.contains(&p) { added_paths.push(p); }
849                }
850                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
851                    "ceo_initial_data" => &initial_data_key,
852                    "initial_data_stage" => &stage1_key,
853                    "stage" => "11",
854                ])?;
855                if !added_paths.contains(&p) { added_paths.push(p); }
856
857                // ceo_thresholds
858                for (ceo_ref, key_val) in &[
859                    (format!("3k_main_ceo_career_historical_{n}"), format!("3k_main_ceo_career_historical_{n}")),
860                    (format!("3k_main_ancilliary_armour_{n}_armour_unique"), format!("3k_main_ancilliary_armour_{n}_armour_unique")),
861                ] {
862                    let p = insert_row(pack, schema, "ceo_thresholds_tables", stem, &row![
863                        "key" => key_val,
864                        "ceo" => ceo_ref,
865                        "point_threshold_to_activate" => "1",
866                        "point_theshold_to_destroy" => "0",
867                        "starting_points" => "1",
868                        "max_points" => "1",
869                        "resets_to_starting_points_when_deactivated" => "false",
870                    ])?;
871                    if !added_paths.contains(&p) { added_paths.push(p); }
872                }
873
874                // ceo_nodes — career
875                let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
876                    "key" => format!("3k_main_ceo_career_historical_{n}"),
877                    "ceo_effect_list" => &effect_list_career,
878                    "title" => "placeholder",
879                    "description" => "placeholder",
880                    "icon_path" => "",
881                    "opinion_topic_modifier" => "",
882                    "point_change_per_turn_if_active" => "0",
883                ])?;
884                if !added_paths.contains(&p) { added_paths.push(p); }
885                // ceo_nodes — armour (with element-specific icon)
886                let armour_icon = format!("armours/3k_main_ancillary_{}_armour_unique.png", element);
887                let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
888                    "key" => format!("3k_main_ancilliary_armour_{n}_armour_unique"),
889                    "ceo_effect_list" => &effect_list_armor,
890                    "title" => "placeholder",
891                    "description" => "placeholder",
892                    "icon_path" => &armour_icon,
893                    "opinion_topic_modifier" => "",
894                    "point_change_per_turn_if_active" => "0",
895                ])?;
896                if !added_paths.contains(&p) { added_paths.push(p); }
897
898                // ceo_threshold_nodes
899                for (node_key, threshold_key) in &[
900                    (format!("3k_main_ceo_career_historical_{n}"), format!("3k_main_ceo_career_historical_{n}")),
901                    (format!("3k_main_ancilliary_armour_{n}_armour_unique"), format!("3k_main_ancilliary_armour_{n}_armour_unique")),
902                ] {
903                    let p = insert_row(pack, schema, "ceo_threshold_nodes_tables", stem, &row![
904                        "ceo_threshold" => threshold_key,
905                        "ceo_node" => node_key,
906                        "points_threshold_to_activate_node" => "1",
907                        "can_downgrade_to_previous_node" => "false",
908                        "auto_id" => auto_id("ceo_threshold_nodes", &format!("{threshold_key}|{node_key}")),
909                    ])?;
910                    if !added_paths.contains(&p) { added_paths.push(p); }
911                }
912
913                // ceo_effect_list_to_effects — dummy subcategory for armour
914                let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
915                    "effect_list" => &effect_list_armor,
916                    "effect" => "3k_dummy_effect_ceo_subcategory_armour_unique",
917                    "value" => "0",
918                    "effect_scope" => "character_to_character_own",
919                    "optional_only_in_game_mode" => "",
920                    "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_armor}|dummy_subcategory")),
921                ])?;
922                if !added_paths.contains(&p) { added_paths.push(p); }
923
924                // ceo_effect_list_to_effects (career: wealth + lives)
925                for (effect, val) in &[
926                    ("3k_main_character_wealth", "2"),
927                    ("3k_main_effect_character_num_lives", "1"),
928                ] {
929                    let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
930                        "effect_list" => &effect_list_career,
931                        "effect" => *effect,
932                        "value" => *val,
933                        "effect_scope" => "character_to_character_own",
934                        "optional_only_in_game_mode" => "",
935                        "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_career}|{effect}")),
936                    ])?;
937                    if !added_paths.contains(&p) { added_paths.push(p); }
938                }
939
940                // Traits
941                for (trait_uuid, trait_key) in &entry.traits {
942                    let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
943                        "initial_data_stage" => &stage1_key,
944                        "active_ceo" => trait_key.as_str(),
945                        "starting_points_delta" => "0",
946                        "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{trait_key}|{trait_uuid}")),
947                    ])?;
948                    if !added_paths.contains(&p) { added_paths.push(p); }
949                }
950
951                // ceos_to_equipment_variants
952                let p = insert_row(pack, schema, "ceos_to_equipment_variants_tables", stem, &row![
953                    "ceos_key" => &format!("3k_main_ancilliary_armour_{n}_armour_unique"),
954                    "game_mode" => "",
955                    "armour" => "3k_ytr_hero_scholar_unique",
956                    "male_vmd" => "",
957                    "female_vmd" => "",
958                    "mount" => "",
959                    "primary_melee_weapon" => "",
960                    "primary_missile_weapon" => "",
961                    "shield" => "",
962                    "man_animation" => "",
963                    "mount_animation" => "",
964                    "secondary_weapon_animation" => "",
965                    "remap_general_unit_to_hero_unit" => "false",
966                    "priority" => "1",
967                    "autonomous_rider_group" => "",
968                    "ground_type_stat_effect_group" => "",
969                ])?;
970                if !added_paths.contains(&p) { added_paths.push(p); }
971
972                // ceo_template_manager_ceo_limits — limit unique armour to 1 globally
973                let p = insert_row(pack, schema, "ceo_template_manager_ceo_limits_tables", stem, &row![
974                    "ceo_to_limit" => &format!("3k_main_ancilliary_armour_{n}_armour_unique"),
975                    "template_manager" => "3k_main_ceo_template_manager_world_generic",
976                    "max_limit_that_can_exist_at_once" => "1",
977                    "scoped_limit_or_local_only_limit" => "true",
978                    "ceo_category_to_limit" => "",
979                    "ceo_node_to_limit" => "",
980                    "auto_id" => auto_id("ceo_template_manager_ceo_limits", &format!("3k_main_ancilliary_armour_{n}_armour_unique")),
981                ])?;
982                if !added_paths.contains(&p) { added_paths.push(p); }
983
984                // Loc entries for unique
985                let human = n.replace("_ironic", "")
986                    .split('_')
987                    .map(|w: &str| {
988                        let mut c = w.chars();
989                        c.next().map(|f: char| f.to_uppercase().collect::<String>() + c.as_str()).unwrap_or_default()
990                    })
991                    .collect::<Vec<_>>()
992                    .join(" ");
993                loc_entries.push((format!("ceo_nodes_title_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
994                loc_entries.push((format!("ceo_nodes_description_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
995                loc_entries.push((format!("ceo_nodes_title_3k_main_ancilliary_armour_{n}_armour_unique"), format!("{human}'s Armour")));
996                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()));
997
998            } else {
999                // ── TITLE PATH ────────────────────────────────
1000
1001                // ceos
1002                let p = insert_row(pack, schema, "ceos_tables", stem, &row![
1003                    "key" => format!("3k_main_ceo_career_historical_{n}"),
1004                    "exists_in_location" => "character_ceo_manager",
1005                    "category" => "3k_main_ceo_category_career",
1006                    "equipped_in_location" => "character_equipment",
1007                    "priority" => "1",
1008                    "turns_to_expire" => "0",
1009                    "point_change_per_turn_if_inactive" => "0",
1010                    "point_change_per_turn_while_active" => "0",
1011                    "point_change_per_turn_while_equipped" => "0",
1012                    "inheritance_chance" => "0",
1013                    "can_be_looted_post_battle" => "false",
1014                    "can_be_traded_in_diplomacy" => "false",
1015                    "can_be_stolen" => "false",
1016                    "rarity" => "common",
1017                    "can_be_unequipped" => "false",
1018                    "can_be_transferred_if_equipped" => "true",
1019                    "cannot_reequip_until_next_round_if_unequipped" => "true",
1020                    "provides_scripted_permissions_on_spawn" => "",
1021                ])?;
1022                if !added_paths.contains(&p) { added_paths.push(p); }
1023
1024                // ceo_group_ceos — career into career_all
1025                let career_key_title = format!("3k_main_ceo_career_historical_{n}");
1026                let p = insert_row(pack, schema, "ceo_group_ceos_tables", stem, &row![
1027                    "ceo_group" => "3k_main_ceo_group_career_all",
1028                    "ceo" => &career_key_title,
1029                    "trigger_weighting" => "1",
1030                    "auto_id" => auto_id("ceo_group_ceos", &format!("3k_main_ceo_group_career_all|{career_key_title}")),
1031                ])?;
1032                if !added_paths.contains(&p) { added_paths.push(p); }
1033
1034                let stage1_key = format!("3k_main_ceo_initial_data_stage_character_traits_historical_{n}");
1035                let p = insert_row(pack, schema, "ceo_initial_data_stages_tables", stem, &row![
1036                    "key" => &stage1_key,
1037                ])?;
1038                if !added_paths.contains(&p) { added_paths.push(p); }
1039
1040                let effect_list_key = format!("3k_main_ceo_career_historical_{n}");
1041                let p = insert_row(pack, schema, "ceo_effect_lists_tables", stem, &row![
1042                    "key" => &effect_list_key,
1043                ])?;
1044                if !added_paths.contains(&p) { added_paths.push(p); }
1045
1046                let initial_data_key = format!("3k_main_ceo_initial_data_character_historical_{n}");
1047                let p = insert_row(pack, schema, "ceo_initial_datas_tables", stem, &row![
1048                    "key" => &initial_data_key,
1049                    "template_manager" => "character_ceo_manager",
1050                ])?;
1051                if !added_paths.contains(&p) { added_paths.push(p); }
1052
1053                // Element branch (title)
1054                let (childhood_stage, equipment_stage, class_ceo, generic_stage, generic_stage_num) = match element.as_str() {
1055                    "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),
1056                    "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),
1057                    "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),
1058                    "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),
1059                    _       => ("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),
1060                };
1061
1062                for (id_stage, stage_num) in &[
1063                    (childhood_stage, 17i32),
1064                    (equipment_stage, 4i32),
1065                ] {
1066                    let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1067                        "ceo_initial_data" => &initial_data_key,
1068                        "initial_data_stage" => *id_stage,
1069                        "stage" => stage_num,
1070                    ])?;
1071                    if !added_paths.contains(&p) { added_paths.push(p); }
1072                }
1073                let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1074                    "initial_data_stage" => &stage1_key,
1075                    "active_ceo" => class_ceo,
1076                    "starting_points_delta" => "0",
1077                    "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{class_ceo}")),
1078                ])?;
1079                if !added_paths.contains(&p) { added_paths.push(p); }
1080                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1081                    "ceo_initial_data" => &initial_data_key,
1082                    "initial_data_stage" => generic_stage,
1083                    "stage" => &generic_stage_num,
1084                ])?;
1085                if !added_paths.contains(&p) { added_paths.push(p); }
1086
1087                // Gender
1088                let gender_stage = if gender == "male" {
1089                    "3k_main_ceo_initial_data_stage_character_gender_male"
1090                } else {
1091                    "3k_main_ceo_initial_data_stage_character_gender_female"
1092                };
1093                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1094                    "ceo_initial_data" => &initial_data_key,
1095                    "initial_data_stage" => gender_stage,
1096                    "stage" => "13",
1097                ])?;
1098                if !added_paths.contains(&p) { added_paths.push(p); }
1099
1100                // Generic shared (title)
1101                let career_key = format!("3k_main_ceo_career_historical_{n}");
1102                let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1103                    "initial_data_stage" => &stage1_key,
1104                    "active_ceo" => &career_key,
1105                    "starting_points_delta" => "0",
1106                    "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{career_key}")),
1107                ])?;
1108                if !added_paths.contains(&p) { added_paths.push(p); }
1109
1110                for (id_stage, stage_num) in &[
1111                    ("3k_main_initial_data_character_ancillaries_global", 2i32),
1112                    ("3k_dlc04_ceo_initial_data_character_give_political_support_random", 21i32),
1113                    ("3k_main_ceo_initial_data_stage_character_wealth_random", 15i32),
1114                    ("3k_main_ceo_initial_data_stage_character_traits_shared_global_permissions", 10i32),
1115                    ("3k_main_ceo_initial_data_stage_character_protagonist", 14i32),
1116                ] {
1117                    let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1118                        "ceo_initial_data" => &initial_data_key,
1119                        "initial_data_stage" => *id_stage,
1120                        "stage" => stage_num,
1121                    ])?;
1122                    if !added_paths.contains(&p) { added_paths.push(p); }
1123                }
1124                let p = insert_row(pack, schema, "ceo_initial_data_to_stages_tables", stem, &row![
1125                    "ceo_initial_data" => &initial_data_key,
1126                    "initial_data_stage" => &stage1_key,
1127                    "stage" => "11",
1128                ])?;
1129                if !added_paths.contains(&p) { added_paths.push(p); }
1130
1131                // ceo_thresholds
1132                let p = insert_row(pack, schema, "ceo_thresholds_tables", stem, &row![
1133                    "key" => &career_key,
1134                    "ceo" => &career_key,
1135                    "point_threshold_to_activate" => "1",
1136                    "point_theshold_to_destroy" => "0",
1137                    "starting_points" => "1",
1138                    "max_points" => "1",
1139                    "resets_to_starting_points_when_deactivated" => "false",
1140                ])?;
1141                if !added_paths.contains(&p) { added_paths.push(p); }
1142
1143                // ceo_nodes
1144                let p = insert_row(pack, schema, "ceo_nodes_tables", stem, &row![
1145                    "key" => &career_key,
1146                    "ceo_effect_list" => &effect_list_key,
1147                    "title" => "placeholder",
1148                    "description" => "placeholder",
1149                    "icon_path" => "",
1150                    "opinion_topic_modifier" => "",
1151                    "point_change_per_turn_if_active" => "0",
1152                ])?;
1153                if !added_paths.contains(&p) { added_paths.push(p); }
1154
1155                // ceo_threshold_nodes
1156                let p = insert_row(pack, schema, "ceo_threshold_nodes_tables", stem, &row![
1157                    "ceo_threshold" => &career_key,
1158                    "ceo_node" => &career_key,
1159                    "points_threshold_to_activate_node" => "1",
1160                    "can_downgrade_to_previous_node" => "false",
1161                    "auto_id" => auto_id("ceo_threshold_nodes", &format!("{career_key}|{career_key}")),
1162                ])?;
1163                if !added_paths.contains(&p) { added_paths.push(p); }
1164
1165                // ceo_effect_list_to_effects
1166                for (effect, val) in &[
1167                    ("3k_main_character_wealth", "2"),
1168                    ("3k_main_effect_character_num_lives", "1"),
1169                ] {
1170                    let p = insert_row(pack, schema, "ceo_effect_list_to_effects_tables", stem, &row![
1171                        "effect_list" => &effect_list_key,
1172                        "effect" => *effect,
1173                        "value" => *val,
1174                        "effect_scope" => "character_to_character_own",
1175                        "optional_only_in_game_mode" => "",
1176                        "auto_id" => auto_id("ceo_effect_list_to_effects", &format!("{effect_list_key}|{effect}")),
1177                    ])?;
1178                    if !added_paths.contains(&p) { added_paths.push(p); }
1179                }
1180
1181                // Traits
1182                for (trait_uuid, trait_key) in &entry.traits {
1183                    let p = insert_row(pack, schema, "ceo_initial_data_active_ceos_tables", stem, &row![
1184                        "initial_data_stage" => &stage1_key,
1185                        "active_ceo" => trait_key.as_str(),
1186                        "starting_points_delta" => "0",
1187                        "auto_id" => auto_id("ceo_initial_data_active_ceos", &format!("{stage1_key}|{trait_key}|{trait_uuid}")),
1188                    ])?;
1189                    if !added_paths.contains(&p) { added_paths.push(p); }
1190                }
1191
1192                // Loc entries for title
1193                loc_entries.push((format!("ceo_nodes_title_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
1194                loc_entries.push((format!("ceo_nodes_description_3k_main_ceo_career_historical_{n}"), "PLACEHOLDER".into()));
1195            }
1196        }
1197
1198        // ── Write loc file ──────────────────────────────────────
1199        if !loc_entries.is_empty() {
1200            let loc_pairs: Vec<(&str, &str)> = loc_entries.iter()
1201                .map(|(k, v)| (k.as_str(), v.as_str()))
1202                .collect();
1203            let p = insert_loc_entries(pack, &loc_path, &loc_pairs)?;
1204            if !added_paths.contains(&p) { added_paths.push(p); }
1205        }
1206
1207        Ok(added_paths)
1208}
1209
1210/// Fetch all trait CEOs from the Assembly Kit data, resolving display names via loc.
1211pub fn get_trait_ceos(deps: &rpfm_extensions::dependencies::Dependencies) -> Vec<(String, String)> {
1212    let ak_tables = deps.asskit_only_db_tables();
1213    let mut trait_ceos: Vec<(String, String)> = Vec::new();
1214
1215    let trait_categories: HashSet<&str> = [
1216        "3k_main_ceo_category_traits_personality",
1217        "3k_main_ceo_category_traits_physical",
1218    ].iter().copied().collect();
1219
1220    info!("GetTraitCeos: AK tables available: {}", ak_tables.len());
1221
1222    // Helper to read rows from an AK table by name, extracting two columns.
1223    fn ak_lookup_pairs(ak_tables: &HashMap<String, DB>, table_name: &str, col_a: &str, col_b: &str) -> Vec<(String, String)> {
1224        let mut result = Vec::new();
1225        if let Some(db) = ak_tables.get(table_name) {
1226            let fields = db.definition().fields_processed();
1227            let a_idx = fields.iter().position(|f| f.name() == col_a);
1228            let b_idx = fields.iter().position(|f| f.name() == col_b);
1229            if let (Some(ai), Some(bi)) = (a_idx, b_idx) {
1230                for row in db.data().iter() {
1231                    result.push((row[ai].data_to_string().to_string(), row[bi].data_to_string().to_string()));
1232                }
1233            }
1234        }
1235        result
1236    }
1237
1238    // Step 1: Get all CEO keys that belong to trait categories from AK.
1239    if let Some(ceos_db) = ak_tables.get("ceos_tables") {
1240        let fields = ceos_db.definition().fields_processed();
1241        let key_idx = fields.iter().position(|f| f.name() == "key");
1242        let cat_idx = fields.iter().position(|f| f.name() == "category");
1243
1244        if let (Some(ki), Some(ci)) = (key_idx, cat_idx) {
1245            for row in ceos_db.data().iter() {
1246                let category = row[ci].data_to_string();
1247                if trait_categories.contains(&*category) {
1248                    let ceo_key = row[ki].data_to_string().to_string();
1249                    trait_ceos.push((ceo_key, String::new()));
1250                }
1251            }
1252        }
1253    }
1254
1255    info!("GetTraitCeos: found {} trait CEOs from AK ceos_tables", trait_ceos.len());
1256
1257    // Step 2: Walk ceos -> ceo_thresholds -> ceo_threshold_nodes -> ceo_nodes for display names.
1258    let ceo_to_threshold: HashMap<String, String> =
1259        ak_lookup_pairs(ak_tables, "ceo_thresholds_tables", "ceo", "key")
1260            .into_iter().collect();
1261
1262    let threshold_to_node: HashMap<String, String> =
1263        ak_lookup_pairs(ak_tables, "ceo_threshold_nodes_tables", "ceo_threshold", "ceo_node")
1264            .into_iter().collect();
1265
1266    let node_to_title: HashMap<String, String> =
1267        ak_lookup_pairs(ak_tables, "ceo_nodes_tables", "key", "title")
1268            .into_iter().collect();
1269
1270    // Build loc lookup from dependencies (game files have the loc data).
1271    let mut loc_lookup: HashMap<String, String> = HashMap::new();
1272    if let Ok(loc_files) = deps.loc_data(true, true) {
1273        for rfile in &loc_files {
1274            if let Ok(RFileDecoded::Loc(loc)) = rfile.decoded() {
1275                for row in loc.data().iter() {
1276                    if row.len() >= 2 {
1277                        let loc_key = row[0].data_to_string().to_string();
1278                        let loc_val = row[1].data_to_string().to_string();
1279                        loc_lookup.insert(loc_key, loc_val);
1280                    }
1281                }
1282            }
1283        }
1284    }
1285
1286    info!("GetTraitCeos: loc_lookup has {} entries, node_to_title has {} entries", loc_lookup.len(), node_to_title.len());
1287
1288    // Step 3: Resolve display names.
1289    for (ceo_key, display_name) in &mut trait_ceos {
1290        // Chain: ceo_key -> threshold -> node -> title (loc key) -> loc text
1291        let resolved = ceo_to_threshold.get(ceo_key.as_str())
1292            .and_then(|thresh| threshold_to_node.get(thresh))
1293            .and_then(|node| {
1294                // Try the title field value as a direct loc key
1295                node_to_title.get(node).and_then(|title_key| {
1296                    loc_lookup.get(title_key)
1297                        .or_else(|| {
1298                            // Try constructing "ceo_nodes_title_{node_key}"
1299                            let constructed = format!("ceo_nodes_title_{}", node);
1300                            loc_lookup.get(&constructed)
1301                        })
1302                })
1303            });
1304
1305        if let Some(name) = resolved {
1306            *display_name = name.clone();
1307        } else {
1308            // Fallback: humanize the key
1309            *display_name = ceo_key
1310                .replace("3k_main_ceo_trait_", "")
1311                .replace("3k_dlc", "")
1312                .replace("3k_ytr_ceo_trait_", "")
1313                .replace('_', " ");
1314        }
1315    }
1316
1317    // Sort by display name for the UI.
1318    trait_ceos.sort_by(|a, b| a.1.cmp(&b.1));
1319
1320    info!("GetTraitCeos: returning {} traits", trait_ceos.len());
1321
1322    trait_ceos
1323}
1324
1325/// Import ceo_data.ccd into the pack after BOB has run.
1326pub fn build_ceo_post(pack: &mut Pack, akit_path: &str) -> Result<Vec<ContainerPath>> {
1327    let ceo_ccd_path = PathBuf::from(akit_path)
1328        .join(r"working_data\campaigns\ceo_data.ccd");
1329
1330    if !ceo_ccd_path.exists() {
1331        return Err(anyhow!("ceo_data.ccd not found. Make sure BOB ran successfully."));
1332    }
1333
1334    let raw_bytes = std::fs::read(&ceo_ccd_path)
1335        .map_err(|e| anyhow!("Failed to read ceo_data.ccd: {e}"))?;
1336
1337    let mut rfile = RFile::new_from_vec(&raw_bytes, FileType::Unknown, 0, "campaigns/ceo_data.ccd");
1338    let _ = rfile.guess_file_type();
1339    match pack.insert(rfile) {
1340        Ok(Some(path)) => Ok(vec![path]),
1341        Ok(None) => Ok(vec![]),
1342        Err(e) => Err(anyhow!("{}", e)),
1343    }
1344}