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