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