Skip to main content

rpfm_server/
background_thread.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Per-session command dispatcher — where every Pack, schema, search,
12//! diagnostics and dependency operation actually runs.
13//!
14//! Each [`Session`] spawns one task running [`background_loop`]. The loop
15//! pulls `(reply_sender, Command)` pairs off the session's mpsc channel,
16//! handles the command synchronously against the session's in-memory state
17//! (open packs, dependency cache, settings cache, schema), and ships every
18//! response back over the per-request `reply_sender`.
19//!
20//! Running commands serially per session is what keeps state consistent
21//! across many concurrent requests in the same session: a `SavePack`
22//! followed by a `ClosePack` always sees the right Pack, even when the
23//! WebSocket multiplexer is firing requests as fast as the client sends
24//! them.
25//!
26//! Telemetry: each dispatched command is recorded via
27//! [`rpfm_telemetry::record_action`] so usage counters reflect what the
28//! session actually did.
29
30use anyhow::{anyhow, Result};
31
32use itertools::Itertools;
33use open::that;
34use rayon::prelude::*;
35
36use std::collections::{BTreeMap, HashMap, HashSet};
37use std::env::temp_dir;
38use std::fs::{DirBuilder, File};
39use std::io::{BufWriter, Cursor, Write};
40use std::path::PathBuf;
41use std::slice::from_ref;
42use std::sync::{Arc, RwLock};
43use std::thread;
44use std::time::SystemTime;
45
46use rpfm_extensions::dependencies::*;
47use rpfm_extensions::diagnostics::Diagnostics;
48use rpfm_extensions::gltf::{gltf_from_rigid, save_gltf_to_disk};
49use rpfm_extensions::merge::{db_baseline, delta_merge_db, delta_merge_loc, loc_baseline, MergeConflict, MergeResolution};
50use rpfm_extensions::optimizer::OptimizableContainer;
51use rpfm_extensions::translator::PackTranslation;
52
53use rpfm_ipc::helpers::*;
54use rpfm_ipc::messages::OperationalMode;
55use rpfm_ipc::settings_keys::*;
56
57use rpfm_lib::compression::CompressionFormat;
58use rpfm_lib::files::{animpack::AnimPack, Container, ContainerPath, db::DB, DecodeableExtraData, EncodeableExtraData, FileType, loc::Loc, pack::*, portrait_settings::PortraitSettings, RFile, RFileDecoded, table::{DecodedData, Table}, text::*};
59use rpfm_lib::games::{GameInfo, LUA_REPO, LUA_BRANCH, LUA_REMOTE, OLD_AK_REPO, OLD_AK_BRANCH, OLD_AK_REMOTE, pfh_file_type::PFHFileType, supported_games::*, VanillaDBTableNameLogic};
60use rpfm_lib::games::{TRANSLATIONS_REPO, TRANSLATIONS_BRANCH, TRANSLATIONS_REMOTE};
61use rpfm_lib::integrations::{assembly_kit::*, git::*};
62use rpfm_lib::schema::*;
63use rpfm_lib::utils::*;
64
65use rpfm_telemetry::*;
66
67use crate::*;
68use crate::ceo_builder::{build_ceo_entries, build_ceo_post, get_trait_ceos};
69use crate::comms::CentralCommand;
70use crate::session::Session;
71use crate::settings::*;
72use crate::updater;
73
74use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
75
76/// Filename of the per-game vanilla English Loc TSV bundled in the
77/// [Total War Translation Hub][tlh] repo. The translator compares mod loc
78/// entries against this file to detect rows that match vanilla and can be
79/// auto-translated from the official localisation.
80///
81/// Lives under [`crate::settings::translations_remote_path`] once the Hub
82/// has been cloned locally.
83///
84/// [tlh]: https://github.com/Frodo45127/total_war_translation_hub
85pub const VANILLA_LOC_NAME: &str = "vanilla_english.tsv";
86
87/// Filename prefix for community-maintained vanilla loc fix TSVs in the
88/// [Total War Translation Hub][tlh] repo (e.g. `vanilla_fixes_es.tsv`).
89/// Each one carries fixes for vanilla loc bugs in a specific language;
90/// the suffix is the language code.
91///
92/// Discovered alongside [`VANILLA_LOC_NAME`] under
93/// [`crate::settings::translations_remote_path`].
94///
95/// [tlh]: https://github.com/Frodo45127/total_war_translation_hub
96pub const VANILLA_FIXES_NAME: &str = "vanilla_fixes_";
97
98/// Stem used to seed names for newly created Packs (`new_pack.pack`,
99/// `new_pack_2.pack`, …).
100const DEFAULT_PACK_STEM: &str = "new_pack";
101
102/// Extension appended to [`DEFAULT_PACK_STEM`] when materialising a new
103/// Pack's filename.
104const DEFAULT_PACK_EXT: &str = ".pack";
105
106/// Outcome of a delta merge attempt: either a finished file ready to insert, or the conflicts blocking it.
107enum DeltaMergeOutcome {
108    Merged(RFile),
109    Conflicts(Vec<MergeConflict>),
110}
111
112/// Extracts the variant name (e.g. `"NewPack"`) from a [`Command`] for telemetry.
113///
114/// Uses the `Debug` impl via a custom `fmt::Write` that captures only the leading
115/// identifier, so we don't pay the cost of formatting any inner data.
116fn command_name(cmd: &Command) -> String {
117    struct NameOnly {
118        out: String,
119        done: bool,
120    }
121
122    impl std::fmt::Write for NameOnly {
123        fn write_str(&mut self, s: &str) -> std::fmt::Result {
124            if self.done {
125                return Ok(());
126            }
127            for c in s.chars() {
128                if c.is_alphanumeric() || c == '_' {
129                    self.out.push(c);
130                } else {
131                    self.done = true;
132                    return Ok(());
133                }
134            }
135            Ok(())
136        }
137    }
138
139    let mut capture = NameOnly { out: String::new(), done: false };
140    let _ = std::fmt::write(&mut capture, format_args!("{:?}", cmd));
141    capture.out
142}
143
144/// Derives a unique pack name for new (unsaved) packs. Appends a numeric suffix (_2, _3, etc.)
145/// to the stem if the base name is already taken. Returns a name like "new_pack.pack", "new_pack_2.pack", etc.
146fn derive_new_pack_name(existing_keys: &BTreeMap<String, Pack>) -> String {
147    let base = format!("{}{}", DEFAULT_PACK_STEM, DEFAULT_PACK_EXT);
148    if !existing_keys.contains_key(&base) {
149        return base;
150    }
151
152    let mut suffix = 2;
153    loop {
154        let candidate = format!("{}_{}{}", DEFAULT_PACK_STEM, suffix, DEFAULT_PACK_EXT);
155        if !existing_keys.contains_key(&candidate) {
156            return candidate;
157        }
158        suffix += 1;
159    }
160}
161
162/// Converts a path to its string representation for use as a pack key.
163fn pack_key_from_path(path: &std::path::Path) -> String {
164    path.to_string_lossy().to_string()
165}
166
167/// Generate an unique pack key that does not conflict with any existing open packs.
168fn unique_pack_key(key: &str, packs: &BTreeMap<String, Pack>) -> String {
169    if !packs.contains_key(key) {
170        return key.to_string();
171    }
172
173    let path = std::path::Path::new(key);
174    let parent = path.parent().map(|parent| parent.to_path_buf()).unwrap_or_default();
175    let stem = path.file_stem().map(|stem| stem.to_string_lossy().to_string()).unwrap_or_else(|| key.to_string());
176    let ext = path.extension().map(|ext| ext.to_string_lossy().to_string());
177
178    let mut suffix = 2;
179    loop {
180        let candidate_name = match &ext {
181            Some(ext) => format!("{stem} ({suffix}).{ext}"),
182            None => format!("{stem} ({suffix})"),
183        };
184        let candidate = parent.join(candidate_name).to_string_lossy().to_string();
185        if !packs.contains_key(&candidate) {
186            return candidate;
187        }
188        suffix += 1;
189    }
190}
191
192/// Expand selected paths into file path entries for the clipboard.
193///
194/// Returns `(file_path, base_path, source_pack_key)` per file. Only paths are stored,
195/// not the file data itself — the actual `RFile` is cloned from the source pack at paste time.
196///
197/// - For a selected file `a/b/c`, the base path is `a/b` (parent folder), so pasting gives just `c`.
198/// - For a selected folder `a/b`, the base path is `a` (parent of folder), so pasting preserves `b/...`.
199fn clipboard_entries_from_paths(pack: &Pack, paths: &[ContainerPath], pack_key: &str) -> Vec<(String, String, String)> {
200    let mut result = Vec::new();
201    for path in paths {
202        let base_path = match path.path_raw().rfind('/') {
203            Some(pos) => path.path_raw()[..pos].to_string(),
204            None => String::new(),
205        };
206        for file in pack.files_by_paths(from_ref(path), false) {
207            result.push((file.path_in_container_raw().to_string(), base_path.clone(), pack_key.to_string()));
208        }
209    }
210    result
211}
212
213/// Looks up a pack by key. If not found, sends a "Pack not found" error and returns `None`.
214fn get_pack<'a>(packs: &'a BTreeMap<String, Pack>, pack_key: &str, sender: &UnboundedSender<Response>) -> Option<&'a Pack> {
215    match packs.get(pack_key) {
216        Some(pack) => Some(pack),
217        None => {
218            CentralCommand::send_back(sender, Response::Error(format!("Pack not found: {}", pack_key)));
219            None
220        }
221    }
222}
223
224/// The per-session command dispatcher.
225///
226/// Receives `(reply_sender, command)` pairs from the session's mpsc
227/// `receiver` and processes them serially against the session's
228/// in-memory state (open packs, dependency cache, schema, settings cache,
229/// per-pack [`OperationalMode`]). For each command, the matching handler
230/// computes the response (often several responses for multi-stage
231/// operations) and ships them back through `reply_sender`.
232///
233/// One instance runs per [`Session`], spawned by [`Session::new`]. The loop
234/// terminates when the session is dropped or [`Command::Exit`] is dispatched.
235///
236/// No UI or `unsafe` work happens here — everything is plain async Rust on
237/// top of `rpfm_lib` and `rpfm_extensions`.
238pub async fn background_loop(mut receiver: UnboundedReceiver<(UnboundedSender<Response>, Command)>, session: Arc<Session>) {
239
240    //---------------------------------------------------------------------------------------//
241    // Initializing stuff...
242    //---------------------------------------------------------------------------------------//
243
244    let supported_games = SupportedGames::default();
245    let mut game = supported_games.game(KEY_WARHAMMER_3).unwrap();
246    let mut schema = None;
247    let mut first_game_change_done = false;
248
249    // All open packs, keyed by their full file path (or a generated name for new/unsaved packs).
250    let mut packs: BTreeMap<String, Pack> = BTreeMap::new();
251
252    // Per-pack operational mode (Normal or MyMod). Keyed by the same pack key as `packs`.
253    let mut pack_modes: BTreeMap<String, OperationalMode> = BTreeMap::new();
254
255    // Internal clipboard for copy/cut/paste operations.
256    let mut clipboard_entries: Vec<(String, String, String)> = Vec::new(); // (file_path, base_path, source_pack_key) per entry.
257    let mut clipboard_is_cut: bool = false;
258
259    // Preload the default game's dependencies.
260    let mut dependencies = Arc::new(RwLock::new(Dependencies::default()));
261
262    // Load settings from disk or use defaults.
263    let _ = init_config_path();
264    let mut settings = Settings::init(false).unwrap_or_else(|error| {
265        rpfm_telemetry::warn!("Failed to initialize settings, falling back to defaults. Error: {error}");
266        Settings::default()
267    });
268    let mut backup_settings = settings.clone();
269
270    // Sync the telemetry toggles with this session's on-disk settings.
271    rpfm_telemetry::set_usage_telemetry_enabled(settings.bool(ENABLE_USAGE_TELEMETRY));
272    rpfm_telemetry::set_crash_reports_enabled(settings.bool(ENABLE_CRASH_REPORTS));
273
274    // Load all the tips we have.
275    //let mut tips = if let Ok(tips) = Tips::load() { tips } else { Tips::default() };
276
277    //---------------------------------------------------------------------------------------//
278    // Looping forever and ever...
279    //---------------------------------------------------------------------------------------//
280    info!("Background Thread looping around…");
281    'background_loop: while let Some((sender, response)) = receiver.recv().await {
282
283        // Record the action for telemetry, skipping lifecycle commands so we only
284        // measure real user-facing work. Counters are dropped silently when disabled.
285        match &response {
286            Command::Exit | Command::ClientDisconnecting => {}
287            cmd => rpfm_telemetry::record_action(&command_name(cmd)),
288        }
289
290        match response {
291
292            // Command to close the thread.
293            Command::Exit => break,
294
295            // ClientDisconnecting is handled at the WebSocket level in main.rs.
296            // If it reaches here, just acknowledge it (shouldn't normally happen).
297            Command::ClientDisconnecting => {
298                CentralCommand::send_back(&sender, Response::Success);
299            }
300
301            // When we want to check if there is an update available for RPFM...
302            Command::CheckUpdates => {
303                let sender = sender.clone();
304                let settings = settings.clone();
305                tokio::spawn(async move {
306                    let result = tokio::task::spawn_blocking(move || {
307                        updater::check_updates_rpfm(&settings)
308                    }).await.unwrap();
309
310                    match result {
311                        Ok(response) => CentralCommand::send_back(&sender, Response::APIResponse(response)),
312                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
313                    }
314                });
315            }
316
317            Command::CheckSchemaUpdates => {
318                git_update_check(sender, schemas_path, SCHEMA_REPO, SCHEMA_BRANCH, SCHEMA_REMOTE);
319            }
320
321            Command::CheckLuaAutogenUpdates => {
322                git_update_check(sender, lua_autogen_base_path, LUA_REPO, LUA_BRANCH, LUA_REMOTE);
323            }
324
325            Command::CheckEmpireAndNapoleonAKUpdates => {
326                git_update_check(sender, old_ak_files_path, OLD_AK_REPO, OLD_AK_BRANCH, OLD_AK_REMOTE);
327            }
328
329            Command::CheckTranslationsUpdates => {
330                git_update_check(sender, translations_remote_path, TRANSLATIONS_REPO, TRANSLATIONS_BRANCH, TRANSLATIONS_REMOTE);
331            }
332
333            // Close a specific pack by key.
334            Command::ClosePack(pack_key) => {
335                if packs.remove(&pack_key).is_some() {
336                    pack_modes.remove(&pack_key);
337                    session.remove_pack_name(&pack_key);
338                    CentralCommand::send_back(&sender, Response::Success);
339                } else {
340                    CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key)));
341                }
342            }
343
344            Command::CloseAllPacks => {
345                for pack_key in packs.keys().cloned().collect::<Vec<_>>() {
346                    session.remove_pack_name(&pack_key);
347                }
348                packs.clear();
349                pack_modes.clear();
350                CentralCommand::send_back(&sender, Response::Success);
351            }
352
353            // List all currently open packs.
354            Command::ListOpenPacks => {
355                let pack_list: Vec<(String, ContainerInfo)> = packs.iter()
356                    .map(|(key, pack)| (key.clone(), ContainerInfo::from(pack)))
357                    .collect();
358                CentralCommand::send_back(&sender, Response::VecStringContainerInfo(pack_list));
359            }
360
361            // Create a new empty PackFile and insert into the map.
362            Command::NewPack => {
363                let pack_version = game.pfh_version_by_file_type(PFHFileType::Mod);
364                let key = derive_new_pack_name(&packs);
365                let mut pack = Pack::new_with_name_and_version(&key, pack_version);
366
367                if let Some(version_number) = game.game_version_number(&settings.path_buf(game.key())) {
368                    pack.set_game_version(version_number);
369                }
370                session.add_pack_name(&key);
371                packs.insert(key.clone(), pack);
372                pack_modes.insert(key.clone(), OperationalMode::Normal);
373                CentralCommand::send_back(&sender, Response::String(key));
374            }
375
376            // Open one or more PackFiles, merge them, and insert into the map.
377            Command::OpenPackFiles(paths) => {
378                let key = if let Some(first_path) = paths.first() {
379                    pack_key_from_path(first_path)
380                } else {
381                    format!("{}{}", DEFAULT_PACK_STEM, DEFAULT_PACK_EXT)
382                };
383
384                let already_open = paths.first().is_some_and(|first_path| {
385                    let normalized = first_path.to_string_lossy().replace('\\', "/");
386                    packs.values().any(|pack| pack.disk_file_path() == normalized.as_str())
387                });
388
389                if already_open {
390                    CentralCommand::send_back(&sender, Response::Error(format!(
391                        "Pack '{}' is already open. Close it first if you want to reopen it.", key
392                    )));
393                } else {
394                    match Pack::read_and_merge(&paths, game, settings.bool("use_lazy_loading"), false, false) {
395                        Ok(mut pack) => {
396
397                            // Force decoding of table/locs, so they're in memory for the diagnostics to work.
398                            if let Some(ref schema) = schema {
399                                let mut decode_extra_data = DecodeableExtraData::default();
400                                decode_extra_data.set_schema(Some(schema));
401                                let extra_data = Some(decode_extra_data);
402
403                                let mut files = pack.files_by_type_mut(&[FileType::DB, FileType::Loc]);
404                                files.par_iter_mut().for_each(|file| {
405                                    let _ = file.decode(&extra_data, true, false);
406                                });
407                            }
408
409                            let key = unique_pack_key(&key, &packs);
410                            session.add_pack_name(&key);
411
412                            let info = ContainerInfo::from(&pack);
413                            packs.insert(key.clone(), pack);
414                            pack_modes.insert(key.clone(), OperationalMode::Normal);
415                            CentralCommand::send_back(&sender, Response::StringContainerInfo(key, info));
416                        }
417                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
418                    }
419                }
420            }
421
422            // Load All CA PackFiles and insert into the map.
423            Command::LoadAllCAPackFiles => {
424                let key = "CA PackFiles".to_string();
425
426                if packs.contains_key(&key) {
427                    CentralCommand::send_back(&sender, Response::Error(format!(
428                        "Pack '{}' is already open. Close it first if you want to reopen it.", key
429                    )));
430                } else {
431                    match Pack::read_and_merge_ca_packs(game, &settings.path_buf(game.key())) {
432                        Ok(mut pack) => {
433
434                            // Force decoding of table/locs, so they're in memory for the diagnostics to work.
435                            if let Some(ref schema) = schema {
436                                let mut decode_extra_data = DecodeableExtraData::default();
437                                decode_extra_data.set_schema(Some(schema));
438                                let extra_data = Some(decode_extra_data);
439
440                                let mut files = pack.files_by_type_mut(&[FileType::DB, FileType::Loc]);
441                                files.par_iter_mut().for_each(|file| {
442                                    let _ = file.decode(&extra_data, true, false);
443                                });
444                            }
445
446                            session.add_pack_name(&key);
447
448                            let info = ContainerInfo::from(&pack);
449                            packs.insert(key.clone(), pack);
450                            pack_modes.insert(key.clone(), OperationalMode::Normal);
451                            CentralCommand::send_back(&sender, Response::StringContainerInfo(key, info));
452                        }
453                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
454                    }
455                }
456            }
457
458            // Save a specific pack to disk.
459            Command::SavePack(pack_key) => {
460                match packs.get_mut(&pack_key) {
461                    Some(pack) => {
462                        let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, pack.compression_format(), settings.bool("disable_uuid_regeneration_on_db_tables")));
463
464                        let pack_type = *pack.header().pfh_file_type();
465                        if !settings.bool("allow_editing_of_ca_packfiles") && pack_type != PFHFileType::Mod && pack_type != PFHFileType::Movie {
466                            CentralCommand::send_back(&sender, Response::Error(anyhow!("Pack cannot be saved due to being of CA-Only type. Either change the Pack Type or enable \"Allow Edition of CA Packs\" in the settings.").to_string()));
467                            continue;
468                        }
469
470                        match pack.save(None, game, &extra_data) {
471                            Ok(_) => CentralCommand::send_back(&sender, Response::ContainerInfo(From::from(&*pack))),
472                            Err(error) => CentralCommand::send_back(&sender, Response::Error(anyhow!("Error while trying to save the currently open PackFile: {}", error).to_string())),
473                        }
474                    }
475                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
476                }
477            }
478
479            // Save a specific pack to a new path.
480            Command::SavePackAs(pack_key, path) => {
481                match packs.get_mut(&pack_key) {
482                    Some(pack) => {
483                        let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, pack.compression_format(), settings.bool("disable_uuid_regeneration_on_db_tables")));
484
485                        let pack_type = *pack.header().pfh_file_type();
486                        if !settings.bool("allow_editing_of_ca_packfiles") && pack_type != PFHFileType::Mod && pack_type != PFHFileType::Movie {
487                            CentralCommand::send_back(&sender, Response::Error(anyhow!("Pack cannot be saved due to being of CA-Only type. Either change the Pack Type or enable \"Allow Edition of CA Packs\" in the settings.").to_string()));
488                            continue;
489                        }
490
491                        match pack.save(Some(&path), game, &extra_data) {
492                            Ok(_) => CentralCommand::send_back(&sender, Response::ContainerInfo(From::from(&*pack))),
493                            Err(error) => CentralCommand::send_back(&sender, Response::Error(anyhow!("Error while trying to save the currently open PackFile: {}", error).to_string())),
494                        }
495                    }
496                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
497                }
498            }
499
500            // Clean and save a specific pack to a path.
501            Command::CleanAndSavePackAs(pack_key, path) => {
502                match packs.get_mut(&pack_key) {
503                    Some(pack) => {
504                        pack.clean_undecoded();
505
506                        let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, pack.compression_format(), settings.bool("disable_uuid_regeneration_on_db_tables")));
507                        match pack.save(Some(&path), game, &extra_data) {
508                            Ok(_) => CentralCommand::send_back(&sender, Response::ContainerInfo(From::from(&*pack))),
509                            Err(error) => CentralCommand::send_back(&sender, Response::Error(anyhow!("Error while trying to save the currently open PackFile: {}", error).to_string())),
510                        }
511                    }
512                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
513                }
514            }
515
516            // Get the data of a specific pack needed to form the TreeView.
517            Command::GetPackFileDataForTreeView(pack_key) => {
518                match packs.get(&pack_key) {
519                    Some(pack) => {
520                        CentralCommand::send_back(&sender, Response::ContainerInfoVecRFileInfo((
521                            From::from(pack),
522                            pack.files().par_iter().map(|(_, file)| From::from(file)).collect(),
523                        )));
524                    }
525                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
526                }
527            }
528
529            // Get the info of one PackedFile from a specific pack.
530            Command::GetRFileInfo(pack_key, path) => {
531                match packs.get(&pack_key) {
532                    Some(pack) => {
533                        CentralCommand::send_back(&sender, Response::OptionRFileInfo(
534                            pack.files().get(&path).map(From::from)
535                        ));
536                    }
537                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
538                }
539            }
540
541            // Get the info of more than one PackedFiles from a specific pack.
542            Command::GetPackedFilesInfo(pack_key, paths) => {
543                match packs.get(&pack_key) {
544                    Some(pack) => {
545                        let paths = paths.iter().map(|path| ContainerPath::File(path.to_owned())).collect::<Vec<_>>();
546                        CentralCommand::send_back(&sender, Response::VecRFileInfo(
547                            pack.files_by_paths(&paths, false).into_iter().map(From::from).collect()
548                        ));
549                    }
550                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
551                }
552            }
553
554            // In case we want to launch a global search on a `PackFile`...
555            Command::GlobalSearch(_pack_key, mut global_search) => {
556                match schema {
557                    Some(ref schema) => {
558                        global_search.search(game, schema, &mut packs, &mut dependencies.write().unwrap(), &[]);
559                        let packed_files_info = RFileInfo::info_from_global_search(&global_search, &packs);
560                        CentralCommand::send_back(&sender, Response::GlobalSearchVecRFileInfo(Box::new(global_search), packed_files_info));
561                    }
562                    None => CentralCommand::send_back(&sender, Response::Error(anyhow!("Schema not found. Maybe you need to download it?").to_string())),
563                }
564            }
565
566            Command::GetGameSelected => CentralCommand::send_back(&sender, Response::String(game.key().to_owned())),
567            Command::SetGameSelected(game_key, rebuild_dependencies) => {
568                info!("Setting game selected.");
569                let game_changed = game.key() != game_key || !first_game_change_done;
570                game = match supported_games.game(&game_key) {
571                    Some(gi) => gi,
572                    None => {
573                        CentralCommand::send_back(&sender, Response::Error(anyhow!("The selected game is not supported!").to_string()));
574                        continue;
575                    }
576                };
577
578                // We need to make sure the compression format is valid for our game for all open packs.
579                for pack in packs.values_mut() {
580                    let current_cf = pack.compression_format();
581                    if current_cf != CompressionFormat::None && !game.compression_formats_supported().contains(&current_cf) {
582                        if let Some(new_cf) = game.compression_formats_supported().first() {
583                            pack.set_compression_format(*new_cf, game);
584                        } else {
585                            pack.set_compression_format(CompressionFormat::None, game);
586                        }
587                    }
588                }
589
590                // Optimization: If we know we need to rebuild the whole dependencies, load them in another thread
591                // while we load the schema. That way we can speed-up the entire game-switching process.
592                //
593                // While this is fast, the rust compiler doesn't like the fact that we're moving out the dependencies,
594                // then moving them back in an if, so we need two branches of code, depending on if rebuild is true or not.
595                //
596                // Branch 1: dependencies rebuilt.
597                // Load the new schema and re-decode tables in all open packs.
598                load_schema(&mut schema, &mut packs, game, &settings);
599
600                if rebuild_dependencies {
601                    info!("Branch 1.");
602                    // Collect dependencies from all open packs.
603                    let pack_dependencies: Vec<_> = packs.values()
604                        .flat_map(|pack| pack.dependencies().iter().map(|x| x.1.clone()))
605                        .collect();
606                    // Get settings values before spawning thread since settings can't be moved into closure
607                    let game_path = settings.path_buf(game.key());
608                    let secondary_path = settings.path_buf(SECONDARY_PATH);
609                    let game_clone = game.clone();
610                    let handle = thread::spawn(move || {
611                        let file_path = dependencies_cache_path().unwrap().join(game_clone.dependencies_cache_file_name());
612                        let file_path = if game_changed { Some(&*file_path) } else { None };
613                        let _ = dependencies.write().unwrap().rebuild(&None, &pack_dependencies, file_path, &game_clone, &game_path, &secondary_path);
614                        dependencies
615                    });
616
617                    // Get the dependencies that were loading in parallel and send their info to the UI.
618                    dependencies = handle.join().unwrap();
619                    let dependencies_info = DependenciesInfo::new(&dependencies.read().unwrap(), game.vanilla_db_table_name_logic());
620                    info!("Sending dependencies info after game selected change.");
621                    // Use compression format from the first pack, or None if no packs open.
622                    let cf = packs.values().next().map(|p| p.compression_format()).unwrap_or(CompressionFormat::None);
623                    CentralCommand::send_back(&sender, Response::CompressionFormatDependenciesInfo(cf, Some(dependencies_info)));
624
625                    // Decode the dependencies tables while the UI does its own thing.
626                    dependencies.write().unwrap().decode_tables(&schema);
627                }
628
629                // Branch 2: no dependencies rebuild.
630                else {
631                    info!("Branch 2.");
632                    let cf = packs.values().next().map(|p| p.compression_format()).unwrap_or(CompressionFormat::None);
633                    CentralCommand::send_back(&sender, Response::CompressionFormatDependenciesInfo(cf, None));
634                };
635
636                // For all open packs, change their id to match the one of the new `Game Selected`.
637                for pack in packs.values_mut() {
638                    if !pack.disk_file_path().is_empty() {
639                        let pfh_file_type = *pack.header().pfh_file_type();
640                        pack.header_mut().set_pfh_version(game.pfh_version_by_file_type(pfh_file_type));
641
642                        if let Some(version_number) = game.game_version_number(&settings.path_buf(game.key())) {
643                            pack.set_game_version(version_number);
644                        }
645                    }
646                }
647
648                if !first_game_change_done {
649                    first_game_change_done = true;
650                }
651
652                info!("Switching game selected done.");
653            }
654
655            // In case we want to generate the dependencies cache for our Game Selected...
656            Command::GenerateDependenciesCache => {
657                let game_path = settings.path_buf(game.key());
658                let ignore_game_files_in_ak = settings.bool("ignore_game_files_in_ak");
659                let asskit_path = settings.assembly_kit_path(game).ok();
660
661                if game_path.is_dir() {
662                    match Dependencies::generate_dependencies_cache(&schema, game, &game_path, &asskit_path, ignore_game_files_in_ak) {
663                        Ok(mut cache) => {
664                            let dependencies_path = dependencies_cache_path().unwrap().join(game.dependencies_cache_file_name());
665                            match cache.save(&dependencies_path) {
666                                Ok(_) => {
667                                    let secondary_path = settings.path_buf(SECONDARY_PATH);
668                                    let pack_dependencies: Vec<_> = packs.values()
669                                        .flat_map(|pack| pack.dependencies().iter().map(|x| x.1.clone()))
670                                        .collect();
671                                    let _ = dependencies.write().unwrap().rebuild(&schema, &pack_dependencies, Some(&dependencies_path), game, &game_path, &secondary_path);
672                                    let dependencies_info = DependenciesInfo::new(&dependencies.read().unwrap(), game.vanilla_db_table_name_logic());
673                                    CentralCommand::send_back(&sender, Response::DependenciesInfo(dependencies_info));
674                                },
675                                Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
676                            }
677                        }
678                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
679                    }
680                } else {
681                    CentralCommand::send_back(&sender, Response::Error(anyhow!("Game Path not configured. Go to <i>'PackFile/Settings'</i> and configure it.").to_string()));
682                }
683            }
684
685            // In case we want to update the Schema for our Game Selected...
686            Command::UpdateCurrentSchemaFromAssKit => {
687                let ignore_game_files_in_ak = settings.bool("ignore_game_files_in_ak");
688
689                if let Some(ref mut schema) = schema {
690                    match settings.assembly_kit_path(game) {
691                        Ok(asskit_path) => {
692                            let schema_path = schemas_path().unwrap().join(game.schema_file_name());
693
694                            let dependencies = dependencies.read().unwrap();
695                            if let Ok(mut tables_to_check) = dependencies.db_and_loc_data(true, false, true, false) {
696
697                                // If there are packs open, also add the packs' tables to it. That way we can treat some special tables, like starpos tables.
698                                for pack in packs.values() {
699                                    if !pack.disk_file_path().is_empty() {
700                                        tables_to_check.append(&mut pack.files_by_type(&[FileType::DB]));
701                                    }
702                                }
703
704                                // Split the tables to check by table name.
705                                let mut tables_to_check_split: HashMap<String, Vec<DB>> = HashMap::new();
706                                for table_to_check in tables_to_check {
707                                    if let Ok(RFileDecoded::DB(table)) = table_to_check.decoded() {
708                                        match tables_to_check_split.get_mut(table.table_name()) {
709                                            Some(tables) => {
710
711                                                // Merge tables of the same name and version, so we got more chances of loc data being found.
712                                                match tables.iter_mut().find(|x| x.definition().version() == table.definition().version()) {
713                                                    Some(db_source) => *db_source = DB::merge(&[db_source, table]).unwrap(),
714                                                    None => tables.push((table.clone()).clone()),
715                                                }
716                                            }
717                                            None => {
718                                                tables_to_check_split.insert(table.table_name().to_owned(), vec![table.clone()]);
719                                            }
720                                        }
721                                    }
722                                }
723
724                                let tables_to_skip = if ignore_game_files_in_ak {
725                                    dependencies.vanilla_loose_tables().keys().chain(dependencies.vanilla_tables().keys()).map(|x| &**x).collect::<Vec<_>>()
726                                } else {
727                                    vec![]
728                                };
729
730                                match update_schema_from_raw_files(schema, game, &asskit_path, &schema_path, &tables_to_skip, &tables_to_check_split) {
731                                    Ok(possible_loc_fields) => {
732
733                                        // NOTE: This deletes all loc fields first, so we need to get the loc fields AGAIN after this from the TExc_LocalisableFields.xml, if said file exists and it's readable.
734                                        // That's why it does the update again, to re-populate the loc fields list with the ones not bruteforced. It's ineficient, but gets the job done.
735                                        // Use the open packs for bruteforce, or None if no packs open.
736                                        let local_packs = if packs.is_empty() { None } else { Some(&packs) };
737                                        if dependencies.bruteforce_loc_key_order(schema, possible_loc_fields, local_packs, None).is_ok() {
738
739                                            // Note: this shows the list of "missing" fields.
740                                            let _ = update_schema_from_raw_files(schema, game, &asskit_path, &schema_path, &tables_to_skip, &tables_to_check_split);
741
742                                            // This generates the automatic patches in the schema (like ".png are files" kinda patches).
743                                            if dependencies.generate_automatic_patches(schema, &packs).is_ok() {
744
745                                                // Fix for old file relative paths using incorrect separators.
746                                                schema.definitions_mut().par_iter_mut().for_each(|x| {
747                                                    x.1.iter_mut().for_each(|y| {
748                                                        y.fields_mut().iter_mut().for_each(|z| {
749                                                            if let Some(path) = z.filename_relative_path(None) {
750                                                                if path.len() == 1 && path[0].contains(",") {
751                                                                    let new_paths = path[0].split(',').map(|x| x.trim()).join(";");
752                                                                    z.set_filename_relative_path(Some(new_paths));
753                                                                }
754                                                            }
755                                                        });
756                                                    });
757                                                });
758
759                                                match schema.save(&schemas_path().unwrap().join(game.schema_file_name())) {
760                                                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
761                                                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
762                                                }
763                                            } else {
764                                                CentralCommand::send_back(&sender, Response::Success)
765                                            }
766                                        } else {
767                                            CentralCommand::send_back(&sender, Response::Success)
768                                        }
769                                    },
770                                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
771                                }
772                            }
773                        }
774                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
775                    }
776                } else {
777                    CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string()));
778                }
779            }
780
781            // In case we want to optimize our PackFile...
782            Command::OptimizePackFile(pack_key, options) => {
783                match packs.get_mut(&pack_key) {
784                    Some(pack) => {
785                        if let Some(ref schema) = schema {
786                            match pack.optimize(None, &mut dependencies.write().unwrap(), schema, game, &options) {
787                                Ok((paths_to_delete, paths_to_add)) => CentralCommand::send_back(&sender, Response::HashSetStringHashSetString(paths_to_delete, paths_to_add)),
788                                Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
789                            }
790                        } else {
791                            CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string()));
792                        }
793                    }
794                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
795                }
796            }
797
798            // In case we want to Patch the SiegeAI of a PackFile...
799            Command::PatchSiegeAI(pack_key) => {
800                match packs.get_mut(&pack_key) {
801                    Some(pack) => {
802                        match pack.patch_siege_ai() {
803                            Ok(result) => CentralCommand::send_back(&sender, Response::StringVecContainerPath(result.0, result.1)),
804                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string()))
805                        }
806                    }
807                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
808                }
809            }
810
811            // In case we want to change the PackFile's Type...
812            Command::SetPackFileType(pack_key, new_type) => {
813                match packs.get_mut(&pack_key) {
814                    Some(pack) => {
815                        pack.set_pfh_file_type(new_type);
816                        CentralCommand::send_back(&sender, Response::Success);
817                    }
818                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
819                }
820            }
821
822            // In case we want to change the "Include Last Modified Date" setting of the PackFile...
823            Command::ChangeIndexIncludesTimestamp(pack_key, state) => {
824                match packs.get_mut(&pack_key) {
825                    Some(pack) => {
826                        let mut bitmask = pack.bitmask();
827                        bitmask.set(PFHFlags::HAS_INDEX_WITH_TIMESTAMPS, state);
828                        pack.set_bitmask(bitmask);
829                        CentralCommand::send_back(&sender, Response::Success);
830                    }
831                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
832                }
833            },
834
835            // In case we want to compress/decompress the PackedFiles of the currently open PackFile...
836            Command::ChangeCompressionFormat(pack_key, cf) => {
837                match packs.get_mut(&pack_key) {
838                    Some(pack) => {
839                        CentralCommand::send_back(&sender, Response::CompressionFormat(pack.set_compression_format(cf, game)));
840                    }
841                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
842                }
843            },
844
845            // In case we want to get the path of the currently open `PackFile`.
846            Command::GetPackFilePath(pack_key) => {
847                match packs.get(&pack_key) {
848                    Some(pack) => CentralCommand::send_back(&sender, Response::PathBuf(PathBuf::from(pack.disk_file_path()))),
849                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
850                }
851            },
852
853            // In case we want to get the Dependency PackFiles of our PackFile...
854            Command::GetDependencyPackFilesList(pack_key) => {
855                match packs.get(&pack_key) {
856                    Some(pack) => CentralCommand::send_back(&sender, Response::VecBoolString(pack.dependencies().to_vec())),
857                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
858                }
859            },
860
861            // In case we want to set the Dependency PackFiles of our PackFile...
862            Command::SetDependencyPackFilesList(pack_key, dep_packs) => {
863                match packs.get_mut(&pack_key) {
864                    Some(pack) => {
865                        pack.set_dependencies(dep_packs);
866                        CentralCommand::send_back(&sender, Response::Success);
867                    }
868                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
869                }
870            },
871
872            // In case we want to check if there is a Dependency Database loaded...
873            Command::IsThereADependencyDatabase(include_asskit) => {
874                let are_dependencies_loaded = dependencies.read().unwrap().is_vanilla_data_loaded(include_asskit);
875                CentralCommand::send_back(&sender, Response::Bool(are_dependencies_loaded))
876            },
877
878            // In case we want to create a PackedFile from scratch...
879            Command::NewPackedFile(pack_key, path, new_packed_file) => {
880                let decoded = match new_packed_file {
881                    NewFile::AnimPack(_) => {
882                        let file = AnimPack::default();
883                        RFileDecoded::AnimPack(file)
884                    },
885                    NewFile::DB(_, table, version) => {
886                        if let Some(ref schema) = schema {
887                            match schema.definition_by_name_and_version(&table, version) {
888                                Some(definition) => {
889                                    let patches = schema.patches_for_table(&table);
890                                    let file = DB::new(definition, patches, &table);
891                                    RFileDecoded::DB(file)
892                                }
893                                None => {
894                                    CentralCommand::send_back(&sender, Response::Error(format!("No definitions found for the table `{}`, version `{}` in the currently loaded schema.", table, version)));
895                                    continue;
896                                }
897                            }
898                        } else {
899                            CentralCommand::send_back(&sender, Response::Error("There is no Schema for the Game Selected.".to_string()));
900                            continue;
901                        }
902                    },
903                    NewFile::Loc(_) => {
904                        let file = Loc::new();
905                        RFileDecoded::Loc(file)
906                    }
907                    NewFile::PortraitSettings(_, version, entries) => {
908                        let mut file = PortraitSettings::default();
909                        file.set_version(version);
910
911                        if !entries.is_empty() {
912
913                            let mut dependencies = dependencies.write().unwrap();
914                            let mut vanilla_files = dependencies.files_by_types_mut(&[FileType::PortraitSettings], true, true);
915                            let vanilla_files_decoded = vanilla_files.iter_mut()
916                                .filter_map(|(_, file)| file.decode(&None, false, true).ok().flatten())
917                                .filter_map(|file| if let RFileDecoded::PortraitSettings(file) = file { Some(file) } else { None })
918                                .collect::<Vec<_>>();
919
920                            let vanilla_values = vanilla_files_decoded.iter()
921                                .flat_map(|file| file.entries())
922                                .map(|entry| (entry.id(), entry))
923                                .collect::<HashMap<_,_>>();
924
925                            for (from_id, to_id) in entries {
926                                if let Some(from_entry) = vanilla_values.get(&from_id) {
927                                    let mut new_entry = (*from_entry).clone();
928                                    new_entry.set_id(to_id);
929                                    file.entries_mut().push(new_entry);
930                                }
931                            }
932                        }
933
934                        RFileDecoded::PortraitSettings(file)
935                    },
936                    NewFile::Text(_, text_type) => {
937                        let mut file = Text::default();
938                        file.set_format(text_type);
939                        RFileDecoded::Text(file)
940                    },
941
942                    NewFile::VMD(_) => {
943                        let mut file = Text::default();
944                        file.set_format(TextFormat::Xml);
945                        RFileDecoded::VMD(file)
946                    },
947
948                    NewFile::WSModel(_) => {
949                        let mut file = Text::default();
950                        file.set_format(TextFormat::Xml);
951                        RFileDecoded::WSModel(file)
952                    },
953                };
954                let file = RFile::new_from_decoded(&decoded, 0, &path);
955                match packs.get_mut(&pack_key) {
956                    Some(pack) => {
957                        match pack.insert(file) {
958                            Ok(_) => CentralCommand::send_back(&sender, Response::Success),
959                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
960                        }
961                    }
962                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
963                }
964            }
965
966            // When we want to add one or more PackedFiles to our PackFile.
967            Command::AddPackedFiles(pack_key, source_paths, destination_paths, paths_to_ignore) => {
968                match packs.get_mut(&pack_key) {
969                    Some(pack) => {
970                        let mut added_paths = vec![];
971                        let mut it_broke = None;
972
973                        let paths = source_paths.iter().zip(destination_paths.iter()).collect::<Vec<(&PathBuf, &ContainerPath)>>();
974                        for (source_path, destination_path) in paths {
975
976                            // Skip ignored paths.
977                            if let Some(ref paths_to_ignore) = paths_to_ignore {
978                                if paths_to_ignore.iter().any(|x| source_path.starts_with(x)) {
979                                    continue;
980                                }
981                            }
982
983                            match destination_path {
984                                ContainerPath::File(destination_path) => {
985                                    match pack.insert_file(source_path, destination_path, &schema) {
986                                        Ok(path) => if let Some(path) = path {
987                                            added_paths.push(path);
988                                        },
989                                        Err(error) => it_broke = Some(error),
990                                    }
991                                },
992
993                                // TODO: See what should we do with the ignored paths.
994                                ContainerPath::Folder(destination_path) => {
995                                    match pack.insert_folder(source_path, destination_path, &None, &schema, settings.bool("include_base_folder_on_add_from_folder")) {
996                                        Ok(mut paths) => added_paths.append(&mut paths),
997                                        Err(error) => it_broke = Some(error),
998                                    }
999                                },
1000                            }
1001                        }
1002
1003                        CentralCommand::send_back(&sender, Response::VecContainerPathOptionString(added_paths.to_vec(), it_broke.map(|e| e.to_string())));
1004
1005                        // Force decoding of table/locs, so they're in memory for the diagnostics to work.
1006                        if let Some(ref schema) = schema {
1007                            let mut decode_extra_data = DecodeableExtraData::default();
1008                            decode_extra_data.set_schema(Some(schema));
1009                            let extra_data = Some(decode_extra_data);
1010
1011                            let mut files = pack.files_by_paths_mut(&added_paths, false);
1012                            files.par_iter_mut()
1013                                .filter(|file| file.file_type() == FileType::DB || file.file_type() == FileType::Loc)
1014                                .for_each(|file| {
1015                                    let _ = file.decode(&extra_data, true, false);
1016                                }
1017                            );
1018                        }
1019                    }
1020                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1021                }
1022            }
1023
1024            // In case we want to move stuff from one PackFile to another...
1025            Command::AddPackedFilesFromPackFile(target_key, source_key, paths) => {
1026                // First, clone files from the source pack.
1027                let files = match packs.get(&source_key) {
1028                    Some(source_pack) => {
1029                        source_pack.files_by_paths(&paths, false)
1030                            .into_iter()
1031                            .map(|file| {
1032                                let mut file = file.clone();
1033                                let _ = file.load();
1034                                file
1035                            })
1036                            .collect::<Vec<RFile>>()
1037                    }
1038                    None => {
1039                        CentralCommand::send_back(&sender, Response::Error(format!("Source pack not found: {}", source_key)));
1040                        continue;
1041                    }
1042                };
1043
1044                // Then, insert the cloned files into the target pack.
1045                match packs.get_mut(&target_key) {
1046                    Some(target_pack) => {
1047                        let mut added_paths = Vec::with_capacity(files.len());
1048                        for file in files {
1049                            if let Ok(Some(path)) = target_pack.insert(file) {
1050                                added_paths.push(path);
1051                            }
1052                        }
1053
1054                        CentralCommand::send_back(&sender, Response::VecContainerPath(added_paths.to_vec()));
1055
1056                        // Force decoding of table/locs, so they're in memory for the diagnostics to work.
1057                        if let Some(ref schema) = schema {
1058                            let mut decode_extra_data = DecodeableExtraData::default();
1059                            decode_extra_data.set_schema(Some(schema));
1060                            let extra_data = Some(decode_extra_data);
1061
1062                            let mut files = target_pack.files_by_paths_mut(&added_paths, false);
1063                            files.par_iter_mut()
1064                                .filter(|file| file.file_type() == FileType::DB || file.file_type() == FileType::Loc)
1065                                .for_each(|file| {
1066                                    let _ = file.decode(&extra_data, true, false);
1067                                }
1068                            );
1069                        }
1070                    }
1071                    None => CentralCommand::send_back(&sender, Response::Error(format!("Target pack not found: {}", target_key))),
1072                }
1073            }
1074
1075            // In case we want to move stuff from our PackFile to an Animpack...
1076            Command::AddPackedFilesFromPackFileToAnimpack(source_pack_key, anim_pack_key, anim_pack_path, paths) => {
1077                let files = match packs.get(&source_pack_key) {
1078                    Some(pack) => pack.files_by_paths(&paths, false)
1079                        .into_iter()
1080                        .map(|file| {
1081                            let mut file = file.clone();
1082                            let _ = file.load();
1083                            file
1084                        })
1085                        .collect::<Vec<RFile>>(),
1086                    None => {
1087                        CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", source_pack_key)));
1088                        continue;
1089                    }
1090                };
1091
1092                match packs.get_mut(&anim_pack_key) {
1093                    Some(pack) => {
1094                match pack.files_mut().get_mut(&anim_pack_path) {
1095                    Some(file) => {
1096
1097                        // Try to decode it using lazy_load if enabled.
1098                        let extra_data = DecodeableExtraData::default();
1099                        //extra_data.set_lazy_load(SETTINGS.read().unwrap().bool("use_lazy_loading"));
1100                        let _ = file.decode(&Some(extra_data), true, false);
1101
1102                        match file.decoded_mut() {
1103                            Ok(decoded) => match decoded {
1104                                RFileDecoded::AnimPack(anim_pack) => {
1105                                    let mut paths = Vec::with_capacity(files.len());
1106                                    for file in files {
1107                                        if let Ok(Some(path)) = anim_pack.insert(file) {
1108                                            paths.push(path);
1109                                        }
1110                                    }
1111
1112                                    CentralCommand::send_back(&sender, Response::VecContainerPath(paths.to_vec()));
1113                                }
1114                                _ => CentralCommand::send_back(&sender, Response::Error(format!("We expected {} to be of type {} but found {}. This is either a bug or you did weird things with the game selected.", anim_pack_path, FileType::AnimPack, FileType::from(&*decoded)))),
1115                            }
1116                            _ => CentralCommand::send_back(&sender, Response::Error(format!("Failed to decode the file at the following path: {}", anim_pack_path))),
1117                        }
1118                    }
1119                    None => CentralCommand::send_back(&sender, Response::Error(format!("File not found in the Pack: {}.", anim_pack_path))),
1120                }
1121                    }
1122                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", anim_pack_key))),
1123                }
1124            }
1125
1126            // In case we want to move stuff from an Animpack to our PackFile...
1127            Command::AddPackedFilesFromAnimpack(anim_pack_key, dest_pack_key, data_source, anim_pack_path, paths) => {
1128                let mut dependencies = dependencies.write().unwrap();
1129                let anim_pack_file = match data_source {
1130                    DataSource::PackFile => packs.get_mut(&anim_pack_key).and_then(|pack| pack.files_mut().get_mut(&anim_pack_path)),
1131                    DataSource::GameFiles => dependencies.file_mut(&anim_pack_path, true, false).ok(),
1132                    DataSource::ParentFiles => dependencies.file_mut(&anim_pack_path, false, true).ok(),
1133                    DataSource::AssKitFiles |
1134                    DataSource::ExternalFile => unreachable!("add_files_to_animpack"),
1135                };
1136
1137                let files = match anim_pack_file {
1138                    Some(file) => {
1139
1140                        // Try to decode it using lazy_load if enabled.
1141                        let extra_data = DecodeableExtraData::default();
1142                        //extra_data.set_lazy_load(SETTINGS.read().unwrap().bool("use_lazy_loading"));
1143                        let _ = file.decode(&Some(extra_data), true, false);
1144
1145                        match file.decoded_mut() {
1146                            Ok(decoded) => match decoded {
1147                                RFileDecoded::AnimPack(anim_pack) => anim_pack.files_by_paths(&paths, false).into_iter().cloned().collect::<Vec<RFile>>(),
1148                                _ => {
1149                                    CentralCommand::send_back(&sender, Response::Error(format!("We expected {} to be of type {} but found {}. This is either a bug or you did weird things with the game selected.", anim_pack_path, FileType::AnimPack, FileType::from(&*decoded))));
1150                                    continue;
1151                                },
1152                            }
1153                            _ => {
1154                                CentralCommand::send_back(&sender, Response::Error(format!("Failed to decode the file at the following path: {}", anim_pack_path)));
1155                                continue;
1156                            },
1157                        }
1158                    }
1159                    None => {
1160                        CentralCommand::send_back(&sender, Response::Error(format!("The file with the path {} doesn't exists on the open Pack.", anim_pack_path)));
1161                        continue;
1162                    }
1163                };
1164
1165                let result_paths = files.iter().map(|file| file.path_in_container()).collect::<Vec<_>>();
1166                match packs.get_mut(&dest_pack_key) {
1167                    Some(pack) => {
1168                        for mut file in files {
1169                            let _ = file.guess_file_type();
1170                            let _ = pack.insert(file);
1171                        }
1172                        CentralCommand::send_back(&sender, Response::VecContainerPath(result_paths));
1173                    }
1174                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", dest_pack_key))),
1175                }
1176            }
1177
1178            // In case we want to delete files from an Animpack...
1179            Command::DeleteFromAnimpack(pack_key, anim_pack_path, paths) => {
1180                match packs.get_mut(&pack_key) {
1181                    Some(pack) => {
1182                match pack.files_mut().get_mut(&anim_pack_path) {
1183                    Some(file) => {
1184
1185                        // Try to decode it using lazy_load if enabled.
1186                        let extra_data = DecodeableExtraData::default();
1187                        //extra_data.set_lazy_load(SETTINGS.read().unwrap().bool("use_lazy_loading"));
1188                        let _ = file.decode(&Some(extra_data), true, false);
1189
1190                        match file.decoded_mut() {
1191                            Ok(decoded) => match decoded {
1192                                RFileDecoded::AnimPack(anim_pack) => {
1193                                    for path in paths {
1194                                        anim_pack.remove(&path);
1195                                    }
1196
1197                                    CentralCommand::send_back(&sender, Response::Success);
1198                                }
1199                                _ => CentralCommand::send_back(&sender, Response::Error(format!("We expected {} to be of type {} but found {}. This is either a bug or you did weird things with the game selected.", anim_pack_path, FileType::AnimPack, FileType::from(&*decoded)))),
1200                            }
1201                            _ => CentralCommand::send_back(&sender, Response::Error(format!("Failed to decode the file at the following path: {}", anim_pack_path))),
1202                        }
1203                    }
1204                    None => CentralCommand::send_back(&sender, Response::Error(format!("File not found in the Pack: {}.", anim_pack_path))),
1205                }
1206                    }
1207                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1208                }
1209            }
1210
1211            // In case we want to decode a RigidModel PackedFile...
1212            Command::DecodePackedFile(pack_key, path, data_source) => {
1213                info!("Trying to decode a file. Path: {}", &path);
1214                info!("Trying to decode a file. Data Source: {}", &data_source);
1215
1216                match data_source {
1217                    DataSource::PackFile => {
1218                        match packs.get_mut(&pack_key) {
1219                            Some(pack) => {
1220                                if path == RESERVED_NAME_NOTES {
1221                                    let mut note = Text::default();
1222                                    note.set_format(TextFormat::Markdown);
1223                                    note.set_contents(pack.notes().pack_notes().to_owned());
1224                                    CentralCommand::send_back(&sender, Response::Text(note));
1225                                }
1226
1227                                else {
1228
1229                                    // Find the PackedFile we want and send back the response.
1230                                    match pack.files_mut().get_mut(&path) {
1231                                        Some(file) => decode_and_send_file(file, &sender, &settings, game, &schema),
1232                                        None => CentralCommand::send_back(&sender, Response::Error(format!("The file with the path {} hasn't been found on this Pack.", path))),
1233                                    }
1234                                }
1235                            }
1236                            None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1237                        }
1238                    }
1239
1240                    DataSource::ParentFiles => {
1241                        match dependencies.write().unwrap().file_mut(&path, false, true) {
1242                            Ok(file) => decode_and_send_file(file, &sender, &settings, game, &schema),
1243                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1244                        }
1245                    }
1246
1247                    DataSource::GameFiles => {
1248                        match dependencies.write().unwrap().file_mut(&path, true, false) {
1249                            Ok(file) => decode_and_send_file(file, &sender, &settings, game, &schema),
1250                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1251                        }
1252                    }
1253
1254                    DataSource::AssKitFiles => {
1255                        let path_split = path.split('/').collect::<Vec<_>>();
1256                        if path_split.len() > 2 {
1257                            match dependencies.read().unwrap().asskit_only_db_tables().get(path_split[1]) {
1258                                Some(db) => CentralCommand::send_back(&sender, Response::DBRFileInfo(db.clone(), RFileInfo::default())),
1259                                None => CentralCommand::send_back(&sender, Response::Error(format!("Table {} not found on Assembly Kit files.", path))),
1260                            }
1261                        } else {
1262                            CentralCommand::send_back(&sender, Response::Error(format!("Path {} doesn't contain an identifiable table name.", path)));
1263                        }
1264                    }
1265
1266                    DataSource::ExternalFile => {
1267                        CentralCommand::send_back(&sender, Response::Success);
1268                    }
1269                }
1270            }
1271
1272            // When we want to save a PackedFile from the view....
1273            Command::SavePackedFileFromView(pack_key, path, file_decoded) => {
1274                match packs.get_mut(&pack_key) {
1275                    Some(pack) => {
1276                        if path == RESERVED_NAME_NOTES {
1277                            if let RFileDecoded::Text(data) = file_decoded {
1278                                pack.notes_mut().set_pack_notes(data.contents().to_owned());
1279                            }
1280                        }
1281                        else if let Some(file) = pack.files_mut().get_mut(&path) {
1282                            if let Err(error) = file.set_decoded(file_decoded) {
1283                                CentralCommand::send_back(&sender, Response::Error(error.to_string()));
1284                                continue;
1285                            }
1286                        }
1287                        CentralCommand::send_back(&sender, Response::Success);
1288                    }
1289                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1290                }
1291            }
1292
1293            // In case we want to delete PackedFiles from a PackFile...
1294            Command::DeletePackedFiles(pack_key, paths) => {
1295                match packs.get_mut(&pack_key) {
1296                    Some(pack) => CentralCommand::send_back(&sender, Response::VecContainerPath(paths.iter().flat_map(|path| pack.remove(path)).collect())),
1297                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1298                }
1299            }
1300
1301            // Copy files to the internal clipboard.
1302            Command::CopyPackedFiles(paths_by_pack) => {
1303                clipboard_entries.clear();
1304                for (pack_key, paths) in &paths_by_pack {
1305                    if let Some(pack) = packs.get(pack_key) {
1306                        clipboard_entries.extend(clipboard_entries_from_paths(pack, paths, pack_key));
1307                    }
1308                }
1309                clipboard_is_cut = false;
1310                CentralCommand::send_back(&sender, Response::Success);
1311            }
1312
1313            // Cut files to the internal clipboard.
1314            Command::CutPackedFiles(paths_by_pack) => {
1315                clipboard_entries.clear();
1316                for (pack_key, paths) in &paths_by_pack {
1317                    if let Some(pack) = packs.get(pack_key) {
1318                        clipboard_entries.extend(clipboard_entries_from_paths(pack, paths, pack_key));
1319                    }
1320                }
1321                clipboard_is_cut = true;
1322                CentralCommand::send_back(&sender, Response::Success);
1323            }
1324
1325            // Paste files from the internal clipboard into a pack.
1326            Command::PastePackedFiles(target_key, destination_path) => {
1327                if clipboard_entries.is_empty() {
1328                    CentralCommand::send_back(&sender, Response::Error("Clipboard is empty.".to_string()));
1329                } else {
1330
1331                    // Clone files from their source packs and compute their new paths.
1332                    // We collect all cloned files first so we don't hold borrows while mutating.
1333                    let mut files_to_insert: Vec<RFile> = Vec::with_capacity(clipboard_entries.len());
1334                    for (file_path, base_path, source_key) in &clipboard_entries {
1335                        if let Some(source_pack) = packs.get(source_key) {
1336                            let path_as_container = ContainerPath::File(file_path.clone());
1337                            let found = source_pack.files_by_paths(&[path_as_container], false);
1338                            if let Some(file) = found.first() {
1339                                let mut new_file = (*file).clone();
1340                                let _ = new_file.load();
1341
1342                                // Compute relative path by stripping this file's base path.
1343                                let relative_path = if !base_path.is_empty() && file_path.starts_with(base_path) {
1344                                    file_path[base_path.len()..].trim_start_matches('/')
1345                                } else {
1346                                    file_path
1347                                };
1348                                let new_path = if destination_path.is_empty() {
1349                                    relative_path.to_string()
1350                                } else {
1351                                    format!("{}/{}", destination_path.trim_end_matches('/'), relative_path)
1352                                };
1353                                new_file.set_path_in_container_raw(&new_path);
1354                                files_to_insert.push(new_file);
1355                            }
1356                        }
1357                    }
1358
1359                    // If it was a cut operation, delete the files from their respective source packs.
1360                    let mut cut_deleted_by_pack: BTreeMap<String, Vec<ContainerPath>> = BTreeMap::new();
1361                    if clipboard_is_cut {
1362                        for (file_path, _, source_key) in &clipboard_entries {
1363                            if let Some(source_pack) = packs.get_mut(source_key) {
1364                                let removed = source_pack.remove(&ContainerPath::File(file_path.clone()));
1365                                cut_deleted_by_pack.entry(source_key.clone()).or_default().extend(removed);
1366                            }
1367                        }
1368                    }
1369
1370                    // Insert the cloned files into the target pack.
1371                    match packs.get_mut(&target_key) {
1372                        Some(target_pack) => {
1373                            let mut added_paths = Vec::with_capacity(files_to_insert.len());
1374                            for new_file in files_to_insert {
1375                                if let Ok(Some(path)) = target_pack.insert(new_file) {
1376                                    added_paths.push(path);
1377                                }
1378                            }
1379
1380                            // Force decoding of table/locs, so they're in memory for the diagnostics to work.
1381                            if let Some(ref schema) = schema {
1382                                let mut decode_extra_data = DecodeableExtraData::default();
1383                                decode_extra_data.set_schema(Some(schema));
1384                                let extra_data = Some(decode_extra_data);
1385
1386                                let mut files = target_pack.files_by_paths_mut(&added_paths, false);
1387                                files.par_iter_mut()
1388                                    .filter(|file| file.file_type() == FileType::DB || file.file_type() == FileType::Loc)
1389                                    .for_each(|file| {
1390                                        let _ = file.decode(&extra_data, true, false);
1391                                    });
1392                            }
1393
1394                            CentralCommand::send_back(&sender, Response::VecContainerPathBTreeMapStringVecContainerPath(added_paths, cut_deleted_by_pack));
1395
1396                            // Clear clipboard after a cut-paste operation.
1397                            if clipboard_is_cut {
1398                                clipboard_entries.clear();
1399                                clipboard_is_cut = false;
1400                            }
1401                        }
1402                        None => CentralCommand::send_back(&sender, Response::Error(format!("Target pack not found: {}", target_key))),
1403                    }
1404                }
1405            }
1406
1407            // Duplicate files in-place within the same pack.
1408            Command::DuplicatePackedFiles(pack_key, paths) => {
1409                match packs.get_mut(&pack_key) {
1410                    Some(pack) => {
1411                        // First, clone all the files we want to duplicate.
1412                        let files_to_dup: Vec<RFile> = pack.files_by_paths(&paths, false)
1413                            .into_iter()
1414                            .cloned()
1415                            .collect();
1416
1417                        let mut added_paths = Vec::with_capacity(files_to_dup.len());
1418                        for file in files_to_dup {
1419                            let old_path = file.path_in_container_raw().to_string();
1420
1421                            // Generate a new name with a numeric suffix: "name.ext" -> "name1.ext", "name1.ext" -> "name2.ext", etc.
1422                            let new_path = if let Some(dot_pos) = old_path.rfind('.') {
1423                                let (base, ext) = old_path.split_at(dot_pos);
1424
1425                                // Find and increment any trailing number in the base name.
1426                                let base_trimmed = base.trim_end_matches(|c: char| c.is_ascii_digit());
1427                                let suffix_str = &base[base_trimmed.len()..];
1428                                let mut counter = suffix_str.parse::<u32>().unwrap_or(0) + 1;
1429
1430                                // Keep incrementing until we find a name that doesn't exist.
1431                                loop {
1432                                    let candidate = format!("{}{}{}", base_trimmed, counter, ext);
1433                                    if !pack.has_file(&candidate) {
1434                                        break candidate;
1435                                    }
1436                                    counter += 1;
1437                                }
1438                            } else {
1439                                // No extension, just append a number.
1440                                let base_trimmed = old_path.trim_end_matches(|c: char| c.is_ascii_digit());
1441                                let suffix_str = &old_path[base_trimmed.len()..];
1442                                let mut counter = suffix_str.parse::<u32>().unwrap_or(0) + 1;
1443
1444                                loop {
1445                                    let candidate = format!("{}{}", base_trimmed, counter);
1446                                    if !pack.has_file(&candidate) {
1447                                        break candidate;
1448                                    }
1449                                    counter += 1;
1450                                }
1451                            };
1452
1453                            let mut new_file = file;
1454                            new_file.set_path_in_container_raw(&new_path);
1455
1456                            if let Ok(Some(path)) = pack.insert(new_file) {
1457                                added_paths.push(path);
1458                            }
1459                        }
1460
1461                        // Force decoding of table/locs, so they're in memory for the diagnostics to work.
1462                        if let Some(ref schema) = schema {
1463                            let mut decode_extra_data = DecodeableExtraData::default();
1464                            decode_extra_data.set_schema(Some(schema));
1465                            let extra_data = Some(decode_extra_data);
1466
1467                            let mut files = pack.files_by_paths_mut(&added_paths, false);
1468                            files.par_iter_mut()
1469                                .filter(|file| file.file_type() == FileType::DB || file.file_type() == FileType::Loc)
1470                                .for_each(|file| {
1471                                    let _ = file.decode(&extra_data, true, false);
1472                                });
1473                        }
1474
1475                        CentralCommand::send_back(&sender, Response::VecContainerPath(added_paths));
1476                    }
1477                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1478                }
1479            }
1480
1481            // In case we want to extract PackedFiles from a PackFile...
1482            Command::ExtractPackedFiles(pack_key, container_paths, path, extract_tables_to_tsv) => {
1483                let schema = if extract_tables_to_tsv { &schema } else { &None };
1484                let mut errors = 0;
1485
1486                // Pack extraction.
1487                if let Some(container_paths) = container_paths.get(&DataSource::PackFile) {
1488                    match packs.get_mut(&pack_key) {
1489                        Some(pack) => {
1490                            let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, pack.compression_format(), settings.bool("disable_uuid_regeneration_on_db_tables")));
1491                            let mut extracted_paths = vec![];
1492
1493                            for container_path in container_paths {
1494                                match pack.extract(container_path.clone(), &path, true, schema, false, settings.bool("tables_use_old_column_order_for_tsv"), &extra_data) {
1495                                    Ok(mut extracted_path) => extracted_paths.append(&mut extracted_path),
1496                                    Err(_) => {
1497                                        //error!("Error extracting {}: {}", container_path.path_raw(), error);
1498                                        errors += 1;
1499                                    },
1500                                }
1501                            }
1502
1503                            if errors == 0 {
1504                                CentralCommand::send_back(&sender, Response::StringVecPathBuf(tr("files_extracted_success"), extracted_paths));
1505                            } else {
1506                                CentralCommand::send_back(&sender, Response::Error(format!("There were {} errors while extracting.", errors)));
1507                            }
1508                        }
1509                        None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1510                    }
1511                }
1512
1513                // Dependencies extraction.
1514                else {
1515
1516                    let dependencies = dependencies.read().unwrap();
1517                    let mut game_files = if let Some(container_paths) = container_paths.get(&DataSource::GameFiles) {
1518                        dependencies.files_by_path(container_paths, true, false, false)
1519                    } else {
1520                        HashMap::new()
1521                    };
1522                    let parent_files = if let Some(container_paths) = container_paths.get(&DataSource::ParentFiles) {
1523                        dependencies.files_by_path(container_paths, false, true, false)
1524                    } else {
1525                        HashMap::new()
1526                    };
1527
1528                    game_files.extend(parent_files);
1529
1530                    let mut pack = Pack::default();
1531                    let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, pack.compression_format(), settings.bool("disable_uuid_regeneration_on_db_tables")));
1532                    let mut extracted_paths = vec![];
1533                    for (path_raw, file) in game_files {
1534                        if pack.insert(file.clone()).is_err() {
1535                            errors += 1;
1536                            continue;
1537                        }
1538
1539                        let container_path = ContainerPath::File(path_raw);
1540                        match pack.extract(container_path.clone(), &path, true, schema, false, settings.bool("tables_use_old_column_order_for_tsv"), &extra_data) {
1541                            Ok(mut extracted_path) => extracted_paths.append(&mut extracted_path),
1542                            Err(_) => errors += 1,
1543                        }
1544
1545                        // Drop the cloned file from the temp pack so memory doesn't grow with the batch.
1546                        pack.remove(&container_path);
1547                    }
1548
1549                    if errors == 0 {
1550                        CentralCommand::send_back(&sender, Response::StringVecPathBuf(tr("files_extracted_success"), extracted_paths));
1551                    } else {
1552                        CentralCommand::send_back(&sender, Response::Error(format!("There were {} errors while extracting.", errors)));
1553                    }
1554                }
1555            }
1556
1557            // In case we want to rename one or more files/folders...
1558            Command::RenamePackedFiles(pack_key, renaming_data) => {
1559                match packs.get_mut(&pack_key) {
1560                    Some(pack) => {
1561                        match pack.move_paths(&renaming_data) {
1562                            Ok(data) => CentralCommand::send_back(&sender, Response::VecContainerPathContainerPath(data)),
1563                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1564                        }
1565                    }
1566                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1567                }
1568            }
1569
1570            // In case we want to know if a Folder exists, knowing his path...
1571            Command::FolderExists(pack_key, path) => {
1572                match packs.get(&pack_key) {
1573                    Some(pack) => CentralCommand::send_back(&sender, Response::Bool(pack.has_folder(&path))),
1574                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1575                }
1576            }
1577
1578            // In case we want to know if PackedFile exists, knowing his path...
1579            Command::PackedFileExists(pack_key, path) => {
1580                match packs.get(&pack_key) {
1581                    Some(pack) => CentralCommand::send_back(&sender, Response::Bool(pack.has_file(&path))),
1582                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1583                }
1584            }
1585
1586            // In case we want to get the list of tables in the dependency database...
1587            Command::GetTableListFromDependencyPackFile => {
1588                let dependencies = dependencies.read().unwrap();
1589                CentralCommand::send_back(&sender, Response::VecString(dependencies.vanilla_loose_tables().keys().chain(dependencies.vanilla_tables().keys()).map(|x| x.to_owned()).collect()))
1590            },
1591            Command::GetCustomTableList => match &schema {
1592                Some(schema) => {
1593                    let tables = schema.definitions().par_iter().filter(|(key, defintions)|
1594                        !defintions.is_empty() && (
1595                            key.starts_with("start_pos_") ||
1596                            key.starts_with("twad_")
1597                        )
1598                    ).map(|(key, _)| key.to_owned()).collect::<Vec<_>>();
1599                    CentralCommand::send_back(&sender, Response::VecString(tables));
1600                }
1601                None => CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string()))
1602            },
1603
1604            Command::LocalArtSetIds(_pack_key) => {
1605                CentralCommand::send_back(&sender, Response::HashSetString(dependencies.read().unwrap().db_values_from_table_name_and_column_name(Some(&packs), "campaign_character_arts_tables", "art_set_id", false, false)));
1606            }
1607
1608            // TODO: This needs to use a list pulled from portrait settings files, not from a table.
1609            Command::DependenciesArtSetIds => CentralCommand::send_back(&sender, Response::HashSetString(dependencies.read().unwrap().db_values_from_table_name_and_column_name(None, "campaign_character_arts_tables", "art_set_id", true, true))),
1610
1611            // In case we want to get the version of an specific table from the dependency database...
1612            Command::GetTableVersionFromDependencyPackFile(table_name) => {
1613                if dependencies.read().unwrap().is_vanilla_data_loaded(false) {
1614                    match dependencies.read().unwrap().db_version(&table_name) {
1615                        Some(version) => CentralCommand::send_back(&sender, Response::I32(version)),
1616                        None => {
1617
1618                            // If the table is one of the starpos tables, we need to return the latest version of the table, even if it's not in the game files.
1619                            if table_name.starts_with("start_pos_") || table_name.starts_with("twad_") || table_name.starts_with("ceo") {
1620                                match &schema {
1621                                    Some(schema) => {
1622                                        match schema.definitions_by_table_name(&table_name) {
1623                                            Some(definitions) => {
1624                                                if definitions.is_empty() {
1625                                                    CentralCommand::send_back(&sender, Response::Error("There are no definitions for this specific table.".to_string()));
1626                                                } else {
1627                                                    CentralCommand::send_back(&sender, Response::I32(*definitions.first().unwrap().version()));
1628                                                }
1629                                            }
1630                                            None => CentralCommand::send_back(&sender, Response::Error("There are no definitions for this specific table.".to_string())),
1631                                        }
1632                                    }
1633                                    None => CentralCommand::send_back(&sender, Response::Error("There is no Schema for the Game Selected.".to_string().to_string()))
1634                                }
1635                            } else {
1636                                CentralCommand::send_back(&sender, Response::Error("Table not found in the game files.".to_string()))
1637                            }
1638                        },
1639                    }
1640                } else { CentralCommand::send_back(&sender, Response::Error("Dependencies cache needs to be regenerated before this.".to_string().to_string())); }
1641            }
1642
1643            Command::GetTableDefinitionFromDependencyPackFile(table_name) => {
1644                if dependencies.read().unwrap().is_vanilla_data_loaded(false) {
1645                    if let Some(ref schema) = schema {
1646                        if let Some(version) = dependencies.read().unwrap().db_version(&table_name) {
1647                            if let Some(definition) = schema.definition_by_name_and_version(&table_name, version) {
1648                                CentralCommand::send_back(&sender, Response::Definition(definition.clone()));
1649                            } else { CentralCommand::send_back(&sender, Response::Error(format!("No definition found for table {}.", table_name).to_string())); }
1650                        } else { CentralCommand::send_back(&sender, Response::Error(format!("Table version not found in dependencies for table {}.", table_name).to_string())); }
1651                    } else { CentralCommand::send_back(&sender, Response::Error("There is no Schema for the Game Selected.".to_string().to_string())); }
1652                } else { CentralCommand::send_back(&sender, Response::Error("Dependencies cache needs to be regenerated before this.".to_string().to_string())); }
1653            }
1654
1655            // In case we want to merge DB or Loc Tables from a PackFile...
1656            Command::MergeFiles(pack_key, paths, merged_path, delete_source_files, options) => {
1657                match packs.get_mut(&pack_key) {
1658                    Some(pack) => {
1659                        let files_to_merge = pack.files_by_paths(&paths, false);
1660
1661                        let merge_result: Result<DeltaMergeOutcome> = if *options.delta_merge() {
1662                            delta_merge_files(&files_to_merge, &merged_path, &dependencies.read().unwrap(), options.resolutions())
1663                        } else {
1664                            RFile::merge(&files_to_merge, &merged_path).map(DeltaMergeOutcome::Merged).map_err(|error| anyhow!(error.to_string()))
1665                        };
1666
1667                        match merge_result {
1668                            Ok(DeltaMergeOutcome::Merged(file)) => {
1669                                let _ = pack.insert(file);
1670
1671                                // Make sure to only delete the files if they're not the destination file.
1672                                if delete_source_files {
1673                                    paths.iter()
1674                                        .filter(|path| merged_path != path.path_raw())
1675                                        .for_each(|path| { pack.remove(path); });
1676                                }
1677
1678                                CentralCommand::send_back(&sender, Response::String(merged_path.to_string()));
1679                            },
1680                            Ok(DeltaMergeOutcome::Conflicts(conflicts)) => CentralCommand::send_back(&sender, Response::MergeConflicts(conflicts)),
1681                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1682                        }
1683                    }
1684                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1685                }
1686            }
1687
1688            // In case we want to update a table...
1689            Command::UpdateTable(pack_key, path) => {
1690                let path = path.path_raw();
1691                match packs.get_mut(&pack_key) {
1692                    Some(pack) => {
1693                if let Some(rfile) = pack.file_mut(path, false) {
1694                    if let Ok(decoded) = rfile.decoded_mut() {
1695                        match dependencies.write().unwrap().update_db(decoded) {
1696                            Ok((old_version, new_version, fields_deleted, fields_added)) => CentralCommand::send_back(&sender, Response::I32I32VecStringVecString(old_version, new_version, fields_deleted, fields_added)),
1697                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1698                        }
1699                    } else { CentralCommand::send_back(&sender, Response::Error(anyhow!("File with the following path undecoded: {}", path).to_string())); }
1700                } else { CentralCommand::send_back(&sender, Response::Error(anyhow!("File not found in the open Pack: {}", path).to_string())); }
1701                    }
1702                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1703                }
1704            }
1705
1706            // In case we want to replace all matches in a Global Search...
1707            Command::GlobalSearchReplaceMatches(_pack_key, mut global_search, matches) => {
1708                if let Some(ref schema) = schema {
1709                    match global_search.replace(game, schema, &mut packs, &mut dependencies.write().unwrap(), &matches) {
1710                        Ok(paths) => {
1711                            let files_info = paths.iter().flat_map(|path| {
1712                                packs.values().flat_map(|pack| pack.files_by_path(path, false).iter().map(|file| RFileInfo::from(*file)).collect::<Vec<RFileInfo>>()).collect::<Vec<_>>()
1713                            }).collect();
1714                            CentralCommand::send_back(&sender, Response::GlobalSearchVecRFileInfo(Box::new(global_search), files_info));
1715                        }
1716                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1717                    }
1718                } else {
1719                    CentralCommand::send_back(&sender, Response::Error(anyhow!("Schema not found. Maybe you need to download it?").to_string()));
1720                }
1721            }
1722
1723            // In case we want to replace all matches in a Global Search...
1724            Command::GlobalSearchReplaceAll(_pack_key, mut global_search) => {
1725                if let Some(ref schema) = schema {
1726                    match global_search.replace_all(game, schema, &mut packs, &mut dependencies.write().unwrap()) {
1727                        Ok(paths) => {
1728                            let files_info = paths.iter().flat_map(|path| {
1729                                packs.values().flat_map(|pack| pack.files_by_path(path, false).iter().map(|file| RFileInfo::from(*file)).collect::<Vec<RFileInfo>>()).collect::<Vec<_>>()
1730                            }).collect();
1731                            CentralCommand::send_back(&sender, Response::GlobalSearchVecRFileInfo(Box::new(global_search), files_info));
1732                        }
1733                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1734                    }
1735                } else {
1736                    CentralCommand::send_back(&sender, Response::Error(anyhow!("Schema not found. Maybe you need to download it?").to_string()));
1737                }
1738            }
1739
1740            // In case we want to get the reference data for a definition...
1741            Command::GetReferenceDataFromDefinition(_pack_key, table_name, definition, force_local_ref_generation) => {
1742                let mut reference_data = HashMap::new();
1743
1744                // Only generate the cache references if we don't already have them generated.
1745                if let Some(ref schema) = schema {
1746                    if dependencies.read().unwrap().local_tables_references().get(&table_name).is_none() || force_local_ref_generation {
1747                        dependencies.write().unwrap().generate_local_definition_references(schema, &table_name, &definition);
1748                    }
1749
1750                    reference_data = dependencies.read().unwrap().db_reference_data(schema, &packs, &table_name, &definition, &None);
1751                }
1752
1753                CentralCommand::send_back(&sender, Response::HashMapI32TableReferences(reference_data));
1754            }
1755
1756            // In case we want to change the format of a ca_vp8 video...
1757            Command::SetVideoFormat(pack_key, path, format) => {
1758                match packs.get_mut(&pack_key) {
1759                    Some(pack) => {
1760                match pack.files_mut().get_mut(&path) {
1761                    Some(ref mut rfile) => {
1762                        match rfile.decoded_mut() {
1763                            Ok(data) => {
1764                                if let RFileDecoded::Video(ref mut data) = data {
1765                                    data.set_format(format);
1766                                    CentralCommand::send_back(&sender, Response::Success);
1767                                } else {
1768                                    CentralCommand::send_back(&sender, Response::Error("The file is not a video.".to_string()));
1769                                }
1770                            }
1771                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1772                        }
1773                    }
1774                    None => CentralCommand::send_back(&sender, Response::Error("This Pack doesn't exists as a file in the disk.".to_string())),
1775                }
1776                    }
1777                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1778                }
1779            },
1780
1781            // In case we want to save an schema to disk...
1782            Command::SaveSchema(mut schema_new) => {
1783                match schema_new.save(&schemas_path().unwrap().join(game.schema_file_name())) {
1784                    Ok(_) => {
1785                        schema = Some(schema_new);
1786                        CentralCommand::send_back(&sender, Response::Success);
1787                    },
1788                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1789                }
1790            }
1791
1792            // In case we want to clean the cache of one or more PackedFiles...
1793            Command::CleanCache(pack_key, paths) => {
1794                match packs.get_mut(&pack_key) {
1795                    Some(pack) => {
1796                        let cf = pack.compression_format();
1797                        let mut files = pack.files_by_paths_mut(&paths, false);
1798                        let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, cf, settings.bool("disable_uuid_regeneration_on_db_tables")));
1799
1800                        files.iter_mut().for_each(|file| {
1801                            let _ = file.encode(&extra_data, true, true, false);
1802                        });
1803                        CentralCommand::send_back(&sender, Response::Success);
1804                    }
1805                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1806                }
1807            }
1808
1809            // In case we want to export a PackedFile as a TSV file...
1810            Command::ExportTSV(pack_key, internal_path, external_path, data_source) => {
1811                let mut dependencies = dependencies.write().unwrap();
1812                match &schema {
1813                    Some(ref schema) => {
1814                        let file = match data_source {
1815                            DataSource::PackFile => packs.get_mut(&pack_key).and_then(|pack| pack.file_mut(&internal_path, false)),
1816                            DataSource::ParentFiles => dependencies.file_mut(&internal_path, false, true).ok(),
1817                            DataSource::GameFiles => dependencies.file_mut(&internal_path, true, false).ok(),
1818                            DataSource::AssKitFiles => {
1819                                CentralCommand::send_back(&sender, Response::Error("Exporting a TSV from the Assembly Kit is not yet supported.".to_string()));
1820                                continue;
1821                            },
1822                            DataSource::ExternalFile => {
1823                                CentralCommand::send_back(&sender, Response::Error("Exporting a TSV from a external file is not yet supported.".to_string()));
1824                                continue;
1825                            },
1826                        };
1827                        match file {
1828                            Some(file) => match file.tsv_export_to_path(&external_path, schema, settings.bool("tables_use_old_column_order_for_tsv")) {
1829                                Ok(_) => CentralCommand::send_back(&sender, Response::Success),
1830                                Err(error) =>  CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1831                            }
1832                            None => CentralCommand::send_back(&sender, Response::Error(format!("File with the following path not found in the Pack: {}", internal_path).to_string())),
1833                        }
1834                    },
1835                    None => CentralCommand::send_back(&sender, Response::Error("There is no Schema for the Game Selected.".to_string().to_string())),
1836                }
1837            }
1838
1839            // In case we want to import a TSV as a PackedFile...
1840            // TODO: This is... unreliable at best, can break stuff at worst. Replace the set_decoded with proper type checking.
1841            Command::ImportTSV(pack_key, internal_path, external_path) => {
1842                match packs.get_mut(&pack_key) {
1843                    Some(pack) => {
1844                match pack.file_mut(&internal_path, false) {
1845                    Some(file) => {
1846                        // Preserve the original table GUID, as set_decoded would replace it with a fresh
1847                        // one from the imported table, making the import non-idempotent for DB tables.
1848                        let original_guid = if let Ok(RFileDecoded::DB(table)) = file.decoded() {
1849                            Some(table.guid().to_owned())
1850                        } else {
1851                            None
1852                        };
1853
1854                        match RFile::tsv_import_from_path(&external_path, &schema) {
1855                            Ok(imported) => {
1856                                let mut decoded = imported.decoded().unwrap().clone();
1857                                if let (RFileDecoded::DB(table), Some(guid)) = (&mut decoded, original_guid) {
1858                                    table.set_guid(guid);
1859                                }
1860                                file.set_decoded(decoded.clone()).unwrap();
1861                                CentralCommand::send_back(&sender, Response::RFileDecoded(decoded))
1862                            },
1863                            Err(error) =>  CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1864                        }
1865                    }
1866                    None => CentralCommand::send_back(&sender, Response::Error(anyhow!("File with the following path not found in the Pack: {}", internal_path).to_string())),
1867                }
1868                    }
1869                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1870                }
1871            }
1872
1873            // In case we want to open a PackFile's location in the file manager...
1874            Command::OpenContainingFolder(pack_key) => {
1875                match packs.get(&pack_key) {
1876                    Some(pack) => {
1877
1878                // If the path exists, try to open it. If not, throw an error.
1879                let mut path_str = pack.disk_file_path().to_owned();
1880
1881                // Remove canonicalization, as it breaks the open thingy.
1882                if path_str.starts_with("//?/") || path_str.starts_with("\\\\?\\") {
1883                    path_str = path_str[4..].to_string();
1884                }
1885
1886                let mut path = PathBuf::from(path_str);
1887                if path.exists() {
1888                    path.pop();
1889                    let _ = open::that(&path);
1890                    CentralCommand::send_back(&sender, Response::Success);
1891                }
1892                else {
1893                    CentralCommand::send_back(&sender, Response::Error("This Pack doesn't exists as a file in the disk.".to_string()));
1894                }
1895                    }
1896                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1897                }
1898            },
1899
1900            // When we want to open a PackedFile in a external program...
1901            Command::OpenPackedFileInExternalProgram(pack_key, data_source, path) => {
1902                match data_source {
1903                    DataSource::PackFile => {
1904                        match packs.get_mut(&pack_key) {
1905                            Some(pack) => {
1906                        let folder = temp_dir().join(format!("rpfm_{}", pack.disk_file_name()));
1907                        let cf = pack.compression_format();
1908                        let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, cf, settings.bool("disable_uuid_regeneration_on_db_tables")));
1909
1910                        match pack.extract(path.clone(), &folder, true, &schema, false, settings.bool("tables_use_old_column_order_for_tsv"), &extra_data) {
1911                            Ok(extracted_path) => {
1912                                let _ = that(&extracted_path[0]);
1913                                CentralCommand::send_back(&sender, Response::PathBuf(extracted_path[0].to_owned()));
1914                            }
1915                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1916                        }
1917                            }
1918                            None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1919                        }
1920                    }
1921                    _ => CentralCommand::send_back(&sender, Response::Error(anyhow!("Opening dependencies files in external programs is not yet supported.").to_string())),
1922                }
1923            }
1924
1925            // When we want to save a PackedFile from the external view....
1926            Command::SavePackedFileFromExternalView(pack_key, path, external_path) => {
1927                match packs.get_mut(&pack_key) {
1928                    Some(pack) => {
1929                match pack.file_mut(&path, false) {
1930                    Some(file) => match file.encode_from_external_data(&schema, &external_path) {
1931                        Ok(_) => CentralCommand::send_back(&sender, Response::Success),
1932                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1933                    }
1934                    None => CentralCommand::send_back(&sender, Response::Error(anyhow!("File not found").to_string())),
1935                }
1936                    }
1937                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
1938                }
1939            }
1940
1941            // When we want to list the plugin scripts available under the config "scripts" folder.
1942            Command::GetPluginScripts => {
1943                match scripts_path() {
1944                    Ok(folder) => {
1945                        let mut scripts = vec![];
1946                        if let Ok(entries) = std::fs::read_dir(&folder) {
1947                            for entry in entries.flatten() {
1948                                let path = entry.path();
1949                                if path.is_file() && plugin_script_interpreter(&path).is_some() {
1950                                    scripts.push(path.to_string_lossy().to_string());
1951                                }
1952                            }
1953                        }
1954
1955                        scripts.sort();
1956                        CentralCommand::send_back(&sender, Response::VecString(scripts));
1957                    }
1958                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
1959                }
1960            }
1961
1962            // When we want to run a plugin script against a selection of files/folders.
1963            Command::RunPluginScript(pack_key, script_path, container_paths) => {
1964                let interpreter = match plugin_script_interpreter(&script_path) {
1965                    Some(interpreter) => interpreter,
1966                    None => {
1967                        CentralCommand::send_back(&sender, Response::Error(format!("Unsupported plugin script type: {}", script_path.display())));
1968                        continue 'background_loop;
1969                    }
1970                };
1971
1972                match packs.get_mut(&pack_key) {
1973                    Some(pack) => {
1974
1975                        // Extract the selection to a per-pack temp folder, keeping the in-pack structure, so the
1976                        // script sees the same paths it would inside the Pack. DB/Loc files are handed out as TSV
1977                        // (same as the normal extract), everything else as raw binary.
1978                        let base_folder = temp_dir().join("rpfm_plugins").join(pack.disk_file_name());
1979                        let _ = std::fs::remove_dir_all(&base_folder);
1980
1981                        let cf = pack.compression_format();
1982                        let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, cf, settings.bool("disable_uuid_regeneration_on_db_tables")));
1983                        let keys_first = settings.bool("tables_use_old_column_order_for_tsv");
1984
1985                        let mut extracted_paths = vec![];
1986                        let mut extract_failed = false;
1987                        for container_path in &container_paths {
1988                            match pack.extract(container_path.clone(), &base_folder, true, &schema, false, keys_first, &extra_data) {
1989                                Ok(mut paths) => extracted_paths.append(&mut paths),
1990                                Err(error) => {
1991                                    CentralCommand::send_back(&sender, Response::Error(format!("Error extracting files for the plugin script: {}", error)));
1992                                    extract_failed = true;
1993                                    break;
1994                                }
1995                            }
1996                        }
1997
1998                        if extract_failed {
1999                            continue 'background_loop;
2000                        }
2001
2002                        // Run the script with the extracted file paths as arguments, waiting until it finishes.
2003                        let output = std::process::Command::new(interpreter)
2004                            .arg(&script_path)
2005                            .args(&extracted_paths)
2006                            .current_dir(&base_folder)
2007                            .output();
2008
2009                        let message = match output {
2010                            Ok(output) => {
2011                                let stdout = String::from_utf8_lossy(&output.stdout);
2012                                let stderr = String::from_utf8_lossy(&output.stderr);
2013
2014                                // Record the run so users can debug their scripts: a `last_run.log` in the
2015                                // scripts folder (overwritten each run), plus the standard terminal logger.
2016                                let report = format!("Script: {}\nStatus: {}\n\n--- stdout ---\n{}\n--- stderr ---\n{}\n", script_path.display(), output.status, stdout, stderr);
2017                                if let Ok(folder) = scripts_path() {
2018                                    let _ = std::fs::write(folder.join("last_run.log"), report.as_bytes());
2019                                }
2020
2021                                info!("Plugin script {} finished with {}.", script_path.display(), output.status);
2022                                if !stderr.trim().is_empty() {
2023                                    warn!("Plugin script stderr:\n{}", stderr.trim());
2024                                }
2025
2026                                if output.status.success() {
2027                                    None
2028                                } else {
2029                                    Some(format!("The plugin script finished with errors. See last_run.log in the scripts folder.\n\n{}", stderr.trim()))
2030                                }
2031                            }
2032                            Err(error) => {
2033                                error!("Failed to run the plugin script {}: {}", script_path.display(), error);
2034                                CentralCommand::send_back(&sender, Response::Error(format!("Failed to run the plugin script: {}", error)));
2035                                continue 'background_loop;
2036                            }
2037                        };
2038
2039                        // Read the (possibly modified) files back into the Pack. Files the script deleted are left untouched.
2040                        let mut reimported_paths = vec![];
2041                        for disk_path in &extracted_paths {
2042                            if !disk_path.is_file() {
2043                                continue;
2044                            }
2045
2046                            // TSV-exported DB/Loc files have a `.tsv` suffix appended to their in-pack name;
2047                            // strip it to find the real file when the direct path doesn't match anything.
2048                            let direct_path = container_path_from_disk_path(disk_path, &base_folder);
2049                            let container_path = if pack.file_mut(&direct_path, false).is_some() {
2050                                direct_path
2051                            } else if let Some(stripped) = direct_path.strip_suffix(".tsv") {
2052                                stripped.to_owned()
2053                            } else {
2054                                direct_path
2055                            };
2056
2057                            if let Some(file) = pack.file_mut(&container_path, false) {
2058                                if file.encode_from_external_data(&schema, disk_path).is_ok() {
2059                                    reimported_paths.push(ContainerPath::File(container_path));
2060                                }
2061                            }
2062                        }
2063
2064                        let _ = std::fs::remove_dir_all(&base_folder);
2065                        CentralCommand::send_back(&sender, Response::VecContainerPathOptionString(reimported_paths, message));
2066                    }
2067                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2068                }
2069            }
2070
2071            // When we want to update our schemas...
2072            Command::UpdateSchemas => {
2073
2074                // Run the git operation on the blocking thread pool to avoid blocking the async runtime.
2075                let git_result = tokio::task::spawn_blocking(|| {
2076                    match schemas_path() {
2077                        Ok(local_path) => {
2078                            let git_integration = GitIntegration::new(&local_path, SCHEMA_REPO, SCHEMA_BRANCH, SCHEMA_REMOTE);
2079                            git_integration.update_repo().map(|_| ()).map_err(|e| anyhow::anyhow!(e.to_string()))
2080                        },
2081                        Err(error) => Err(error),
2082                    }
2083                }).await.unwrap();
2084
2085                // Post-download state mutation must stay in the main loop (accesses local mutable state).
2086                match git_result {
2087                    Ok(_) => {
2088                        let schema_path = schemas_path().unwrap().join(game.schema_file_name());
2089                        let patches_path = table_patches_path().unwrap().join(game.schema_file_name());
2090
2091                        // Encode the decoded tables with the old schema, then re-decode them with the new one for all open packs.
2092                        for pack in packs.values_mut() {
2093                            let cf = pack.compression_format();
2094                            let mut tables = pack.files_by_type_mut(&[FileType::DB]);
2095                            let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, cf, settings.bool("disable_uuid_regeneration_on_db_tables")));
2096
2097                            tables.par_iter_mut().for_each(|x| { let _ = x.encode(&extra_data, true, true, false); });
2098                        }
2099
2100                        schema = Schema::load(&schema_path, Some(&patches_path)).ok();
2101
2102                        for pack in packs.values_mut() {
2103                            let mut extra_data = DecodeableExtraData::default();
2104                            extra_data.set_schema(schema.as_ref());
2105                            let extra_data = Some(extra_data);
2106
2107                            let mut tables = pack.files_by_type_mut(&[FileType::DB]);
2108                            tables.par_iter_mut().for_each(|x| {
2109                                let _ = x.decode(&extra_data, true, false);
2110                            });
2111                        }
2112
2113                        // Then rebuild the dependencies stuff.
2114                        if dependencies.read().unwrap().is_vanilla_data_loaded(false) {
2115                            let game_path = settings.path_buf(game.key());
2116                            let secondary_path = settings.path_buf(SECONDARY_PATH);
2117                            let dependencies_file_path = dependencies_cache_path().unwrap().join(game.dependencies_cache_file_name());
2118                            let pack_dependencies: Vec<_> = packs.values()
2119                                .flat_map(|pack| pack.dependencies().iter().map(|x| x.1.clone()))
2120                                .collect();
2121
2122                            match dependencies.write().unwrap().rebuild(&schema, &pack_dependencies, Some(&*dependencies_file_path), game, &game_path, &secondary_path) {
2123                                Ok(_) => CentralCommand::send_back(&sender, Response::Success),
2124                                Err(_) => CentralCommand::send_back(&sender, Response::Error("Schema updated, but dependencies cache rebuilding failed. You may need to regenerate it.".to_string())),
2125                            }
2126                        } else {
2127                            CentralCommand::send_back(&sender, Response::Success)
2128                        }
2129                    },
2130                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
2131                }
2132            }
2133
2134            // When we want to update our lua setup...
2135            Command::UpdateLuaAutogen => {
2136                let sender = sender.clone();
2137                tokio::spawn(async move {
2138                    let result = tokio::task::spawn_blocking(|| {
2139                        match lua_autogen_base_path() {
2140                            Ok(local_path) => {
2141                                let git_integration = GitIntegration::new(&local_path, LUA_REPO, LUA_BRANCH, LUA_REMOTE);
2142                                git_integration.update_repo().map(|_| ()).map_err(|e| e.into())
2143                            },
2144                            Err(error) => Err(error),
2145                        }
2146                    }).await.unwrap();
2147
2148                    match result {
2149                        Ok(_) => CentralCommand::send_back(&sender, Response::Success),
2150                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
2151                    }
2152                });
2153            }
2154
2155            // When we want to update our program...
2156            Command::UpdateMainProgram => {
2157                let sender = sender.clone();
2158                let settings = settings.clone();
2159                tokio::spawn(async move {
2160                    let result = tokio::task::spawn_blocking(move || {
2161                        crate::updater::update_main_program(&settings)
2162                    }).await.unwrap();
2163
2164                    match result {
2165                        Ok(_) => CentralCommand::send_back(&sender, Response::Success),
2166                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
2167                    }
2168                });
2169            }
2170
2171            // When we want to update our program...
2172            Command::TriggerBackupAutosave(pack_key) => {
2173                match packs.get(&pack_key) {
2174                    Some(pack) => {
2175                        let folder = backup_autosave_path().unwrap().join(pack.disk_file_name());
2176                        let _ = DirBuilder::new().recursive(true).create(&folder);
2177
2178                        let game_path = settings.path_buf(game.key());
2179                        let ca_paths = game.ca_packs_paths(&game_path)
2180                            .unwrap_or_default()
2181                            .iter()
2182                            .map(|path| path.to_string_lossy().replace('\\', "/"))
2183                            .collect::<Vec<_>>();
2184
2185                        let pack_disable_autosaves = pack.settings().setting_bool("disable_autosaves")
2186                            .unwrap_or(&true);
2187
2188                        let pack_type = pack.pfh_file_type();
2189                        let pack_path = pack.disk_file_path().replace('\\', "/");
2190
2191                        // Do not autosave vanilla packs, packs with autosave disabled, or non-mod or movie packs.
2192                        if folder.is_dir() &&
2193                            !pack_disable_autosaves &&
2194                            (pack_type == PFHFileType::Mod || pack_type == PFHFileType::Movie) &&
2195                            (ca_paths.is_empty() || !ca_paths.contains(&pack_path))
2196                        {
2197                            let date = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
2198                            let new_name = format!("{date}.pack");
2199                            let new_path = folder.join(new_name);
2200                            let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, pack.compression_format(), settings.bool("disable_uuid_regeneration_on_db_tables")));
2201                            let _ = pack.clone().save(Some(&new_path), game, &extra_data);
2202
2203                            // If we have more than the limit, delete the older one.
2204                            if let Ok(files) = files_in_folder_from_newest_to_oldest(&folder) {
2205                                let max_files = settings.i32("autosave_amount") as usize;
2206                                for (index, file) in files.iter().enumerate() {
2207                                    if index >= max_files {
2208                                        let _ = std::fs::remove_file(file);
2209                                    }
2210                                }
2211                            }
2212                        }
2213                        CentralCommand::send_back(&sender, Response::Success);
2214                    }
2215                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2216                }
2217            }
2218
2219            // In case we want to perform a diagnostics check...
2220            Command::DiagnosticsCheck(diagnostics_ignored, check_ak_only_refs) => {
2221                let game_path = settings.path_buf(game.key());
2222                let mut diagnostics = Diagnostics::default();
2223                *diagnostics.diagnostics_ignored_mut() = diagnostics_ignored;
2224
2225                if let Some(ref schema) = schema {
2226                    diagnostics.check(&mut packs, &mut dependencies.write().unwrap(), schema, game, &game_path, &[], check_ak_only_refs);
2227                }
2228
2229                info!("Checking diagnostics: done.");
2230
2231                CentralCommand::send_back(&sender, Response::Diagnostics(diagnostics));
2232            }
2233
2234            Command::DiagnosticsUpdate(mut diagnostics, path_types, check_ak_only_refs) => {
2235                let game_path = settings.path_buf(game.key());
2236
2237                if let Some(ref schema) = schema {
2238                    diagnostics.check(&mut packs, &mut dependencies.write().unwrap(), schema, game, &game_path, &path_types, check_ak_only_refs);
2239                }
2240
2241                info!("Checking diagnostics (update): done.");
2242
2243                CentralCommand::send_back(&sender, Response::Diagnostics(diagnostics));
2244            }
2245
2246            // In case we want to get the open PackFile's Settings...
2247            Command::GetPackSettings(pack_key) => {
2248                match packs.get(&pack_key) {
2249                    Some(pack) => CentralCommand::send_back(&sender, Response::PackSettings(pack.settings().clone())),
2250                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2251                }
2252            }
2253            Command::SetPackSettings(pack_key, pack_settings) => {
2254                match packs.get_mut(&pack_key) {
2255                    Some(pack) => {
2256                        pack.set_settings(pack_settings);
2257                        CentralCommand::send_back(&sender, Response::Success);
2258                    }
2259                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2260                }
2261            }
2262
2263            Command::GetMissingDefinitions(pack_key) => {
2264                match packs.get_mut(&pack_key) {
2265                    Some(pack) => {
2266                        // Test to see if every DB Table can be decoded. This is slow and only useful when
2267                        // a new patch lands and you want to know what tables you need to decode.
2268                        let mut counter = 0;
2269                        let mut table_list = String::new();
2270                        if let Some(ref schema) = schema {
2271                            let mut extra_data = DecodeableExtraData::default();
2272                            extra_data.set_schema(Some(schema));
2273                            let extra_data = Some(extra_data);
2274
2275                            let mut files = pack.files_by_type_mut(&[FileType::DB]);
2276                            files.sort_by_key(|file| file.path_in_container_raw().to_lowercase());
2277
2278                            for file in files {
2279                                if file.decode(&extra_data, false, false).is_err() && file.load().is_ok() {
2280                                    if let Ok(raw_data) = file.cached() {
2281                                        let mut reader = Cursor::new(raw_data);
2282                                        if let Ok((_, _, _, entry_count)) = DB::read_header(&mut reader) {
2283                                            if entry_count > 0 {
2284                                                counter += 1;
2285                                                table_list.push_str(&format!("{}, {:?}\n", counter, file.path_in_container_raw()))
2286                                            }
2287                                        }
2288                                    }
2289                                }
2290                            }
2291                        }
2292
2293                        // Try to save the file. And I mean "try". Someone seems to love crashing here...
2294                        let path = exe_path().join("missing_table_definitions.txt");
2295
2296                        if let Ok(file) = File::create(path) {
2297                            let mut file = BufWriter::new(file);
2298                            let _ = file.write_all(table_list.as_bytes());
2299                        }
2300                        CentralCommand::send_back(&sender, Response::Success);
2301                    }
2302                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2303                }
2304            }
2305
2306            // Ignore errors for now.
2307            Command::RebuildDependencies(rebuild_only_current_mod_dependencies) => {
2308                if schema.is_some() {
2309                    let game_path = settings.path_buf(game.key());
2310                    let dependencies_file_path = dependencies_cache_path().unwrap().join(game.dependencies_cache_file_name());
2311                    let file_path = if !rebuild_only_current_mod_dependencies { Some(&*dependencies_file_path) } else { None };
2312                    let pack_dependencies: Vec<_> = packs.values()
2313                        .flat_map(|pack| pack.dependencies().iter().map(|x| x.1.clone()))
2314                        .collect();
2315
2316                    let secondary_path = settings.path_buf(SECONDARY_PATH);
2317                    let _ = dependencies.write().unwrap().rebuild(&schema, &pack_dependencies, file_path, game, &game_path, &secondary_path);
2318                    let dependencies_info = DependenciesInfo::new(&dependencies.read().unwrap(), game.vanilla_db_table_name_logic());
2319                    CentralCommand::send_back(&sender, Response::DependenciesInfo(dependencies_info));
2320                } else {
2321                    CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string()));
2322                }
2323            },
2324
2325            Command::CascadeEdition(pack_key, table_name, definition, changes) => {
2326                match packs.get_mut(&pack_key) {
2327                    Some(pack) => {
2328                        let edited_paths = if let Some(ref schema) = schema {
2329                            changes.iter().flat_map(|(field, value_before, value_after)| {
2330                                DB::cascade_edition(pack, schema, &table_name, field, &definition, value_before, value_after)
2331                            }).collect::<Vec<_>>()
2332                        } else { vec![] };
2333
2334                        let packed_files_info = pack.files_by_paths(&edited_paths, false).into_par_iter().map(From::from).collect();
2335                        CentralCommand::send_back(&sender, Response::VecContainerPathVecRFileInfo(edited_paths, packed_files_info));
2336                    }
2337                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2338                }
2339            },
2340
2341            Command::GetTablesByTableName(pack_key, table_name) => {
2342                match packs.get(&pack_key) {
2343                    Some(pack) => {
2344                        let path = ContainerPath::Folder(format!("db/{table_name}/"));
2345                        let files = pack.files_by_type_and_paths(&[FileType::DB], &[path], true);
2346                        let paths = files.iter()
2347                            .map(|x| x.path_in_container_raw().to_owned())
2348                            .collect::<Vec<_>>();
2349
2350                        CentralCommand::send_back(&sender, Response::VecString(paths));
2351                    }
2352                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2353                }
2354            },
2355
2356            Command::AddKeysToKeyDeletes(pack_key, table_file_name, key_table_name, keys) => {
2357                match packs.get_mut(&pack_key) {
2358                    Some(pack) => {
2359                let path = ContainerPath::File(format!("db/{KEY_DELETES_TABLE_NAME}/{table_file_name}"));
2360                let mut files = pack.files_by_type_and_paths_mut(&[FileType::DB], &[path], true);
2361
2362                let mut cont_path = None;
2363                if let Some(file) = files.first_mut() {
2364                    if let Ok(RFileDecoded::DB(db)) = file.decoded_mut() {
2365                        for key in &keys {
2366                            let row = vec![
2367                                DecodedData::StringU8(key.to_owned()),
2368                                DecodedData::StringU8(key_table_name.to_owned()),
2369                            ];
2370
2371                            db.data_mut().push(row);
2372                        }
2373
2374                        cont_path = Some(file.path_in_container());
2375                    }
2376                }
2377
2378                CentralCommand::send_back(&sender, Response::OptionContainerPath(cont_path));
2379                    }
2380                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2381                }
2382            }
2383
2384            Command::GoToDefinition(pack_key, ref_table, mut ref_column, ref_data) => {
2385                let table_name = format!("{ref_table}_tables");
2386                let table_folder = format!("db/{table_name}");
2387                let mut found = false;
2388
2389                // Search first in the pack that sent the request (if still open), then in the rest of the open packs.
2390                let mut packs_to_search: Vec<&Pack> = Vec::with_capacity(packs.len());
2391                if let Some(pack) = packs.get(&pack_key) {
2392                    packs_to_search.push(pack);
2393                }
2394                packs_to_search.extend(packs.iter().filter(|kv| kv.0 != &pack_key).map(|kv| kv.1));
2395
2396                for pack in packs_to_search {
2397                    let packed_files = pack.files_by_path(&ContainerPath::Folder(table_folder.to_owned()), true);
2398                    for packed_file in &packed_files {
2399                        if let Ok(RFileDecoded::DB(data)) = packed_file.decoded() {
2400
2401                            // If the column is a loc column, we need to search in the first key column instead.
2402                            if data.definition().localised_fields().iter().any(|x| x.name() == ref_column) {
2403                                if let Some(first_key_index) = data.definition().localised_key_order().first() {
2404                                    if let Some(first_key_field) = data.definition().fields_processed().get(*first_key_index as usize) {
2405                                        ref_column = first_key_field.name().to_owned();
2406                                    }
2407                                }
2408                            }
2409
2410                            if let Some((column_index, row_index)) = data.table().rows_containing_data(&ref_column, &ref_data[0]) {
2411                                CentralCommand::send_back(&sender, Response::DataSourceStringUsizeUsize(DataSource::PackFile, packed_file.path_in_container_raw().to_owned(), column_index, row_index[0]));
2412                                found = true;
2413                                break;
2414                            }
2415                        }
2416                    }
2417
2418                    if found {
2419                        break;
2420                    }
2421                }
2422
2423                if !found {
2424                    if let Ok(packed_files) = dependencies.read().unwrap().db_data(&table_name, false, true) {
2425                        for packed_file in &packed_files {
2426                            if let Ok(RFileDecoded::DB(data)) = packed_file.decoded() {
2427
2428                                // If the column is a loc column, we need to search in the first key column instead.
2429                                if data.definition().localised_fields().iter().any(|x| x.name() == ref_column) {
2430                                    if let Some(first_key_index) = data.definition().localised_key_order().first() {
2431                                        if let Some(first_key_field) = data.definition().fields_processed().get(*first_key_index as usize) {
2432                                            ref_column = first_key_field.name().to_owned();
2433                                        }
2434                                    }
2435                                }
2436
2437                                if let Some((column_index, row_index)) = data.table().rows_containing_data(&ref_column, &ref_data[0]) {
2438                                    CentralCommand::send_back(&sender, Response::DataSourceStringUsizeUsize(DataSource::ParentFiles, packed_file.path_in_container_raw().to_owned(), column_index, row_index[0]));
2439                                    found = true;
2440                                    break;
2441                                }
2442                            }
2443                        }
2444                    }
2445                }
2446
2447                if !found {
2448                    if let Ok(packed_files) = dependencies.read().unwrap().db_data(&table_name, true, false) {
2449                        for packed_file in &packed_files {
2450                            if let Ok(RFileDecoded::DB(data)) = packed_file.decoded() {
2451
2452                                // If the column is a loc column, we need to search in the first key column instead.
2453                                if data.definition().localised_fields().iter().any(|x| x.name() == ref_column) {
2454                                    if let Some(first_key_index) = data.definition().localised_key_order().first() {
2455                                        if let Some(first_key_field) = data.definition().fields_processed().get(*first_key_index as usize) {
2456                                            ref_column = first_key_field.name().to_owned();
2457                                        }
2458                                    }
2459                                }
2460
2461                                if let Some((column_index, row_index)) = data.table().rows_containing_data(&ref_column, &ref_data[0]) {
2462                                    CentralCommand::send_back(&sender, Response::DataSourceStringUsizeUsize(DataSource::GameFiles, packed_file.path_in_container_raw().to_owned(), column_index, row_index[0]));
2463                                    found = true;
2464                                    break;
2465                                }
2466                            }
2467                        }
2468                    }
2469                }
2470
2471                if !found {
2472                    if let Some(data) = dependencies.read().unwrap().asskit_only_db_tables().get(&table_name) {
2473
2474                        // If the column is a loc column, we need to search in the first key column instead.
2475                        if data.definition().localised_fields().iter().any(|x| x.name() == ref_column) {
2476                            if let Some(first_key_index) = data.definition().localised_key_order().first() {
2477                                if let Some(first_key_field) = data.definition().fields_processed().get(*first_key_index as usize) {
2478                                    ref_column = first_key_field.name().to_owned();
2479                                }
2480                            }
2481                        }
2482
2483                        if let Some((column_index, row_index)) = data.table().rows_containing_data(&ref_column, &ref_data[0]) {
2484                            let path = format!("{}/ak_data", &table_folder);
2485                            CentralCommand::send_back(&sender, Response::DataSourceStringUsizeUsize(DataSource::AssKitFiles, path, column_index, row_index[0]));
2486                            found = true;
2487                        }
2488                    }
2489                }
2490
2491                if !found {
2492                    CentralCommand::send_back(&sender, Response::Error(tr("source_data_for_field_not_found")));
2493                }
2494            },
2495
2496            Command::SearchReferences(pack_key, reference_map, value) => {
2497                let paths = reference_map.keys().map(|x| ContainerPath::Folder(format!("db/{x}"))).collect::<Vec<ContainerPath>>();
2498                let Some(pack) = get_pack(&packs, &pack_key, &sender) else { continue 'background_loop; };
2499                let files = pack.files_by_paths(&paths, true);
2500
2501                let mut references: Vec<(DataSource, String, String, String, usize, usize)> = vec![];
2502
2503                // Pass for local tables. Tag each hit with the searched pack key so the UI can
2504                // open the right tab when several packs are open with files at the same path.
2505                for (table_name, columns) in &reference_map {
2506                    for file in &files {
2507                        if file.db_table_name_from_path().unwrap() == table_name {
2508                            if let Ok(RFileDecoded::DB(data)) = file.decoded() {
2509                                for column_name in columns {
2510                                    if let Some((column_index, row_indexes)) = data.table().rows_containing_data(column_name, &value) {
2511                                        for row_index in &row_indexes {
2512                                            references.push((DataSource::PackFile, pack_key.clone(), file.path_in_container_raw().to_owned(), column_name.to_owned(), column_index, *row_index));
2513                                        }
2514                                    }
2515                                }
2516                            }
2517                        }
2518                    }
2519                }
2520
2521                // Pass for parent tables. Pack key is empty here: parent/vanilla results are navigated
2522                // through the dependencies tree, which has a single root per source and doesn't need
2523                // pack-level disambiguation.
2524                for (table_name, columns) in &reference_map {
2525                        if let Ok(tables) = dependencies.read().unwrap().db_data(table_name, false, true) {
2526                        references.append(&mut tables.par_iter().map(|table| {
2527                            let mut references = vec![];
2528                            if let Ok(RFileDecoded::DB(data)) = table.decoded() {
2529                                for column_name in columns {
2530                                    if let Some((column_index, row_indexes)) = data.table().rows_containing_data(column_name, &value) {
2531                                        for row_index in &row_indexes {
2532                                            references.push((DataSource::ParentFiles, String::new(), table.path_in_container_raw().to_owned(), column_name.to_owned(), column_index, *row_index));
2533                                        }
2534                                    }
2535                                }
2536                            }
2537
2538                            references
2539                        }).flatten().collect());
2540                    }
2541                }
2542
2543                // Pass for vanilla tables.
2544                for (table_name, columns) in &reference_map {
2545                    if let Ok(tables) = dependencies.read().unwrap().db_data(table_name, true, false) {
2546                        references.append(&mut tables.par_iter().map(|table| {
2547                            let mut references = vec![];
2548                            if let Ok(RFileDecoded::DB(data)) = table.decoded() {
2549                                for column_name in columns {
2550                                    if let Some((column_index, row_indexes)) = data.table().rows_containing_data(column_name, &value) {
2551                                        for row_index in &row_indexes {
2552                                            references.push((DataSource::GameFiles, String::new(), table.path_in_container_raw().to_owned(), column_name.to_owned(), column_index, *row_index));
2553                                        }
2554                                    }
2555                                }
2556                            }
2557
2558                            references
2559                        }).flatten().collect());
2560                    }
2561                }
2562
2563                CentralCommand::send_back(&sender, Response::VecDataSourceStringStringStringUsizeUsize(references));
2564            },
2565
2566            Command::GoToLoc(pack_key, loc_key) => {
2567                let Some(pack) = get_pack(&packs, &pack_key, &sender) else { continue 'background_loop; };
2568                let packed_files = pack.files_by_type(&[FileType::Loc]);
2569                let mut found = false;
2570                for packed_file in &packed_files {
2571                    if let Ok(RFileDecoded::Loc(data)) = packed_file.decoded() {
2572                        if let Some((column_index, row_index)) = data.table().rows_containing_data("key", &loc_key) {
2573                            CentralCommand::send_back(&sender, Response::DataSourceStringUsizeUsize(DataSource::PackFile, packed_file.path_in_container_raw().to_owned(), column_index, row_index[0]));
2574                            found = true;
2575                            break;
2576                        }
2577                    }
2578                }
2579
2580                if !found {
2581                    if let Ok(packed_files) = dependencies.read().unwrap().loc_data(false, true) {
2582                        for packed_file in &packed_files {
2583                            if let Ok(RFileDecoded::Loc(data)) = packed_file.decoded() {
2584                                if let Some((column_index, row_index)) = data.table().rows_containing_data("key", &loc_key) {
2585                                    CentralCommand::send_back(&sender, Response::DataSourceStringUsizeUsize(DataSource::ParentFiles, packed_file.path_in_container_raw().to_owned(), column_index, row_index[0]));
2586                                    found = true;
2587                                    break;
2588                                }
2589                            }
2590                        }
2591                    }
2592                }
2593
2594                if !found {
2595                    if let Ok(packed_files) = dependencies.read().unwrap().loc_data(true, false) {
2596                        for packed_file in &packed_files {
2597                            if let Ok(RFileDecoded::Loc(data)) = packed_file.decoded() {
2598                                if let Some((column_index, row_index)) = data.table().rows_containing_data("key", &loc_key) {
2599                                    CentralCommand::send_back(&sender, Response::DataSourceStringUsizeUsize(DataSource::GameFiles, packed_file.path_in_container_raw().to_owned(), column_index, row_index[0]));
2600                                    found = true;
2601                                    break;
2602                                }
2603                            }
2604                        }
2605                    }
2606                }
2607
2608                if !found {
2609                    CentralCommand::send_back(&sender, Response::Error(tr("loc_key_not_found")));
2610                }
2611            },
2612
2613            Command::GetSourceDataFromLocKey(_pack_key, loc_key) => CentralCommand::send_back(&sender, Response::OptionStringStringVecString(dependencies.read().unwrap().loc_key_source(&loc_key))),
2614            Command::GetPackFileName(pack_key) => {
2615                match packs.get(&pack_key) {
2616                    Some(pack) => CentralCommand::send_back(&sender, Response::String(pack.disk_file_name())),
2617                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2618                }
2619            }
2620            Command::GetPackedFileRawData(pack_key, path) => {
2621                match packs.get_mut(&pack_key) {
2622                    Some(pack) => {
2623                let cf = pack.compression_format();
2624                match pack.files_mut().get_mut(&path) {
2625                    Some(ref mut rfile) => {
2626
2627                        // Make sure it's in memory.
2628                        match rfile.load() {
2629                            Ok(_) => match rfile.cached() {
2630                                Ok(data) => CentralCommand::send_back(&sender, Response::VecU8(data.to_vec())),
2631
2632                                // If we don't have binary data, it may be decoded. Encode it and return the binary data.
2633                                //
2634                                // NOTE: This fucks up the table decoder if the table was badly decoded.
2635                                Err(_) =>  {
2636                                    let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, cf, settings.bool("disable_uuid_regeneration_on_db_tables")));
2637                                    match rfile.encode(&extra_data, false, false, true) {
2638                                        Ok(data) => CentralCommand::send_back(&sender, Response::VecU8(data.unwrap())),
2639                                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
2640                                    }
2641                                },
2642                            },
2643                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
2644                        }
2645                    }
2646                    None => CentralCommand::send_back(&sender, Response::Error(anyhow!("This PackedFile no longer exists in the PackFile.").to_string())),
2647                }
2648                    }
2649                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2650                }
2651            },
2652
2653            Command::ImportDependenciesToOpenPackFile(pack_key, paths_by_data_source) => {
2654                match packs.get_mut(&pack_key) {
2655                    Some(pack) => {
2656                let mut added_paths = vec![];
2657                let mut not_added_paths = vec![];
2658
2659                let dependencies = dependencies.read().unwrap();
2660                for (data_source, paths) in &paths_by_data_source {
2661                    let files = match data_source {
2662                        DataSource::GameFiles => dependencies.files_by_path(paths, true, false, false),
2663                        DataSource::ParentFiles => dependencies.files_by_path(paths, false, true, false),
2664                        DataSource::AssKitFiles => HashMap::new(),
2665                        _ => {
2666                            CentralCommand::send_back(&sender, Response::Error("You can't import files from this source.".to_string()));
2667                            continue 'background_loop;
2668                        },
2669                    };
2670
2671                    for file in files.into_values() {
2672                        let file_path = file.path_in_container_raw().to_owned();
2673                        let mut file = file.clone();
2674                        let _ = file.guess_file_type();
2675                        if let Ok(Some(path)) = pack.insert(file) {
2676                            added_paths.push(path);
2677                        } else {
2678                            not_added_paths.push(file_path);
2679                        }
2680                    }
2681                }
2682
2683                // Once we're done with normal files, we process the ak ones.
2684                for (data_source, paths) in &paths_by_data_source {
2685                    match data_source {
2686                        DataSource::GameFiles | DataSource::ParentFiles => {},
2687                        DataSource::AssKitFiles => {
2688                            match &schema {
2689                                Some(ref schema) => {
2690                                    let mut files = vec![];
2691                                    for path in paths {
2692
2693                                        // We only have tables. If it's a folder, it's either a table folder, db or the root.
2694                                        match path {
2695                                            ContainerPath::Folder(path) => {
2696                                                let mut path = path.to_owned();
2697
2698                                                if path.ends_with('/') {
2699                                                    path.pop();
2700                                                }
2701
2702                                                let path_split = path.split('/').collect::<Vec<_>>();
2703                                                let table_name_logic = game.vanilla_db_table_name_logic();
2704
2705                                                // The db folder or the root folder directly.
2706                                                if path_split.len() == 1 {
2707                                                    let table_names = dependencies.asskit_only_db_tables().keys();
2708                                                    for table_name in table_names {
2709                                                        let table_file_name = match table_name_logic {
2710                                                            VanillaDBTableNameLogic::DefaultName(ref name) => name,
2711                                                            VanillaDBTableNameLogic::FolderName => table_name,
2712                                                        };
2713
2714                                                        match dependencies.import_from_ak(table_name, schema) {
2715                                                            Ok(table) => {
2716                                                                let mut path = path_split.to_vec();
2717                                                                path.push(table_file_name);
2718                                                                let mut path = path.join("/");
2719
2720                                                                if table_name.starts_with("ceo") {
2721                                                                    path = format!("ceo_{path}");
2722                                                                }
2723
2724                                                                let file = RFile::new_from_decoded(&RFileDecoded::DB(table), 0, &path);
2725                                                                files.push(file);
2726                                                            },
2727                                                            Err(_) => not_added_paths.push(path.clone()),
2728                                                        }
2729                                                    }
2730                                                }
2731
2732                                                // A table folder.
2733                                                else if path_split.len() == 2 {
2734
2735                                                    let table_name = path_split[1];
2736                                                    let table_file_name = match table_name_logic {
2737                                                        VanillaDBTableNameLogic::DefaultName(ref name) => name,
2738                                                        VanillaDBTableNameLogic::FolderName => table_name,
2739                                                    };
2740
2741                                                    match dependencies.import_from_ak(table_name, schema) {
2742                                                        Ok(table) => {
2743                                                            let mut path = path_split.to_vec();
2744                                                            path.push(table_file_name);
2745                                                            let mut path = path.join("/");
2746
2747                                                            if table_name.starts_with("ceo") {
2748                                                                path = format!("ceo_{path}");
2749                                                            }
2750
2751                                                            let file = RFile::new_from_decoded(&RFileDecoded::DB(table), 0, &path);
2752                                                            files.push(file);
2753                                                        },
2754                                                        Err(_) => not_added_paths.push(path.clone()),
2755                                                    }
2756                                                }
2757
2758                                                // Any other situation is an error.
2759                                                else {
2760                                                    CentralCommand::send_back(&sender, Response::Error("No idea how you were able to trigger this.".to_string()));
2761                                                    continue 'background_loop;
2762                                                }
2763
2764                                            }
2765                                            ContainerPath::File(path) => {
2766                                                let table_name = path.split('/').collect::<Vec<_>>()[1];
2767                                                match dependencies.import_from_ak(table_name, schema) {
2768                                                    Ok(table) => {
2769                                                        let file_path = if table_name.starts_with("ceo") {
2770                                                            format!("ceo_{}", path)
2771                                                        } else {
2772                                                            path.clone()
2773                                                        };
2774
2775                                                        let file = RFile::new_from_decoded(&RFileDecoded::DB(table), 0, &file_path);
2776                                                        files.push(file);
2777                                                    },
2778                                                    Err(_) => not_added_paths.push(path.clone()),
2779                                                }
2780                                            }
2781                                        }
2782                                    }
2783
2784                                    for file in files {
2785                                        if let Ok(Some(path)) = pack.insert(file) {
2786                                            added_paths.push(path);
2787                                        }
2788                                    }
2789                                },
2790                                None => {
2791                                    CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string()));
2792                                    continue 'background_loop;
2793                                }
2794                            }
2795                        },
2796                        _ => {
2797                            CentralCommand::send_back(&sender, Response::Error("You can't import files from this source.".to_string()));
2798                            continue 'background_loop;
2799                        },
2800                    }
2801                }
2802
2803                CentralCommand::send_back(&sender, Response::VecContainerPathVecString(added_paths, not_added_paths));
2804                    }
2805                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2806                }
2807            },
2808
2809            Command::GetRFilesFromAllSources(paths, force_lowercased_paths) => {
2810                let mut packed_files = HashMap::new();
2811                let dependencies = dependencies.read().unwrap();
2812
2813                // Get PackedFiles requested from the Parent Files.
2814                let mut packed_files_parent = HashMap::new();
2815                for (path, file) in dependencies.files_by_path(&paths, false, true, true) {
2816                    packed_files_parent.insert(if force_lowercased_paths { path.to_lowercase() } else { path }, file.clone());
2817                }
2818
2819                // Get PackedFiles requested from the Game Files.
2820                let mut packed_files_game = HashMap::new();
2821                for (path, file) in dependencies.files_by_path(&paths, true, false, true) {
2822                    packed_files_game.insert(if force_lowercased_paths { path.to_lowercase() } else { path }, file.clone());
2823                }
2824
2825                // Get PackedFiles requested from the AssKit Files.
2826                //let mut packed_files_asskit = HashMap::new();
2827                //if let Ok((packed_files_decoded, _)) = dependencies.get_packedfile_from_asskit_files(&paths) {
2828                //    for packed_file in packed_files_decoded {
2829                //        packed_files_asskit.insert(packed_file.get_path().to_vec(), packed_file);
2830                //    }
2831                //    packed_files.insert(DataSource::AssKitFiles, packed_files_asskit);
2832                //}
2833
2834                // Get PackedFiles requested from all currently open packs.
2835                let mut packed_files_packfile = HashMap::new();
2836                for pack in packs.values() {
2837                    for file in pack.files_by_paths(&paths, true) {
2838                        packed_files_packfile.insert(if force_lowercased_paths { file.path_in_container_raw().to_lowercase() } else { file.path_in_container_raw().to_owned() }, file.clone());
2839                    }
2840                }
2841
2842                packed_files.insert(DataSource::ParentFiles, packed_files_parent);
2843                packed_files.insert(DataSource::GameFiles, packed_files_game);
2844                packed_files.insert(DataSource::PackFile, packed_files_packfile);
2845
2846                // Return the full list of PackedFiles requested, split by source.
2847                CentralCommand::send_back(&sender, Response::HashMapDataSourceHashMapStringRFile(packed_files));
2848            },
2849
2850            Command::GetAnimPathsBySkeletonName(skeleton_name) => {
2851                let mut paths = HashSet::new();
2852                let mut dependencies = dependencies.write().unwrap();
2853
2854                // Get PackedFiles requested from the Parent Files.
2855                let mut packed_files_parent = HashSet::new();
2856                for (path, file) in dependencies.files_by_types_mut(&[FileType::Anim], false, true) {
2857                    if let Ok(Some(RFileDecoded::Anim(file))) = file.decode(&None, false, true) {
2858                        if file.skeleton_name() == &skeleton_name {
2859                            packed_files_parent.insert(path);
2860                        }
2861                    }
2862                }
2863
2864                // Get PackedFiles requested from the Game Files.
2865                let mut packed_files_game = HashSet::new();
2866                for (path, file) in dependencies.files_by_types_mut(&[FileType::Anim], true, false) {
2867                    if let Ok(Some(RFileDecoded::Anim(file))) = file.decode(&None, false, true) {
2868                        if file.skeleton_name() == &skeleton_name {
2869                            packed_files_game.insert(path);
2870                        }
2871                    }
2872                }
2873
2874                // Get PackedFiles requested from all currently open packs.
2875                let mut packed_files_packfile = HashSet::new();
2876                for pack in packs.values_mut() {
2877                    for file in pack.files_by_type_mut(&[FileType::Anim]) {
2878                        if let Ok(Some(RFileDecoded::Anim(anim_file))) = file.decode(&None, false, true) {
2879                            if anim_file.skeleton_name() == &skeleton_name {
2880                                packed_files_packfile.insert(file.path_in_container_raw().to_owned());
2881                            }
2882                        }
2883                    }
2884                }
2885
2886                paths.extend(packed_files_game);
2887                paths.extend(packed_files_parent);
2888                paths.extend(packed_files_packfile);
2889
2890                // Return the full list of PackedFiles requested, split by source.
2891                CentralCommand::send_back(&sender, Response::HashSetString(paths));
2892            },
2893
2894            Command::GetPackedFilesNamesStartingWitPathFromAllSources(path) => {
2895                let mut files: HashMap<DataSource, HashSet<ContainerPath>> = HashMap::new();
2896                let dependencies = dependencies.read().unwrap();
2897
2898                let parent_files = dependencies.files_by_path(std::slice::from_ref(&path), false, true, true);
2899                if !parent_files.is_empty() {
2900                    files.insert(DataSource::ParentFiles, parent_files.into_keys().map(ContainerPath::File).collect());
2901                }
2902
2903                let game_files = dependencies.files_by_path(std::slice::from_ref(&path), true, false, true);
2904                if !game_files.is_empty() {
2905                    files.insert(DataSource::GameFiles, game_files.into_keys().map(ContainerPath::File).collect());
2906                }
2907
2908                let mut local_file_paths = HashSet::new();
2909                for pack in packs.values() {
2910                    for file in pack.files_by_path(&path, true) {
2911                        local_file_paths.insert(file.path_in_container());
2912                    }
2913                }
2914                if !local_file_paths.is_empty() {
2915                    files.insert(DataSource::PackFile, local_file_paths);
2916                }
2917
2918                // Return the full list of PackedFile names requested, split by source.
2919                CentralCommand::send_back(&sender, Response::HashMapDataSourceHashSetContainerPath(files));
2920            },
2921
2922            Command::SavePackedFilesToPackFileAndClean(pack_key, files, optimize) => {
2923                match packs.get_mut(&pack_key) {
2924                    Some(pack) => {
2925                match &schema {
2926                    Some(ref schema) => {
2927
2928                        // We receive a list of edited PackedFiles. The UI is the one that takes care of editing them to have the data we want where we want.
2929                        // Also, the UI is responsible for naming them in case they're new. Here we grab them and directly add them into the PackFile.
2930                        let mut added_paths = vec![];
2931                        for file in files {
2932                            if let Ok(Some(path)) = pack.insert(file) {
2933                                added_paths.push(path);
2934                            }
2935                        }
2936
2937                        // Clean up duplicates from overwrites.
2938                        added_paths.sort();
2939                        added_paths.dedup();
2940
2941                        if optimize {
2942
2943                            // TODO: DO NOT CALL QT ON BACKEND.
2944                            let options = settings.optimizer_options();
2945
2946                            // Then, optimize the PackFile. This should remove any non-edited rows/files.
2947                            match pack.optimize(None, &mut dependencies.write().unwrap(), schema, game, &options) {
2948                                Ok((paths_to_delete, paths_to_add)) => {
2949                                    added_paths.extend(paths_to_add.into_iter()
2950                                        .map(ContainerPath::File)
2951                                        .collect::<Vec<_>>());
2952                                    CentralCommand::send_back(&sender, Response::VecContainerPathVecContainerPath(added_paths, paths_to_delete.into_iter()
2953                                        .map(ContainerPath::File)
2954                                        .collect()));
2955                                },
2956                                Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
2957                            }
2958                        } else {
2959                            CentralCommand::send_back(&sender, Response::VecContainerPathVecContainerPath(added_paths, vec![]));
2960                        }
2961                    },
2962                    None => CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string())),
2963                }
2964                    }
2965                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2966                }
2967            },
2968
2969            Command::NotesForPath(pack_key, path) => {
2970                match packs.get(&pack_key) {
2971                    Some(pack) => CentralCommand::send_back(&sender, Response::VecNote(pack.notes().notes_by_path(&path))),
2972                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2973                }
2974            }
2975            Command::AddNote(pack_key, note) => {
2976                match packs.get_mut(&pack_key) {
2977                    Some(pack) => CentralCommand::send_back(&sender, Response::Note(pack.notes_mut().add_note(note))),
2978                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2979                }
2980            }
2981            Command::DeleteNote(pack_key, path, id) => {
2982                match packs.get_mut(&pack_key) {
2983                    Some(pack) => {
2984                        pack.notes_mut().delete_note(&path, id);
2985                        CentralCommand::send_back(&sender, Response::Success);
2986                    }
2987                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
2988                }
2989            }
2990
2991            Command::SaveLocalSchemaPatch(patches) => {
2992                let path = table_patches_path().unwrap().join(game.schema_file_name());
2993                match Schema::save_patches(&patches, &path) {
2994                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
2995                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
2996                }
2997            }
2998            Command::RemoveLocalSchemaPatchesForTable(table_name) => {
2999                let path = table_patches_path().unwrap().join(game.schema_file_name());
3000                match Schema::remove_patches_for_table(&table_name, &path) {
3001                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3002                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3003                }
3004            }
3005            Command::RemoveLocalSchemaPatchesForTableAndField(table_name, field_name) => {
3006                let path = table_patches_path().unwrap().join(game.schema_file_name());
3007                match Schema::remove_patches_for_table_and_field(&table_name, &field_name, &path) {
3008                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3009                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3010                }
3011            }
3012            Command::ImportSchemaPatch(patch) => {
3013                match schema {
3014                    Some(ref mut schema) => {
3015                        Schema::add_patches_to_patch_set(schema.patches_mut(), &patch);
3016                        match schema.save(&schemas_path().unwrap().join(game.schema_file_name())) {
3017                            Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3018                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3019                        }
3020                    }
3021                    None => CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string())),
3022                }
3023            }
3024
3025            Command::GenerateMissingLocData(_pack_key) => {
3026                match dependencies.read().unwrap().generate_missing_loc_data(&mut packs) {
3027                    Ok(path) => CentralCommand::send_back(&sender, Response::VecContainerPath(path)),
3028                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3029                }
3030            }
3031
3032            Command::PackMap(pack_key, tile_maps, tiles) => {
3033                match schema {
3034                    Some(ref schema) => {
3035                        let mut dependencies = dependencies.write().unwrap();
3036                        let options = settings.optimizer_options();
3037                        match dependencies.add_tile_maps_and_tiles(&mut packs, Some(&pack_key), game, schema, options, tile_maps, tiles) {
3038                            Ok((paths_to_add, paths_to_delete)) => CentralCommand::send_back(&sender, Response::VecContainerPathVecContainerPath(paths_to_add, paths_to_delete)),
3039                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3040                        }
3041                    }
3042                    None => CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string())),
3043                }
3044            }
3045
3046            // Initialize the folder for a MyMod, including the folder structure it needs.
3047            Command::InitializeMyModFolder(mod_name, mod_game, sublime_support, vscode_support, git_support)  => {
3048                let mut mymod_path = settings.path_buf(MYMOD_BASE_PATH);
3049                if !mymod_path.is_dir() {
3050                    CentralCommand::send_back(&sender, Response::Error("MyMod path is not configured. Configure it in the settings and try again.".to_string()));
3051                    continue;
3052                }
3053
3054                mymod_path.push(&mod_game);
3055
3056                // Just in case the folder doesn't exist, we try to create it.
3057                if let Err(error) = DirBuilder::new().recursive(true).create(&mymod_path) {
3058                    CentralCommand::send_back(&sender, Response::Error(format!("Error while creating the MyMod's Game folder: {}.", error)));
3059                    continue;
3060                }
3061
3062                // We need to create another folder inside the game's folder with the name of the new "MyMod", to store extracted files.
3063                mymod_path.push(&mod_name);
3064                if let Err(error) = DirBuilder::new().recursive(true).create(&mymod_path) {
3065                    CentralCommand::send_back(&sender, Response::Error(format!("Error while creating the MyMod's Assets folder: {}.", error)));
3066                    continue;
3067                };
3068
3069                // Create a repo inside the MyMod's folder.
3070                if let Some(gitignore) = git_support {
3071                    let git_integration = GitIntegration::new(&mymod_path, "", "", "");
3072                    if let Err(error) = git_integration.init() {
3073                        CentralCommand::send_back(&sender, Response::Error(error.to_string()));
3074                        continue
3075                    }
3076
3077                    if let Err(error) = git_integration.add_gitignore(&gitignore) {
3078                        CentralCommand::send_back(&sender, Response::Error(error.to_string()));
3079                        continue
3080                    }
3081                }
3082
3083                // If the tw_autogen supports the game, create the vscode and sublime configs for lua mods.
3084                if sublime_support || vscode_support {
3085                    if let Ok(lua_autogen_folder) = lua_autogen_game_path(game) {
3086                        let lua_autogen_folder = lua_autogen_folder.to_string_lossy().to_string().replace('\\', "/");
3087
3088                        // VSCode support.
3089                        if vscode_support {
3090                            let mut vscode_config_path = mymod_path.to_owned();
3091                            vscode_config_path.push(".vscode");
3092
3093                            if let Err(error) = DirBuilder::new().recursive(true).create(&vscode_config_path) {
3094                                CentralCommand::send_back(&sender, Response::Error(format!("Error while creating the VSCode Config folder: {}.", error)));
3095                                continue;
3096                            };
3097
3098                            let mut vscode_extensions_path_file = vscode_config_path.to_owned();
3099                            vscode_extensions_path_file.push("extensions.json");
3100                            if let Ok(file) = File::create(vscode_extensions_path_file) {
3101                                let mut file = BufWriter::new(file);
3102                                let _ = file.write_all("
3103{
3104    \"recommendations\": [
3105        \"sumneko.lua\",
3106        \"formulahendry.code-runner\"
3107    ],
3108}".as_bytes());
3109                            }
3110                        }
3111
3112                        // Sublime support.
3113                        if sublime_support {
3114                            let mut sublime_config_path = mymod_path.to_owned();
3115                            sublime_config_path.push(format!("{mod_name}.sublime-project"));
3116                            if let Ok(file) = File::create(sublime_config_path) {
3117                                let mut file = BufWriter::new(file);
3118                                let _ = file.write_all("
3119{
3120    \"folders\":
3121    [
3122        {
3123            \"path\": \".\"
3124        }
3125    ]
3126}".to_string().as_bytes());
3127                            }
3128                        }
3129
3130                        // Generic lua support.
3131                        let mut luarc_config_path = mymod_path.to_owned();
3132                        luarc_config_path.push(".luarc.json");
3133
3134                        if let Ok(file) = File::create(luarc_config_path) {
3135                            let mut file = BufWriter::new(file);
3136                            let _ = file.write_all(format!("
3137{{
3138    \"workspace.library\": [
3139        \"{lua_autogen_folder}/global/\",
3140        \"{lua_autogen_folder}/campaign/\",
3141        \"{lua_autogen_folder}/frontend/\",
3142        \"{lua_autogen_folder}/battle/\"
3143    ],
3144    \"runtime.version\": \"Lua 5.1\",
3145    \"completion.autoRequire\": false,
3146    \"workspace.preloadFileSize\": 1500,
3147    \"workspace.ignoreSubmodules\": false,
3148    \"diagnostics.workspaceDelay\": 500,
3149    \"diagnostics.workspaceRate\": 40,
3150    \"diagnostics.disable\": [
3151        \"lowercase-global\",
3152        \"trailing-space\"
3153    ],
3154    \"hint.setType\": true,
3155    \"workspace.ignoreDir\": [
3156        \".vscode\",
3157        \".git\"
3158    ]
3159}}").as_bytes());
3160                        }
3161                    }
3162                }
3163
3164                // Return the name of the MyMod Pack.
3165                mymod_path.set_extension("pack");
3166                CentralCommand::send_back(&sender, Response::PathBuf(mymod_path));
3167            },
3168
3169            Command::LiveExport(pack_key) => {
3170                match packs.get_mut(&pack_key) {
3171                    Some(pack) => {
3172                        let game_path = settings.path_buf(game.key());
3173                        let disable_regen_table_guid = settings.bool("disable_uuid_regeneration_on_db_tables");
3174                        let keys_first = settings.bool("tables_use_old_column_order_for_tsv");
3175                        match pack.live_export(game, &game_path, disable_regen_table_guid, keys_first) {
3176                            Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3177                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3178                        }
3179                    }
3180                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
3181                }
3182            },
3183
3184            Command::SetPackOperationalMode(pack_key, mode) => {
3185                if packs.contains_key(&pack_key) {
3186                    pack_modes.insert(pack_key, mode);
3187                    CentralCommand::send_back(&sender, Response::Success);
3188                } else {
3189                    CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key)));
3190                }
3191            },
3192
3193            Command::GetPackOperationalMode(pack_key) => {
3194                let mode = pack_modes.get(&pack_key).cloned().unwrap_or(OperationalMode::Normal);
3195                CentralCommand::send_back(&sender, Response::OperationalMode(mode));
3196            },
3197
3198            Command::AddLineToPackIgnoredDiagnostics(pack_key, line) => {
3199                match packs.get_mut(&pack_key) {
3200                    Some(pack) => {
3201                        if let Some(diagnostics_ignored) = pack.settings_mut().settings_text_mut().get_mut("diagnostics_files_to_ignore") {
3202                            diagnostics_ignored.push_str(&line);
3203                        } else {
3204                            pack.settings_mut().settings_text_mut().insert("diagnostics_files_to_ignore".to_owned(), line);
3205                        }
3206                        CentralCommand::send_back(&sender, Response::Success);
3207                    }
3208                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
3209                }
3210            },
3211
3212            Command::UpdateEmpireAndNapoleonAK => {
3213                let sender = sender.clone();
3214                tokio::spawn(async move {
3215                    let result = tokio::task::spawn_blocking(|| {
3216                        match old_ak_files_path() {
3217                            Ok(local_path) => {
3218                                let git_integration = GitIntegration::new(&local_path, OLD_AK_REPO, OLD_AK_BRANCH, OLD_AK_REMOTE);
3219                                git_integration.update_repo().map(|_| ()).map_err(|e| e.into())
3220                            },
3221                            Err(error) => Err(error),
3222                        }
3223                    }).await.unwrap();
3224
3225                    match result {
3226                        Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3227                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3228                    }
3229                });
3230            }
3231
3232            Command::GetPackTranslation(pack_key, language) => {
3233                let game_key = game.key();
3234                match translations_local_path() {
3235                    Ok(local_path) => {
3236                        let mut base_english = HashMap::new();
3237                        let mut base_local_fixes = HashMap::new();
3238
3239                        match translations_remote_path() {
3240                            Ok(remote_path) => {
3241
3242                                let vanilla_loc_path = remote_path.join(format!("{}/{}", game.key(), VANILLA_LOC_NAME));
3243                                if let Ok(mut vanilla_loc) = RFile::tsv_import_from_path(&vanilla_loc_path, &None) {
3244                                    let _ = vanilla_loc.guess_file_type();
3245                                    if let Ok(RFileDecoded::Loc(vanilla_loc)) = vanilla_loc.decoded() {
3246
3247                                        // If we have a fixes file for the vanilla translation, apply it before everything else.
3248                                        let fixes_loc_path = remote_path.join(format!("{}/{}{}.tsv", game.key(), VANILLA_FIXES_NAME, language));
3249                                        if let Ok(mut fixes_loc) = RFile::tsv_import_from_path(&fixes_loc_path, &None) {
3250                                            let _ = fixes_loc.guess_file_type();
3251
3252                                            if let Ok(RFileDecoded::Loc(fixes_loc)) = fixes_loc.decoded() {
3253                                                base_local_fixes.extend(fixes_loc.data().iter().map(|x| (x[0].data_to_string().to_string(), x[1].data_to_string().to_string())).collect::<Vec<_>>());
3254                                            }
3255                                        }
3256
3257                                        base_english.extend(vanilla_loc.data().iter().map(|x| (x[0].data_to_string().to_string(), x[1].data_to_string().to_string())).collect::<Vec<_>>());
3258                                    }
3259                                }
3260
3261                                let dependencies = dependencies.read().unwrap();
3262                                let paths = vec![local_path, remote_path];
3263                                let Some(pack_ref) = get_pack(&packs, &pack_key, &sender) else { continue 'background_loop; };
3264                                match PackTranslation::new(&paths, pack_ref, game_key, &language, &dependencies, &base_english, &base_local_fixes) {
3265                                    Ok(tr) => CentralCommand::send_back(&sender, Response::PackTranslation(tr)),
3266                                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3267                                }
3268                            }
3269                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3270                        }
3271                    },
3272                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3273                }
3274            }
3275
3276            Command::UpdateTranslations => {
3277                let sender = sender.clone();
3278                tokio::spawn(async move {
3279                    let result = tokio::task::spawn_blocking(|| {
3280                        match translations_remote_path() {
3281                            Ok(local_path) => {
3282                                let git_integration = GitIntegration::new(&local_path, TRANSLATIONS_REPO, TRANSLATIONS_BRANCH, TRANSLATIONS_REMOTE);
3283                                git_integration.update_repo().map(|_| ()).map_err(|e| e.into())
3284                            },
3285                            Err(error) => Err(error),
3286                        }
3287                    }).await.unwrap();
3288
3289                    match result {
3290                        Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3291                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3292                    }
3293                });
3294            }
3295
3296            Command::BuildStarposGetCampaingIds(_pack_key) => {
3297                let ids = dependencies.read().unwrap().db_values_from_table_name_and_column_name(Some(&packs), "campaigns_tables", "campaign_name", true, true);
3298                CentralCommand::send_back(&sender, Response::HashSetString(ids));
3299            }
3300
3301            Command::BuildStarposCheckVictoryConditions(pack_key) => {
3302                let Some(pack_ref) = get_pack(&packs, &pack_key, &sender) else { continue 'background_loop; };
3303                if !GAMES_NEEDING_VICTORY_OBJECTIVES.contains(&game.key()) || (
3304                        GAMES_NEEDING_VICTORY_OBJECTIVES.contains(&game.key()) &&
3305                        pack_ref.file(VICTORY_OBJECTIVES_FILE_NAME, false).is_some()
3306                    ) {
3307                    CentralCommand::send_back(&sender, Response::Success);
3308                } else {
3309                    CentralCommand::send_back(&sender, Response::Error("Missing \"db/victory_objectives.txt\" file. Processing the startpos without this file will result in issues in campaign. Add the file to the pack and try again.".to_string()));
3310                }
3311            }
3312
3313            Command::BuildStarpos(pack_key, campaign_id, process_hlp_spd_data) => {
3314                let dependencies = dependencies.read().unwrap();
3315                let game_path = settings.path_buf(game.key());
3316
3317                // 3K needs two passes, one per startpos, and there are two per campaign.
3318                if game.key() == KEY_THREE_KINGDOMS {
3319                    match dependencies.build_starpos_pre(&mut packs, Some(&pack_key), game, &game_path, &campaign_id, process_hlp_spd_data, "historical") {
3320                        Ok(_) => match dependencies.build_starpos_pre(&mut packs, Some(&pack_key), game, &game_path, &campaign_id, false, "romance") {
3321                            Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3322                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3323                        }
3324                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3325                    }
3326                } else {
3327                    match dependencies.build_starpos_pre(&mut packs, Some(&pack_key), game, &game_path, &campaign_id, process_hlp_spd_data, "") {
3328                        Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3329                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3330                    }
3331                }
3332            }
3333
3334            Command::BuildStarposPost(pack_key, campaign_id, process_hlp_spd_data) => {
3335                let dependencies = dependencies.read().unwrap();
3336                let game_path = settings.path_buf(game.key());
3337                let asskit_path = Some(settings.path_buf(&(game.key().to_owned() + ASSEMBLY_KIT_SUFFIX)));
3338
3339                let sub_start_pos = if game.key() == KEY_THREE_KINGDOMS {
3340                    vec!["historical".to_owned(), "romance".to_owned()]
3341                } else {
3342                    vec![]
3343                };
3344
3345                match dependencies.build_starpos_post(&mut packs, Some(&pack_key), game, &game_path, asskit_path, &campaign_id, process_hlp_spd_data, false, &sub_start_pos) {
3346                    Ok(paths) => CentralCommand::send_back(&sender, Response::VecContainerPath(paths)),
3347                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3348                }
3349            },
3350
3351            Command::BuildStarposCleanup(pack_key, campaign_id, process_hlp_spd_data) => {
3352                let dependencies = dependencies.read().unwrap();
3353                let game_path = settings.path_buf(game.key());
3354                let asskit_path = Some(settings.path_buf(&(game.key().to_owned() + ASSEMBLY_KIT_SUFFIX)));
3355
3356                let sub_start_pos = if game.key() == KEY_THREE_KINGDOMS {
3357                    vec!["historical".to_owned(), "romance".to_owned()]
3358                } else {
3359                    vec![]
3360                };
3361
3362                match dependencies.build_starpos_post(&mut packs, Some(&pack_key), game, &game_path, asskit_path, &campaign_id, process_hlp_spd_data, true, &sub_start_pos) {
3363                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3364                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3365                }
3366            },
3367
3368            Command::BuildCeo(pack_key, akit_path, bob_exe_path) => {
3369                use std::process::{Command as SysCommand, Stdio};
3370                use std::time::Instant;
3371
3372                info!("[BuildCeo] handler entered: pack={pack_key}, akit={akit_path}, bob={bob_exe_path}");
3373                let akit_root = PathBuf::from(&akit_path);
3374                let bob_exe = PathBuf::from(&bob_exe_path);
3375                let bob_dir = match bob_exe.parent() {
3376                    Some(d) => d.to_path_buf(),
3377                    None => { CentralCommand::send_back(&sender, Response::Error("Invalid BOB path".into())); continue 'background_loop; }
3378                };
3379                let raw_db = akit_root.join(r"raw_data\db");
3380                let ceo_ccd = akit_root.join(r"working_data\campaigns\ceo_data.ccd");
3381
3382                // ── Step 1: Backup existing ceo_data.ccd ─────────────────────
3383                if ceo_ccd.exists() {
3384                    let bak = ceo_ccd.with_extension("ccd.bak1");
3385                    if let Err(e) = std::fs::copy(&ceo_ccd, &bak) {
3386                        CentralCommand::send_back(&sender, Response::Error(format!("Failed to backup ceo_data.ccd: {e}")));
3387                        continue 'background_loop;
3388                    }
3389                }
3390
3391                // ── Step 2: Backup raw_data/db/ceo_*.xml files ────────────────
3392                let mut xml_backups: Vec<(PathBuf, PathBuf)> = Vec::new();
3393                if raw_db.exists() {
3394                    match std::fs::read_dir(&raw_db) {
3395                        Ok(entries) => {
3396                            for entry in entries.filter_map(|e| e.ok()) {
3397                                let fname = entry.file_name();
3398                                let s = fname.to_string_lossy().to_lowercase();
3399                                if s.starts_with("ceo") && s.ends_with(".xml") {
3400                                    let orig = entry.path();
3401                                    let bak = orig.with_extension("xml.bak");
3402                                    if std::fs::copy(&orig, &bak).is_ok() {
3403                                        xml_backups.push((orig, bak));
3404                                    }
3405                                }
3406                            }
3407                        }
3408                        Err(e) => {
3409                            CentralCommand::send_back(&sender, Response::Error(format!("Failed to read raw_data/db: {e}")));
3410                            continue 'background_loop;
3411                        }
3412                    }
3413                }
3414
3415                // ── Step 3: Export CEO DB tables from pack → raw_data/db XML ──
3416                let pack_ref = match packs.get_mut(&pack_key) {
3417                    Some(p) => p,
3418                    None => {
3419                        for (orig, bak) in &xml_backups { let _ = std::fs::rename(bak, orig); }
3420                        CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {pack_key}")));
3421                        continue 'background_loop;
3422                    }
3423                };
3424
3425                // Only export the tables that BOB actually reads when building ceo_data.ccd.
3426                let ceo_allowed_folders: std::collections::HashSet<&str> = [
3427                    "ceo_active_permissions_tables",
3428                    "ceo_anti_ceo_pairs_tables",
3429                    "ceo_can_equip_requirements_tables",
3430                    "ceo_categories_tables",
3431                    "ceo_effect_list_to_effects_tables",
3432                    "ceo_effect_lists_tables",
3433                    "ceo_equipment_category_managers_tables",
3434                    "ceo_equipment_manager_all_possible_ceos_tables",
3435                    "ceo_equipment_manager_campaign_lookups_tables",
3436                    "ceo_equipment_manager_to_category_managers_tables",
3437                    "ceo_equipment_manager_types_tables",
3438                    "ceo_equipment_managers_tables",
3439                    "ceo_equipped_set_bonus_ceos_tables",
3440                    "ceo_equipped_set_bonus_effect_bundles_tables",
3441                    "ceo_equipped_set_bonuses_tables",
3442                    "ceo_equipped_set_bonuses_to_incident_junctions_tables",
3443                    "ceo_event_feed_categories_tables",
3444                    "ceo_group_ceos_tables",
3445                    "ceo_group_spawners_tables",
3446                    "ceo_groups_tables",
3447                    "ceo_initial_data_active_ceos_tables",
3448                    "ceo_initial_data_active_spawners_tables",
3449                    "ceo_initial_data_equipments_tables",
3450                    "ceo_initial_data_scripted_permissions_tables",
3451                    "ceo_initial_data_stages_tables",
3452                    "ceo_initial_data_to_stages_tables",
3453                    "ceo_initial_data_triggers_tables",
3454                    "ceo_initial_datas_tables",
3455                    "ceo_location_enums_tables",
3456                    "ceo_nodes_tables",
3457                    "ceo_permissions_groups_tables",
3458                    "ceo_permissions_tables",
3459                    "ceo_post_battle_loot_chances_tables",
3460                    "ceo_rarities_tables",
3461                    "ceo_scripted_permissions_tables",
3462                    "ceo_scripted_permissions_to_permissions_tables",
3463                    "ceo_set_items_tables",
3464                    "ceo_sets_tables",
3465                    "ceo_spawner_can_spawn_requirements_tables",
3466                    "ceo_spawners_tables",
3467                    "ceo_template_manager_all_possible_ceos_tables",
3468                    "ceo_template_manager_campaign_lookups_tables",
3469                    "ceo_template_manager_ceo_limits_tables",
3470                    "ceo_template_manager_ceo_spawn_limits_tables",
3471                    "ceo_template_manager_supported_categories_tables",
3472                    "ceo_template_manager_types_tables",
3473                    "ceo_template_managers_tables",
3474                    "ceo_threshold_nodes_tables",
3475                    "ceo_thresholds_tables",
3476                    "ceo_to_target_ceo_junctions_tables",
3477                    "ceo_to_target_factions_tables",
3478                    "ceo_to_target_junction_reasons_tables",
3479                    "ceo_to_target_province_junctions_tables",
3480                    "ceo_to_ui_display_junctions_tables",
3481                    "ceo_trigger_behaviour_enums_tables",
3482                    "ceo_trigger_target_requirements_tables",
3483                    "ceo_trigger_targets_tables",
3484                    "ceo_trigger_to_trigger_targets_tables",
3485                    "ceo_triggers_tables",
3486                    "ceos_tables",
3487                    "ceos_to_equipment_variants_tables",
3488                ].iter().copied().collect();
3489
3490                let ceo_table_paths: Vec<String> = pack_ref.files()
3491                    .keys()
3492                    .filter(|p| {
3493                        let mut parts = p.splitn(3, '/');
3494                        let prefix = parts.next().unwrap_or("");
3495                        if prefix != "db" && prefix != "ceo_db" {
3496                            return false;
3497                        }
3498                        parts.next()
3499                            .map(|folder| ceo_allowed_folders.contains(folder))
3500                            .unwrap_or(false)
3501                    })
3502                    .cloned()
3503                    .collect();
3504
3505                // Validate that we have CEO tables to export.
3506                if ceo_table_paths.is_empty() {
3507                    for (orig, bak) in &xml_backups { let _ = std::fs::rename(bak, orig); }
3508                    CentralCommand::send_back(&sender, Response::Error(
3509                        "No CEO tables found in the pack (looked in db/ and ceo_db/ folders). \
3510                         Import CEO tables from the Assembly Kit.".into()
3511                    ));
3512                    continue 'background_loop;
3513                }
3514
3515                // Check that the critical tables needed by BOB are present.
3516                let required_tables = [
3517                    "ceos_tables",
3518                    "ceo_nodes_tables",
3519                    "ceo_thresholds_tables",
3520                    "ceo_threshold_nodes_tables",
3521                    "ceo_initial_datas_tables",
3522                ];
3523                let present_folders: std::collections::HashSet<&str> = ceo_table_paths.iter()
3524                    .filter_map(|p| p.split('/').nth(1))
3525                    .collect();
3526                let missing: Vec<&&str> = required_tables.iter()
3527                    .filter(|t| !present_folders.contains(**t))
3528                    .collect();
3529                if !missing.is_empty() {
3530                    for (orig, bak) in &xml_backups { let _ = std::fs::rename(bak, orig); }
3531                    let missing_list = missing.iter().map(|t| format!("  - {}", t)).collect::<Vec<_>>().join("\n");
3532                    CentralCommand::send_back(&sender, Response::Error(
3533                        format!("The following required CEO tables are missing from the pack:\n{}\n\n\
3534                                 Import them from the Assembly Kit generate them.", missing_list)
3535                    ));
3536                    continue 'background_loop;
3537                }
3538
3539                let decode_extra = {
3540                    let mut d = DecodeableExtraData::default();
3541                    d.set_schema(schema.as_ref());
3542                    Some(d)
3543                };
3544                let mut export_errors: Vec<String> = Vec::new();
3545
3546                // Group table paths by their target XML file so multiple
3547                // db tables (e.g. data__, data__01) are combined into one XML.
3548                let mut xml_groups: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
3549                for table_path in &ceo_table_paths {
3550                    // "db/ceos_tables/data__" -> folder="ceos_tables" -> xml="ceos.xml"
3551                    let parts: Vec<&str> = table_path.split('/').collect();
3552                    if parts.len() < 2 { continue; }
3553                    let folder = parts[1];
3554                    let xml_name = if let Some(folder) = folder.strip_suffix("_tables") {
3555                        folder.to_owned() + ".xml"
3556                    } else {
3557                        folder.to_owned() + ".xml"
3558                    };
3559                    xml_groups.entry(xml_name).or_default().push(table_path.clone());
3560                }
3561
3562                for (xml_name, table_paths) in &xml_groups {
3563                    let xml_path = raw_db.join(xml_name);
3564                    let table_tag = xml_name.trim_end_matches(".xml");
3565                    let xsd_name = xml_name.replace(".xml", ".xsd");
3566
3567                    let mut xml = format!(
3568                        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n\
3569                         <dataroot xmlns:od=\"urn:schemas-microsoft-com:officedata\" \
3570                         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \
3571                         xsi:noNamespaceSchemaLocation=\"{xsd_name}\" \
3572                         export_time=\"\" revision=\"0\" export_branch=\"\" export_user=\"rpfm\">\r\n\
3573                         <edit_uuid>00000000-0000-0000-0000-000000000000</edit_uuid>\r\n"
3574                    );
3575
3576                    for table_path in table_paths {
3577                        let rfile = match pack_ref.files_mut().get_mut(table_path.as_str()) {
3578                            Some(f) => f,
3579                            None => continue,
3580                        };
3581
3582                        let _ = rfile.load();
3583                        let _ = rfile.decode(&decode_extra, true, false);
3584
3585                        let db_table = match rfile.decoded() {
3586                            Ok(RFileDecoded::DB(db)) => db.clone(),
3587                            _ => { export_errors.push(format!("Could not decode {table_path}")); continue; }
3588                        };
3589
3590                        let fields: Vec<_> = db_table.definition().fields_processed().to_vec();
3591
3592                        for row in db_table.data().iter() {
3593                            let mut field_pairs: Vec<(String, String)> = Vec::new();
3594                            for (field_def, value) in fields.iter().zip(row.iter()) {
3595                                let fname = field_def.name().to_owned();
3596                                let val_str = match value {
3597                                    DecodedData::Boolean(b) => if *b { "1".to_owned() } else { "0".to_owned() },
3598                                    DecodedData::I16(v) => v.to_string(),
3599                                    DecodedData::I32(v) => v.to_string(),
3600                                    DecodedData::I64(v) => v.to_string(),
3601                                    DecodedData::OptionalI16(v) => v.to_string(),
3602                                    DecodedData::OptionalI32(v) => v.to_string(),
3603                                    DecodedData::OptionalI64(v) => v.to_string(),
3604                                    DecodedData::F32(v) => v.to_string(),
3605                                    DecodedData::F64(v) => v.to_string(),
3606                                    DecodedData::StringU8(s) | DecodedData::StringU16(s) |
3607                                    DecodedData::OptionalStringU8(s) | DecodedData::OptionalStringU16(s) => s.clone(),
3608                                    DecodedData::ColourRGB(s) => s.clone(),
3609                                    _ => String::new(),
3610                                };
3611                                let escaped = val_str
3612                                    .replace('&', "&amp;")
3613                                    .replace('<', "&lt;")
3614                                    .replace('>', "&gt;")
3615                                    .replace('"', "&quot;");
3616                                field_pairs.push((fname, escaped));
3617                            }
3618                            field_pairs.sort_by(|a, b| a.0.cmp(&b.0));
3619
3620                            xml.push_str(&format!("<{table_tag}>\r\n"));
3621                            for (fname, val) in &field_pairs {
3622                                xml.push_str(&format!("<{fname}>{val}</{fname}>\r\n"));
3623                            }
3624                            xml.push_str(&format!("</{table_tag}>\r\n"));
3625                        }
3626                    }
3627
3628                    xml.push_str("</dataroot>\r\n");
3629
3630                    if let Err(e) = std::fs::write(&xml_path, xml.as_bytes()) {
3631                        export_errors.push(format!("Failed to write {}: {e}", xml_path.display()));
3632                    }
3633                }
3634
3635                if !export_errors.is_empty() {
3636                    for (orig, bak) in &xml_backups { let _ = std::fs::rename(bak, orig); }
3637                    CentralCommand::send_back(&sender, Response::Error(format!("Export errors:\n{}", export_errors.join("\n"))));
3638                    continue 'background_loop;
3639                }
3640
3641                // ── Step 4: Write BOB config and launch ───────────────────────
3642                let cfg_path = bob_dir.join("BOB/default_configuration.xml");
3643
3644                // The `binaries\BOB` config dir is absent on a fresh Assembly Kit that never ran
3645                // BOB; create it up front so the config write below doesn't fail with OS error 3.
3646                if let Some(cfg_dir) = cfg_path.parent() {
3647                    if let Err(e) = std::fs::create_dir_all(cfg_dir) {
3648                        for (orig, bak) in &xml_backups { let _ = std::fs::rename(bak, orig); }
3649                        CentralCommand::send_back(&sender, Response::Error(format!(
3650                            "Failed to create BOB config directory {}: {e}", cfg_dir.display())));
3651                        continue 'background_loop;
3652                    }
3653                }
3654
3655                // Backup any existing config so we can restore it after BOB runs.
3656                let cfg_backup = bob_dir.join("BOB/default_configuration.xml.rpfm_bak");
3657                let cfg_existed = cfg_path.exists();
3658                if cfg_existed {
3659                    if let Err(e) = std::fs::rename(&cfg_path, &cfg_backup) {
3660                        for (orig, bak) in &xml_backups { let _ = std::fs::rename(bak, orig); }
3661                        CentralCommand::send_back(&sender, Response::Error(format!(
3662                            "Failed to backup BOB config {}: {e}", cfg_path.display())));
3663                        continue 'background_loop;
3664                    }
3665                }
3666
3667                const BOB_CONFIG_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
3668                <bob_configuration><processors><processor>Campaign</processor></processors>
3669                <directories/><global_rules/><retail>0</retail><silent>1</silent>
3670                <get_latest>0</get_latest><connect_db>0</connect_db>
3671                <merge_for_checkin_mode>2</merge_for_checkin_mode>
3672                <selected_files><entry>&lt;working&gt;/campaigns/ceo_data.ccd</entry></selected_files>
3673                </bob_configuration>"#;
3674
3675                if let Err(e) = std::fs::write(&cfg_path, BOB_CONFIG_XML) {
3676                    // Restore backup before bailing.
3677                    if cfg_existed { let _ = std::fs::rename(&cfg_backup, &cfg_path); }
3678                    for (orig, bak) in &xml_backups { let _ = std::fs::rename(bak, orig); }
3679                    CentralCommand::send_back(&sender, Response::Error(format!(
3680                        "Failed to write BOB config {}: {e}", cfg_path.display())));
3681                    continue 'background_loop;
3682                }
3683
3684                info!("[BuildCeo] launching BOB: {} (cwd {})", bob_exe.display(), bob_dir.display());
3685                let bob_start = Instant::now();
3686
3687                // Use `.status()`, not `.output()`: it waits only on BOB's process handle, so it
3688                // returns the moment BOB exits without blocking to read captured output.
3689                let mut cmd = SysCommand::new(&bob_exe);
3690                cmd.current_dir(&bob_dir).stdin(Stdio::null());
3691
3692                let status = match cmd.status() {
3693                    Ok(s) => s,
3694                    Err(e) => {
3695                        let _ = std::fs::remove_file(&cfg_path);
3696                        if cfg_existed { let _ = std::fs::rename(&cfg_backup, &cfg_path); }
3697                        for (orig, bak) in &xml_backups { let _ = std::fs::rename(bak, orig); }
3698                        info!("[BuildCeo] BOB spawn failed after {:?}: {e}", bob_start.elapsed());
3699                        CentralCommand::send_back(&sender, Response::Error(format!("Failed to launch BOB: {e}")));
3700                        continue 'background_loop;
3701                    }
3702                };
3703
3704                info!("[BuildCeo] BOB exited after {:?}, exit={:?}", bob_start.elapsed(), status.code());
3705
3706                // Restore config regardless of BOB's result.
3707                let _ = std::fs::remove_file(&cfg_path);
3708                if cfg_existed { let _ = std::fs::rename(&cfg_backup, &cfg_path); }
3709
3710                // ── Step 5: Confirm ceo_data.ccd was produced ────────────────
3711                // BOB is single-process and we waited on its handle above, so its writes are
3712                // already flushed and visible: the file either exists now or never will.
3713                let found = ceo_ccd.exists();
3714                info!("[BuildCeo] ceo_data.ccd present: {found}");
3715
3716                // ── Step 6: Restore original ceo_*.xml files ─────────────────
3717                for (orig, bak) in &xml_backups {
3718                    let _ = std::fs::rename(bak, orig);
3719                }
3720
3721                // ── Step 7: Report result ─────────────────────────────────────
3722                if found {
3723                    CentralCommand::send_back(&sender, Response::Success);
3724                } else {
3725                    CentralCommand::send_back(&sender, Response::Error(format!(
3726                        "BOB finished (exit {:?}) but ceo_data.ccd was not generated. \
3727                         This usually means BOB hit an error in the exported tables.",
3728                        status.code()
3729                    )));
3730                }
3731            }
3732
3733            Command::BuildCeoPost(pack_key, akit_path) => {
3734                match packs.get_mut(&pack_key) {
3735                    Some(pack) => {
3736                        match build_ceo_post(pack, &akit_path) {
3737                            Ok(paths) => CentralCommand::send_back(&sender, Response::VecContainerPath(paths)),
3738                            Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3739                        }
3740                    }
3741                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {pack_key}"))),
3742                }
3743            }
3744
3745            Command::GetTraitCeos => {
3746                let deps = dependencies.read().unwrap();
3747                let trait_ceos = get_trait_ceos(&deps);
3748                CentralCommand::send_back(&sender, Response::VecStringTuples(trait_ceos));
3749            }
3750
3751            Command::BuildCeoEntries(pack_key, entries) => {
3752                let result = (|| -> Result<Vec<ContainerPath>> {
3753                    let schema = schema.as_ref()
3754                        .ok_or_else(|| anyhow!("No schema loaded for the current game."))?;
3755                    let pack = packs.get_mut(&pack_key)
3756                        .ok_or_else(|| anyhow!("Pack not found: {}", pack_key))?;
3757                    build_ceo_entries(pack, schema, &entries)
3758                })();
3759
3760                match result {
3761                    Ok(paths) => CentralCommand::send_back(&sender, Response::VecContainerPath(paths)),
3762                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3763                }
3764            }
3765
3766            Command::UpdateAnimIds(pack_key, starting_id, offset) => {
3767                match packs.get_mut(&pack_key) {
3768                    Some(pack) => {
3769                        match pack.update_anim_ids(game, starting_id, offset) {
3770                            Ok(paths) => CentralCommand::send_back(&sender, Response::VecContainerPath(paths)),
3771                            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3772                        }
3773                    }
3774                    None => CentralCommand::send_back(&sender, Response::Error(format!("Pack not found: {}", pack_key))),
3775                }
3776            }
3777
3778            Command::GetTablesFromDependencies(table_name) => {
3779                let dependencies = dependencies.read().unwrap();
3780                match dependencies.db_data(&table_name, true, true) {
3781                    Ok(files) => CentralCommand::send_back(&sender, Response::VecRFile(files.iter().map(|x| (**x).clone()).collect::<Vec<_>>())),
3782                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3783                }
3784            }
3785
3786            Command::ExportRigidToGltf(rigid, path) => {
3787                let mut dependencies = dependencies.write().unwrap();
3788                match gltf_from_rigid(&rigid, &mut dependencies) {
3789                    Ok(gltf) => match save_gltf_to_disk(&gltf, &PathBuf::from(path)) {
3790                        Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3791                        Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3792                    },
3793                    Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
3794                }
3795            }
3796
3797            // Settings IPC handlers - all settings are now managed locally in background_loop
3798            Command::SettingsGetBool(key) => {
3799                CentralCommand::send_back(&sender, Response::Bool(settings.bool(&key)));
3800            }
3801            Command::SettingsGetI32(key) => {
3802                CentralCommand::send_back(&sender, Response::I32(settings.i32(&key)));
3803            }
3804            Command::SettingsGetF32(key) => {
3805                CentralCommand::send_back(&sender, Response::F32(settings.f32(&key)));
3806            }
3807            Command::SettingsGetString(key) => {
3808                CentralCommand::send_back(&sender, Response::String(settings.string(&key)));
3809            }
3810            Command::SettingsGetPathBuf(key) => {
3811                CentralCommand::send_back(&sender, Response::PathBuf(settings.path_buf(&key)));
3812            }
3813            Command::SettingsGetVecString(key) => {
3814                CentralCommand::send_back(&sender, Response::VecString(settings.vec_string(&key)));
3815            }
3816            Command::SettingsGetVecRaw(key) => {
3817                CentralCommand::send_back(&sender, Response::VecU8(settings.raw_data(&key)));
3818            }
3819            Command::SettingsGetAll => {
3820                CentralCommand::send_back(&sender, Response::SettingsAll(SettingsSnapshot {
3821                    bool: settings.bool.clone(),
3822                    i32: settings.i32.clone(),
3823                    f32: settings.f32.clone(),
3824                    string: settings.string.clone(),
3825                    raw_data: settings.raw_data.clone(),
3826                    vec_string: settings.vec_string.clone(),
3827                }));
3828            }
3829            Command::SettingsSetBool(key, value) => {
3830                match settings.set_bool(&key, value) {
3831                    Ok(_) => {
3832                        match key.as_str() {
3833                            ENABLE_USAGE_TELEMETRY => rpfm_telemetry::set_usage_telemetry_enabled(value),
3834                            ENABLE_CRASH_REPORTS => rpfm_telemetry::set_crash_reports_enabled(value),
3835                            _ => {}
3836                        }
3837                        CentralCommand::send_back(&sender, Response::Success);
3838                    }
3839                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3840                }
3841            }
3842            Command::SettingsSetI32(key, value) => {
3843                match settings.set_i32(&key, value) {
3844                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3845                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3846                }
3847            }
3848            Command::SettingsSetF32(key, value) => {
3849                match settings.set_f32(&key, value) {
3850                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3851                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3852                }
3853            }
3854            Command::SettingsSetString(key, value) => {
3855                match settings.set_string(&key, &value) {
3856                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3857                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3858                }
3859            }
3860            Command::SettingsSetPathBuf(key, value) => {
3861                match settings.set_path_buf(&key, &value) {
3862                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3863                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3864                }
3865            }
3866            Command::SettingsSetVecString(key, value) => {
3867                match settings.set_vec_string(&key, &value) {
3868                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3869                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3870                }
3871            }
3872            Command::SettingsSetVecRaw(key, value) => {
3873                match settings.set_raw_data(&key, &value) {
3874                    Ok(_) => CentralCommand::send_back(&sender, Response::Success),
3875                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3876                }
3877            },
3878            Command::ConfigPath => {
3879                match config_path() {
3880                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path)),
3881                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3882                }
3883            },
3884            Command::AssemblyKitPath => {
3885                match settings.assembly_kit_path(game) {
3886                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path)),
3887                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3888                }
3889            },
3890            Command::BackupAutosavePath => {
3891                match backup_autosave_path() {
3892                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path)),
3893                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3894                }
3895            },
3896            Command::OldAkDataPath => {
3897                match old_ak_files_path() {
3898                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path)),
3899                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3900                }
3901            },
3902            Command::SchemasPath => {
3903                match schemas_path() {
3904                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path)),
3905                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3906                }
3907            },
3908            Command::TableProfilesPath => {
3909                match table_profiles_path() {
3910                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path)),
3911                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3912                }
3913            },
3914            Command::TranslationsLocalPath => {
3915                match translations_local_path() {
3916                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path)),
3917                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3918                }
3919            },
3920            Command::DependenciesCachePath => {
3921                match dependencies_cache_path() {
3922                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path)),
3923                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3924                }
3925            },
3926            Command::SettingsClearPath(path) => {
3927                match clear_config_path(&path) {
3928                    Ok(()) => CentralCommand::send_back(&sender, Response::Success),
3929                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3930                }
3931            },
3932            Command::CustomConfigPath => {
3933                match custom_config_path() {
3934                    Ok(path) => CentralCommand::send_back(&sender, Response::PathBuf(path.unwrap_or_default())),
3935                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3936                }
3937            },
3938            Command::SetCustomConfigPath(path) => {
3939                let path = if path.as_os_str().is_empty() { None } else { Some(path.as_path()) };
3940                match set_custom_config_path(path) {
3941                    Ok(()) => CentralCommand::send_back(&sender, Response::Success),
3942                    Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3943                }
3944            },
3945            Command::BackupSettings => {
3946                backup_settings = settings.clone();
3947                CentralCommand::send_back(&sender, Response::Success);
3948            }
3949            Command::ClearSettings => match Settings::init(true) {
3950                Ok(set) => {
3951                    settings = set;
3952                    CentralCommand::send_back(&sender, Response::Success);},
3953                Err(e) => CentralCommand::send_back(&sender, Response::Error(e.to_string())),
3954            },
3955            Command::RestoreBackupSettings => {
3956                settings = backup_settings.clone();
3957                CentralCommand::send_back(&sender, Response::Success);
3958            }
3959            Command::OptimizerOptions => CentralCommand::send_back(&sender, Response::OptimizerOptions(settings.optimizer_options())),
3960
3961            Command::IsSchemaLoaded => CentralCommand::send_back(&sender, Response::Bool(schema.is_some())),
3962            Command::DefinitionsByTableName(name) => match schema {
3963                Some(ref schema) => {
3964                    match schema.definitions_by_table_name(&name) {
3965                        Some(defs) => CentralCommand::send_back(&sender, Response::VecDefinition(defs.to_vec())),
3966                        None => CentralCommand::send_back(&sender, Response::VecDefinition(vec![])),
3967                    }
3968                },
3969                None => CentralCommand::send_back(&sender, Response::Error(anyhow!("There is no Schema for the Game Selected.").to_string())),
3970            },
3971            Command::ReferencingColumnsForDefinition(name, definition) => match schema {
3972                Some(ref schema) => CentralCommand::send_back(&sender, Response::HashMapStringHashMapStringVecString(schema.referencing_columns_for_table(&name, &definition))),
3973                None => CentralCommand::send_back(&sender, Response::Error("There is no Schema for the Game Selected.".to_string())),
3974            },
3975            Command::Schema => match &schema {
3976                Some(schema) => CentralCommand::send_back(&sender, Response::Schema(schema.clone())),
3977                None => CentralCommand::send_back(&sender, Response::Error("There is no Schema for the Game Selected.".to_string())),
3978            }
3979            Command::DefinitionByTableNameAndVersion(name, version) => match schema {
3980                Some(ref schema) => match schema.definition_by_name_and_version(&name, version) {
3981                    Some(def) => CentralCommand::send_back(&sender, Response::Definition(def.clone())),
3982                    None => CentralCommand::send_back(&sender, Response::Error(format!("No definition found for table '{}' with version {}.", name, version))),
3983                },
3984                None => CentralCommand::send_back(&sender, Response::Error("There is no Schema for the Game Selected.".to_string())),
3985            },
3986
3987            Command::DeleteDefinition(name, version) => {
3988                if let Some(ref mut schema) = schema {
3989                    schema.remove_definition(&name, version);
3990                }
3991                CentralCommand::send_back(&sender, Response::Success);
3992            }
3993
3994            Command::FieldsProcessed(definition) => {
3995                CentralCommand::send_back(&sender, Response::VecField(definition.fields_processed()));
3996            }
3997        }
3998    }
3999}
4000
4001/// Function to simplify logic for changing game selected.
4002fn load_schema(schema: &mut Option<Schema>, packs: &mut BTreeMap<String, Pack>, game: &GameInfo, settings: &Settings) {
4003
4004    // Before loading the schema, make sure we don't have tables with definitions from the current schema.
4005    for pack in packs.values_mut() {
4006        let cf = pack.compression_format();
4007        let mut files = pack.files_by_type_mut(&[FileType::DB]);
4008        let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, cf, settings.bool("disable_uuid_regeneration_on_db_tables")));
4009
4010        files.par_iter_mut().for_each(|file| {
4011            let _ = file.encode(&extra_data, true, true, false);
4012        });
4013    }
4014
4015    // Load the new schema.
4016    let schema_path = schemas_path().unwrap().join(game.schema_file_name());
4017    let local_patches_path = table_patches_path().unwrap().join(game.schema_file_name());
4018    *schema = Schema::load(&schema_path, Some(&local_patches_path)).ok();
4019
4020    // Re-decode all the tables in the open packs.
4021    if let Some(ref schema) = schema {
4022        for pack in packs.values_mut() {
4023            let mut files = pack.files_by_type_mut(&[FileType::DB]);
4024            let mut extra_data = DecodeableExtraData::default();
4025            extra_data.set_schema(Some(schema));
4026            let extra_data = Some(extra_data);
4027
4028            files.par_iter_mut().for_each(|file| {
4029                let _ = file.decode(&extra_data, true, false);
4030            });
4031        }
4032    }
4033}
4034
4035fn decode_and_send_file(file: &mut RFile, sender: &UnboundedSender<Response>, settings: &Settings, game: &GameInfo, schema: &Option<Schema>) {
4036    let mut extra_data = DecodeableExtraData::default();
4037    extra_data.set_schema(schema.as_ref());
4038    extra_data.set_game_info(Some(game));
4039
4040    // Do not attempt to decode these.
4041    let mut ignored_file_types = vec![
4042        FileType::Anim,
4043        FileType::BMD,
4044        FileType::BMDVegetation,
4045        FileType::Dat,
4046        FileType::Font,
4047        FileType::HlslCompiled,
4048        FileType::Pack,
4049        FileType::SoundBank,
4050        FileType::Unknown
4051    ];
4052
4053    // Do not even attempt to decode esf files if the editor is disabled.
4054    if !settings.bool("enable_esf_editor") {
4055        ignored_file_types.push(FileType::ESF);
4056    }
4057
4058    if ignored_file_types.contains(&file.file_type()) {
4059        return CentralCommand::send_back(sender, Response::Unknown);
4060    }
4061    let result = file.decode(&Some(extra_data), true, true).transpose().unwrap();
4062
4063    match result {
4064        Ok(RFileDecoded::AnimFragmentBattle(data)) => CentralCommand::send_back(sender, Response::AnimFragmentBattleRFileInfo(data, From::from(&*file))),
4065        Ok(RFileDecoded::AnimPack(data)) => CentralCommand::send_back(sender, Response::AnimPackRFileInfo(data.files().values().map(From::from).collect(), From::from(&*file))),
4066        Ok(RFileDecoded::AnimsTable(data)) => CentralCommand::send_back(sender, Response::AnimsTableRFileInfo(data, From::from(&*file))),
4067        Ok(RFileDecoded::Anim(_)) => CentralCommand::send_back(sender, Response::Unknown),
4068        Ok(RFileDecoded::Atlas(data)) => CentralCommand::send_back(sender, Response::AtlasRFileInfo(data, From::from(&*file))),
4069        Ok(RFileDecoded::Audio(data)) => CentralCommand::send_back(sender, Response::AudioRFileInfo(data, From::from(&*file))),
4070        Ok(RFileDecoded::BMD(_)) => CentralCommand::send_back(sender, Response::Unknown),
4071        Ok(RFileDecoded::BMDVegetation(_)) => CentralCommand::send_back(sender, Response::Unknown),
4072        Ok(RFileDecoded::Dat(_)) => CentralCommand::send_back(sender, Response::Unknown),
4073        Ok(RFileDecoded::DB(table)) => CentralCommand::send_back(sender, Response::DBRFileInfo(table, From::from(&*file))),
4074        Ok(RFileDecoded::ESF(data)) => CentralCommand::send_back(sender, Response::ESFRFileInfo(data, From::from(&*file))),
4075        Ok(RFileDecoded::Font(_)) => CentralCommand::send_back(sender, Response::Unknown),
4076        Ok(RFileDecoded::HlslCompiled(_)) => CentralCommand::send_back(sender, Response::Unknown),
4077        Ok(RFileDecoded::GroupFormations(data)) => CentralCommand::send_back(sender, Response::GroupFormationsRFileInfo(data, From::from(&*file))),
4078        Ok(RFileDecoded::Image(image)) => CentralCommand::send_back(sender, Response::ImageRFileInfo(image, From::from(&*file))),
4079        Ok(RFileDecoded::Loc(table)) => CentralCommand::send_back(sender, Response::LocRFileInfo(table, From::from(&*file))),
4080        Ok(RFileDecoded::MatchedCombat(data)) => CentralCommand::send_back(sender, Response::MatchedCombatRFileInfo(data, From::from(&*file))),
4081        Ok(RFileDecoded::Pack(_)) => CentralCommand::send_back(sender, Response::Unknown),
4082        Ok(RFileDecoded::PortraitSettings(data)) => CentralCommand::send_back(sender, Response::PortraitSettingsRFileInfo(data, From::from(&*file))),
4083        Ok(RFileDecoded::RigidModel(data)) => CentralCommand::send_back(sender, Response::RigidModelRFileInfo(data, From::from(&*file))),
4084        Ok(RFileDecoded::SoundBank(_)) => CentralCommand::send_back(sender, Response::Unknown),
4085        Ok(RFileDecoded::Text(text)) => CentralCommand::send_back(sender, Response::TextRFileInfo(text, From::from(&*file))),
4086        Ok(RFileDecoded::UIC(uic)) => CentralCommand::send_back(sender, Response::UICRFileInfo(uic, From::from(&*file))),
4087        Ok(RFileDecoded::UnitVariant(data)) => CentralCommand::send_back(sender, Response::UnitVariantRFileInfo(data, From::from(&*file))),
4088        Ok(RFileDecoded::Unknown(_)) => CentralCommand::send_back(sender, Response::Unknown),
4089        Ok(RFileDecoded::Video(data)) => CentralCommand::send_back(sender, Response::VideoInfoRFileInfo(From::from(&data), From::from(&*file))),
4090        Ok(RFileDecoded::VMD(data)) => CentralCommand::send_back(sender, Response::VMDRFileInfo(data, From::from(&*file))),
4091        Ok(RFileDecoded::WSModel(data)) => CentralCommand::send_back(sender, Response::WSModelRFileInfo(data, From::from(&*file))),
4092        Err(error) => CentralCommand::send_back(sender, Response::Error(error.to_string())),
4093    }
4094}
4095
4096/// In debug mode, this function returns the base folder of the repo.
4097/// In release mode, it returns the folder where the executable of the program is.
4098fn exe_path() -> PathBuf {
4099    if cfg!(debug_assertions) {
4100        std::env::current_dir().unwrap()
4101    } else {
4102        let mut path = std::env::current_exe().unwrap();
4103        path.pop();
4104        path
4105    }
4106}
4107
4108/// Spawns an async task that checks for git updates for the given repository configuration,
4109/// sending the result back through `sender`.
4110fn git_update_check(
4111    sender: UnboundedSender<Response>,
4112    path_fn: fn() -> Result<PathBuf>,
4113    repo: &'static str,
4114    branch: &'static str,
4115    remote: &'static str,
4116) {
4117    tokio::spawn(async move {
4118        let result = tokio::task::spawn_blocking(move || {
4119            match path_fn() {
4120                Ok(local_path) => {
4121                    let git_integration = GitIntegration::new(&local_path, repo, branch, remote);
4122                    git_integration.check_update().map_err(|e| e.into())
4123                }
4124                Err(error) => Err(error),
4125            }
4126        }).await.unwrap();
4127
4128        match result {
4129            Ok(response) => CentralCommand::send_back(&sender, Response::APIResponseGit(response)),
4130            Err(error) => CentralCommand::send_back(&sender, Response::Error(error.to_string())),
4131        }
4132    });
4133}
4134
4135/// Returns the interpreter command for a plugin script, based on its extension.
4136///
4137/// Returns `None` for unsupported extensions, which is also how we filter the scripts folder.
4138fn plugin_script_interpreter(path: &std::path::Path) -> Option<&'static str> {
4139    match path.extension().and_then(|extension| extension.to_str()) {
4140        Some("py") => Some("python"),
4141        Some("lua") => Some("lua"),
4142        _ => None,
4143    }
4144}
4145
4146/// Rebuilds the in-pack container path of a file extracted under `base_folder`.
4147///
4148/// The extraction keeps the in-pack structure, so the container path is just the file's path
4149/// relative to `base_folder` with forward slashes (the separator container paths use).
4150fn container_path_from_disk_path(disk_path: &std::path::Path, base_folder: &std::path::Path) -> String {
4151    disk_path.strip_prefix(base_folder)
4152        .unwrap_or(disk_path)
4153        .components()
4154        .filter_map(|component| match component {
4155            std::path::Component::Normal(part) => Some(part.to_string_lossy().to_string()),
4156            _ => None,
4157        })
4158        .collect::<Vec<_>>()
4159        .join("/")
4160}
4161
4162/// Delta-merges the decoded DB or Loc `sources` (same type/table) against the vanilla/parent baseline,
4163/// applying any already-known `resolutions`. See [`rpfm_extensions::merge`] for the merge rules.
4164fn delta_merge_files(sources: &[&RFile], merged_path: &str, dependencies: &Dependencies, resolutions: &[MergeResolution]) -> Result<DeltaMergeOutcome> {
4165    if sources.len() < 2 {
4166        return Err(anyhow!("Not enough tables provided to merge."));
4167    }
4168
4169    match sources[0].decoded()? {
4170        RFileDecoded::DB(_) => {
4171            let tables = sources.iter()
4172                .filter_map(|file| if let Ok(RFileDecoded::DB(table)) = file.decoded() { Some((file.path_in_container_raw(), table)) } else { None })
4173                .collect::<Vec<_>>();
4174
4175            let baseline = db_baseline(dependencies, tables[0].1.table_name());
4176            let (merged, conflicts) = delta_merge_db(&tables, baseline.as_ref(), resolutions)?;
4177            if conflicts.is_empty() {
4178                Ok(DeltaMergeOutcome::Merged(RFile::new_from_decoded(&RFileDecoded::DB(merged), current_time()?, merged_path)))
4179            } else {
4180                Ok(DeltaMergeOutcome::Conflicts(conflicts))
4181            }
4182        },
4183        RFileDecoded::Loc(_) => {
4184            let tables = sources.iter()
4185                .filter_map(|file| if let Ok(RFileDecoded::Loc(table)) = file.decoded() { Some((file.path_in_container_raw(), table)) } else { None })
4186                .collect::<Vec<_>>();
4187
4188            let baseline = loc_baseline(dependencies);
4189            let (merged, conflicts) = delta_merge_loc(&tables, baseline.as_ref(), resolutions)?;
4190            if conflicts.is_empty() {
4191                Ok(DeltaMergeOutcome::Merged(RFile::new_from_decoded(&RFileDecoded::Loc(merged), current_time()?, merged_path)))
4192            } else {
4193                Ok(DeltaMergeOutcome::Conflicts(conflicts))
4194            }
4195        },
4196        _ => Err(anyhow!("Delta merge is only supported for DB and Loc tables.")),
4197    }
4198}
4199
4200// TODO: what do we do with this?
4201fn tr(s: &str) -> String {
4202    s.to_owned()
4203}