Skip to main content

rpfm_server/
settings.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//! Persistent settings store and config-path helpers.
12//!
13//! Settings are kept in a single JSON file under the OS-specific config
14//! directory (resolved through [`directories::ProjectDirs`]) and exposed as a
15//! per-type [`Settings`] map: `bool`, `i32`, `f32`, `String`, raw bytes,
16//! and `Vec<String>`. Both the UI and the server side use the same
17//! [`rpfm_ipc::settings_keys`] constants when reading and writing, so a typo
18//! becomes a compile error rather than a silently-missed setting.
19
20use anyhow::{anyhow, Result};
21use directories::ProjectDirs;
22use ron::ser::{PrettyConfig, to_string_pretty};
23use serde_derive::{Serialize, Deserialize};
24
25use std::collections::HashMap;
26use std::io::{BufReader, BufWriter, Read, Write};
27use std::fs::{DirBuilder, File};
28use std::path::{Path, PathBuf};
29
30use rpfm_extensions::optimizer::OptimizerOptions;
31
32use rpfm_ipc::settings_keys::*;
33
34use rpfm_lib::error::RLibError;
35use rpfm_lib::games::{GameInfo, LUA_AUTOGEN_FOLDER, supported_games::*};
36use rpfm_lib::schema::{DefinitionPatch, SCHEMA_FOLDER};
37
38use crate::*;
39
40const SETTINGS_FILE_NAME: &str = "settings.json";
41
42/// File for storing the path to the user-chosen custom config folder.
43const CONFIG_REDIRECT_FILE_NAME: &str = "config_folder.txt";
44
45const DEPENDENCIES_FOLDER: &str = "dependencies";
46
47/// Folder under [`config_path`] where the user drops plugin scripts (`.py`/`.lua`).
48const SCRIPTS_FOLDER: &str = "scripts";
49
50const TABLE_PATCHES_FOLDER: &str = "table_patches";
51const TABLE_PROFILES_FOLDER: &str = "table_profiles";
52const TRANSLATIONS_LOCAL_FOLDER: &str = "translations_local";
53const TRANSLATIONS_REMOTE_FOLDER: &str = "translations_remote";
54
55//-------------------------------------------------------------------------------//
56//                                  Macros
57//-------------------------------------------------------------------------------//
58
59/// Macro to set a batch of settings in one go in an efficient way.
60///
61/// It expects a list of the following:
62///
63/// - $rtype: The setting's setter (set_bool, set_i32, etc.)
64/// - $id: The ID of the setting as a string literal.
65/// - $source: The expression to get the value.
66///
67/// You can add more settings by adding another 3 arguments to the macro.
68#[macro_export]
69macro_rules! set_batch {
70    ($( $rtype:ident, $id:literal, $source:expr), *) => {
71        {
72            let mut set = SETTINGS.write().unwrap();
73            set.set_block_write(true);
74            $(
75                let _ = set.$rtype($id, $source);
76            )*
77            set.set_block_write(false);
78            let _ = set.write();
79        }
80    };
81}
82
83//-------------------------------------------------------------------------------//
84//                              Enums & Structs
85//-------------------------------------------------------------------------------//
86
87/// Snapshot of every persisted setting.
88///
89/// Each typed sub-map keeps its own keys; lookups never cross types, so
90/// `settings.bool("X")` and `settings.i32("X")` are independent. Lookups for
91/// a missing key return the type's default (`false`, `0`, `""`, …).
92///
93/// Values mutate through the typed `set_*` / `initialize_*` methods. Each
94/// successful set persists to disk immediately, unless [`set_block_write`]
95/// is set to `true` (used for batch updates via the [`set_batch!`] macro).
96///
97/// [`set_block_write`]: Self::set_block_write
98#[derive(Clone, Debug, Default, Serialize, Deserialize)]
99pub struct Settings {
100
101    /// When `true`, [`Self::write`] becomes a no-op. Used by [`set_batch!`]
102    /// to coalesce many updates into a single disk write.
103    #[serde(skip_serializing, skip_deserializing)]
104    pub block_write: bool,
105
106    /// Boolean settings.
107    pub bool: HashMap<String, bool>,
108    /// Signed 32-bit integer settings.
109    pub i32: HashMap<String, i32>,
110    /// 32-bit floating-point settings.
111    pub f32: HashMap<String, f32>,
112    /// String settings (also used for path-shaped strings; see
113    /// [`Self::path_buf`] for `PathBuf` access on top of the same map).
114    pub string: HashMap<String, String>,
115    /// Opaque byte-blob settings.
116    pub raw_data: HashMap<String, Vec<u8>>,
117    /// Lists-of-strings settings.
118    pub vec_string: HashMap<String, Vec<String>>
119}
120
121//-------------------------------------------------------------------------------//
122//                         Settings implementation
123//-------------------------------------------------------------------------------//
124
125impl Settings {
126
127    /// Build a fresh `Settings` instance, loading from disk and applying
128    /// per-key default initialisation.
129    ///
130    /// If `as_new` is `true` the on-disk file is ignored and a fully default
131    /// settings struct is returned (still applying the per-key defaults).
132    /// If reading the on-disk file fails, the broken file is backed up to
133    /// `settings.json.bak` and defaults are used — protects against sporadic
134    /// read failures silently resetting every setting.
135    pub fn init(as_new: bool) -> Result<Self> {
136        let mut settings = if !as_new {
137            match Settings::read() {
138                Ok(settings) => settings,
139                Err(error) => {
140
141                    // On read failure, try to backup the old settings file before overwriting it with defaults.
142                    // This protects against sporadic read failures that would otherwise silently reset all settings.
143                    if let Ok(config) = config_path() {
144                        let settings_path = config.join(SETTINGS_FILE_NAME);
145                        if settings_path.exists() {
146                            let backup_path = config.join(format!("{SETTINGS_FILE_NAME}.bak"));
147                            let _ = std::fs::copy(&settings_path, &backup_path);
148                        }
149                    }
150
151                    rpfm_telemetry::warn!("Failed to read settings file, using defaults. Error: {error}");
152                    Settings::default()
153                }
154            }
155        } else {
156            Settings::default()
157        };
158
159        settings.set_block_write(true);
160
161        settings.initialize_string(MYMOD_BASE_PATH, "");
162        settings.initialize_string(SECONDARY_PATH, "");
163
164        let supported_games = SupportedGames::default();
165        for game in &supported_games.games() {
166            let game_key = game.key();
167
168            // Fix unsanitized paths.
169            let current_path = settings.string(game_key);
170            if current_path.is_empty() {
171                if current_path.contains("\\") {
172                    let _ = settings.set_string(game_key, &current_path.replace("\\", "/"));
173                }
174
175                let game_path = if let Ok(Some(game_path)) = game.find_game_install_location() {
176                    game_path.to_string_lossy().replace("\\", "/")
177                } else {
178                    String::new()
179                };
180
181                // If we got a path and we don't have it saved yet, save it automatically.
182                if !game_path.is_empty() {
183                    let _ = settings.set_string(game_key, &game_path);
184                } else {
185                    settings.initialize_string(game_key, &game_path);
186                }
187            }
188
189            if game_key != KEY_EMPIRE &&
190                game_key != KEY_NAPOLEON &&
191                game_key != KEY_ARENA {
192
193                // If we got a path and we don't have it saved yet, save it automatically.
194                let ak_key = game_key.to_owned() + ASSEMBLY_KIT_SUFFIX;
195                let current_path = settings.string(&ak_key);
196
197                if current_path.is_empty() {
198                    let ak_path = if let Ok(Some(ak_path)) = game.find_assembly_kit_install_location() {
199                        ak_path.join("assembly_kit").to_string_lossy().replace("\\", "/")
200                    } else {
201                        String::new()
202                    };
203
204                    // Fix unsanitized paths.
205                    if current_path.contains("\\") {
206                        let _ = settings.set_string(&ak_key, &current_path.replace("\\", "/"));
207                    }
208
209                    // Ignore shogun 2, as that one is a zip.
210                    if !ak_path.is_empty() && game_key != KEY_SHOGUN_2 {
211                        let _ = settings.set_string(&ak_key, &ak_path);
212                    } else {
213                        settings.initialize_string(&ak_key, &ak_path);
214                    }
215                }
216            }
217        }
218
219        // Hidden setting.
220        settings.initialize_bool(IMPORT_FROM_QT, false);
221
222        // General Settings.
223        settings.initialize_string(DEFAULT_GAME, KEY_WARHAMMER_3);
224        settings.initialize_string(LANGUAGE, "English_en");
225        settings.initialize_string(THEME, THEME_OS);
226        //settings.initialize_string(UPDATE_CHANNEL, STABLE);
227        settings.initialize_i32(AUTOSAVE_AMOUNT, 10);
228        settings.initialize_i32(AUTOSAVE_INTERVAL, 5);
229
230        /*
231        let font = QApplication::font();
232        let font_name = font.family().to_std_string();
233        let font_size = font.point_size();
234        settings.initialize_string("font_name", &font_name);
235        settings.initialize_i32("font_size", font_size);
236        settings.initialize_string("original_font_name", &font_name);
237        settings.initialize_i32("original_font_size", font_size);
238    */
239        // UI Settings.
240        settings.initialize_bool(START_MAXIMIZED, false);
241        settings.initialize_bool(ALLOW_EDITING_OF_CA_PACKFILES, false);
242        settings.initialize_bool(CHECK_UPDATES_ON_START, true);
243        settings.initialize_bool(CHECK_SCHEMA_UPDATES_ON_START, true);
244        settings.initialize_bool(CHECK_LUA_AUTOGEN_UPDATES_ON_START, true);
245        settings.initialize_bool(CHECK_OLD_AK_UPDATES_ON_START, true);
246        settings.initialize_bool(USE_LAZY_LOADING, true);
247        settings.initialize_bool(DISABLE_UUID_REGENERATION_ON_DB_TABLES, true);
248        settings.initialize_bool(PACKFILE_TREEVIEW_RESIZE_TO_FIT, false);
249        settings.initialize_bool(EXPAND_TREEVIEW_WHEN_ADDING_ITEMS, true);
250        settings.initialize_bool(USE_RIGHT_SIZE_MARKERS, false);
251        settings.initialize_bool(DISABLE_FILE_PREVIEWS, false);
252        settings.initialize_bool(INCLUDE_BASE_FOLDER_ON_ADD_FROM_FOLDER, true);
253        settings.initialize_bool(DELETE_EMPTY_FOLDERS_ON_DELETE, true);
254        settings.initialize_bool(AUTOSAVE_FOLDER_SIZE_WARNING_TRIGGERED, false);
255        settings.initialize_bool(IGNORE_GAME_FILES_IN_AK, false);
256        settings.initialize_bool(ENABLE_MULTIFOLDER_FILEPICKER, false);
257        settings.initialize_bool(ENABLE_PACK_CONTENTS_DRAG_AND_DROP, true);
258        settings.initialize_bool(CLEAN_UI, false);
259        settings.initialize_bool(SINGLE_PACK_MODE, true);
260        settings.initialize_bool(GLOBAL_SEARCH_COLLAPSE_RESULTS, false);
261
262        // Table Settings.
263        settings.initialize_bool(ADJUST_COLUMNS_TO_CONTENT, true);
264        settings.initialize_bool(EXTEND_LAST_COLUMN_ON_TABLES, true);
265        settings.initialize_bool(DISABLE_COMBOS_ON_TABLES, false);
266        settings.initialize_bool(TIGHT_TABLE_MODE, false);
267        settings.initialize_bool(TABLE_RESIZE_ON_EDIT, false);
268        settings.initialize_bool(TABLES_USE_OLD_COLUMN_ORDER, true);
269        settings.initialize_bool(TABLES_USE_OLD_COLUMN_ORDER_FOR_TSV, true);
270        settings.initialize_bool(ENABLE_LOOKUPS, true);
271        settings.initialize_bool(ENABLE_ICONS, true);
272        settings.initialize_bool(ENABLE_DIFF_MARKERS, true);
273        settings.initialize_bool(HIDE_UNUSED_COLUMNS, true);
274        settings.initialize_bool(SHOW_TABLE_TOOLBAR, false);
275
276        // Debug Settings.
277        settings.initialize_bool(CHECK_FOR_MISSING_TABLE_DEFINITIONS, false);
278        settings.initialize_bool(ENABLE_DEBUG_MENU, false);
279        settings.initialize_bool(ENABLE_UNIT_EDITOR, false);
280        settings.initialize_bool(ENABLE_ESF_EDITOR, false);
281        settings.initialize_bool(USE_DEBUG_VIEW_UNIT_VARIANT, false);
282        settings.initialize_bool(ENABLE_RENDERER, true);
283
284        // Diagnostics Settings.
285        settings.initialize_bool(DIAGNOSTICS_TRIGGER_ON_OPEN, true);
286        settings.initialize_bool(DIAGNOSTICS_TRIGGER_ON_TABLE_EDIT, true);
287
288        // Telemetry settings: opt-out, both default to on. Users can disable either in the preferences.
289        settings.initialize_bool(ENABLE_USAGE_TELEMETRY, true);
290        settings.initialize_bool(ENABLE_CRASH_REPORTS, true);
291
292        // Anonymous id to track distinct installs across sessions.
293        if settings.string(ANONYMOUS_TELEMETRY_ID).is_empty() {
294            let _ = settings.set_string(ANONYMOUS_TELEMETRY_ID, &uuid::Uuid::new_v4().to_string());
295        }
296
297        settings.initialize_string(AI_API_URL, "https://api.openai.com/v1/chat/completions");
298        settings.initialize_string(AI_API_KEY, "");
299        settings.initialize_string(AI_MODEL, "gpt-4o-mini");
300        settings.initialize_string(DEEPL_API_KEY, "");
301
302        settings.initialize_vec_string(RECENT_FILE_LIST, &[]);
303
304        // Colours.
305    /*    let q_settings = qt_core::QSettings::new();
306        set_setting_if_new_string(&q_settings, "colour_light_table_added", "#87ca00");
307        set_setting_if_new_string(&q_settings, "colour_light_table_modified", "#e67e22");
308        set_setting_if_new_string(&q_settings, "colour_light_diagnostic_error", "#ff0000");
309        set_setting_if_new_string(&q_settings, "colour_light_diagnostic_warning", "#bebe00");
310        set_setting_if_new_string(&q_settings, "colour_light_diagnostic_info", "#55aaff");
311        set_setting_if_new_string(&q_settings, "colour_dark_table_added", "#00ff00");
312        set_setting_if_new_string(&q_settings, "colour_dark_table_modified", "#e67e22");
313        set_setting_if_new_string(&q_settings, "colour_dark_diagnostic_error", "#ff0000");
314        set_setting_if_new_string(&q_settings, "colour_dark_diagnostic_warning", "#cece67");
315        set_setting_if_new_string(&q_settings, "colour_dark_diagnostic_info", "#55aaff");
316        q_settings.sync();*/
317
318        // Optimizer settings.
319        let opt = OptimizerOptions::default();
320        settings.initialize_bool(PACK_REMOVE_ITM_FILES, *opt.pack_remove_itm_files());
321        settings.initialize_bool(PACK_APPLY_COMPRESSION, *opt.pack_apply_compression());
322        settings.initialize_bool(PACK_REMOVE_DUPLICATED_FILES, *opt.pack_remove_duplicated_files());
323        settings.initialize_bool(DB_IMPORT_DATACORES_INTO_TWAD_KEY_DELETES, *opt.db_import_datacores_into_twad_key_deletes());
324        settings.initialize_bool(DB_OPTIMIZE_DATACORED_TABLES, *opt.db_optimize_datacored_tables());
325        settings.initialize_bool(TABLE_REMOVE_DUPLICATED_ENTRIES, *opt.table_remove_duplicated_entries());
326        settings.initialize_bool(TABLE_REMOVE_ITM_ENTRIES, *opt.table_remove_itm_entries());
327        settings.initialize_bool(TABLE_REMOVE_ITNR_ENTRIES, *opt.table_remove_itnr_entries());
328        settings.initialize_bool(TABLE_REMOVE_EMPTY_FILE, *opt.table_remove_empty_file());
329        settings.initialize_bool(TEXT_REMOVE_UNUSED_XML_MAP_FOLDERS, *opt.text_remove_unused_xml_map_folders());
330        settings.initialize_bool(TEXT_REMOVE_UNUSED_XML_PREFAB_FOLDER, *opt.text_remove_unused_xml_prefab_folder());
331        settings.initialize_bool(TEXT_REMOVE_AGF_FILES, *opt.text_remove_agf_files());
332        settings.initialize_bool(TEXT_REMOVE_MODEL_STATISTICS_FILES, *opt.text_remove_model_statistics_files());
333        settings.initialize_bool(PTS_REMOVE_UNUSED_ART_SETS, *opt.pts_remove_unused_art_sets());
334        settings.initialize_bool(PTS_REMOVE_UNUSED_VARIANTS, *opt.pts_remove_unused_variants());
335        settings.initialize_bool(PTS_REMOVE_EMPTY_MASKS, *opt.pts_remove_empty_masks());
336        settings.initialize_bool(PTS_REMOVE_EMPTY_FILE, *opt.pts_remove_empty_file());
337
338        settings.set_block_write(false);
339
340        settings.write()?;
341
342        Ok(settings)
343    }
344
345    /// Read the on-disk settings file (`settings.json` under [`config_path`]).
346    ///
347    /// Errors if the file is missing or cannot be parsed as JSON. Most callers
348    /// want [`Self::init`] instead, which falls back to defaults on failure.
349    pub fn read() -> Result<Self> {
350        let mut data = vec![];
351        let mut file = BufReader::new(File::open(config_path()?.join(SETTINGS_FILE_NAME))?);
352        file.read_to_end(&mut data)?;
353
354        serde_json::from_slice(&data).map_err(From::from)
355    }
356
357    /// Writes the settings to disk. Does nothing if the block write flag is set.
358    pub fn write(&self) -> Result<()> {
359        if self.block_write {
360            return Ok(());
361        }
362
363        let mut file = BufWriter::new(File::create(config_path()?.join(SETTINGS_FILE_NAME))?);
364        file.write_all(serde_json::to_string_pretty(self)?.as_bytes()).map_err(From::from)
365    }
366
367    /// Disables save to disk when storing a setting. For batch operations.
368    pub fn set_block_write(&mut self, status: bool) {
369        self.block_write = status;
370    }
371
372    /// Read a `bool` setting; returns `false` if `setting` isn't set.
373    pub fn bool(&self, setting: &str) -> bool {
374        self.bool.get(setting).copied().unwrap_or_default()
375    }
376
377    /// Read an `i32` setting; returns `0` if `setting` isn't set.
378    pub fn i32(&self, setting: &str) -> i32 {
379        self.i32.get(setting).copied().unwrap_or_default()
380    }
381
382    /// Read an `f32` setting; returns `0.0` if `setting` isn't set.
383    pub fn f32(&self, setting: &str) -> f32 {
384        self.f32.get(setting).copied().unwrap_or_default()
385    }
386
387    /// Read a `String` setting; returns an empty string if `setting` isn't set.
388    pub fn string(&self, setting: &str) -> String {
389        self.string.get(setting).map(|x| x.to_owned()).unwrap_or_default()
390    }
391
392    /// Read a path-shaped string setting as a `PathBuf`; returns an empty
393    /// `PathBuf` if `setting` isn't set. Backed by the same map as
394    /// [`Self::string`].
395    pub fn path_buf(&self, setting: &str) -> PathBuf {
396        self.string.get(setting).map(PathBuf::from).unwrap_or_default()
397    }
398
399    /// Read a raw byte-blob setting; returns an empty `Vec` if `setting` isn't set.
400    pub fn raw_data(&self, setting: &str) -> Vec<u8> {
401        self.raw_data.get(setting).map(|x| x.to_vec()).unwrap_or_default()
402    }
403
404    /// Read a `Vec<String>` setting; returns an empty `Vec` if `setting` isn't set.
405    pub fn vec_string(&self, setting: &str) -> Vec<String> {
406        self.vec_string.get(setting).map(|x| x.to_vec()).unwrap_or_default()
407    }
408
409    /// Set a `bool` setting and persist to disk (subject to `block_write`).
410    pub fn set_bool(&mut self, setting: &str, value: bool) -> Result<()> {
411        self.bool.insert(setting.to_owned(), value);
412        self.write()
413    }
414
415    /// Set an `i32` setting and persist to disk (subject to `block_write`).
416    pub fn set_i32(&mut self, setting: &str, value: i32) -> Result<()> {
417        self.i32.insert(setting.to_owned(), value);
418        self.write()
419    }
420
421    /// Set an `f32` setting and persist to disk (subject to `block_write`).
422    pub fn set_f32(&mut self, setting: &str, value: f32) -> Result<()> {
423        self.f32.insert(setting.to_owned(), value);
424        self.write()
425    }
426
427    /// Set a `String` setting and persist to disk (subject to `block_write`).
428    pub fn set_string(&mut self, setting: &str, value: &str) -> Result<()> {
429        self.string.insert(setting.to_owned(), value.to_owned());
430        self.write()
431    }
432
433    /// Set a path setting (stored as a string) and persist to disk
434    /// (subject to `block_write`).
435    pub fn set_path_buf(&mut self, setting: &str, value: &Path) -> Result<()> {
436        self.string.insert(setting.to_owned(), value.to_string_lossy().to_string());
437        self.write()
438    }
439
440    /// Set a raw byte-blob setting and persist to disk (subject to `block_write`).
441    pub fn set_raw_data(&mut self, setting: &str, value: &[u8]) -> Result<()> {
442        self.raw_data.insert(setting.to_owned(), value.to_vec());
443        self.write()
444    }
445
446    /// Set a `Vec<String>` setting and persist to disk (subject to `block_write`).
447    pub fn set_vec_string(&mut self, setting: &str, value: &[String]) -> Result<()> {
448        self.vec_string.insert(setting.to_owned(), value.to_vec());
449        self.write()
450    }
451
452    /// Set a `bool` setting only if it isn't already set. Used by
453    /// [`Self::init`] to seed defaults without clobbering user choices.
454    pub fn initialize_bool(&mut self, setting: &str, value: bool) {
455        if !self.bool.contains_key(setting) {
456            self.bool.insert(setting.to_owned(), value);
457        }
458    }
459
460    /// Set an `i32` setting only if it isn't already set. See [`Self::initialize_bool`].
461    pub fn initialize_i32(&mut self, setting: &str, value: i32) {
462        if !self.i32.contains_key(setting) {
463            self.i32.insert(setting.to_owned(), value);
464        }
465    }
466
467    /// Set an `f32` setting only if it isn't already set. See [`Self::initialize_bool`].
468    pub fn initialize_f32(&mut self, setting: &str, value: f32) {
469        if !self.f32.contains_key(setting) {
470            self.f32.insert(setting.to_owned(), value);
471        }
472    }
473
474    /// Set a `String` setting only if it isn't already set. See [`Self::initialize_bool`].
475    pub fn initialize_string(&mut self, setting: &str, value: &str) {
476        if !self.string.contains_key(setting) {
477            self.string.insert(setting.to_owned(), value.to_owned());
478        }
479    }
480
481    /// Set a path setting only if it isn't already set. See [`Self::initialize_bool`].
482    pub fn initialize_path_buf(&mut self, setting: &str, value: &Path) {
483        if !self.string.contains_key(setting) {
484            self.string.insert(setting.to_owned(), value.to_string_lossy().to_string());
485        }
486    }
487
488    /// Set a raw byte-blob setting only if it isn't already set. See [`Self::initialize_bool`].
489    pub fn initialize_raw_data(&mut self, setting: &str, value: &[u8]) {
490        if !self.raw_data.contains_key(setting) {
491            self.raw_data.insert(setting.to_owned(), value.to_vec());
492        }
493    }
494
495    /// Set a `Vec<String>` setting only if it isn't already set. See [`Self::initialize_bool`].
496    pub fn initialize_vec_string(&mut self, setting: &str, value: &[String]) {
497        if !self.vec_string.contains_key(setting) {
498            self.vec_string.insert(setting.to_owned(), value.to_vec());
499        }
500    }
501
502    /// Project the optimiser-related boolean settings into an
503    /// [`OptimizerOptions`] suitable for handing to
504    /// [`rpfm_extensions::optimizer`].
505    pub fn optimizer_options(&self) -> OptimizerOptions {
506        let mut options = OptimizerOptions::default();
507
508        options.set_pack_remove_itm_files(self.bool(PACK_REMOVE_ITM_FILES));
509        options.set_pack_apply_compression(self.bool(PACK_APPLY_COMPRESSION));
510        options.set_pack_remove_duplicated_files(self.bool(PACK_REMOVE_DUPLICATED_FILES));
511        options.set_db_import_datacores_into_twad_key_deletes(self.bool(DB_IMPORT_DATACORES_INTO_TWAD_KEY_DELETES));
512        options.set_db_optimize_datacored_tables(self.bool(DB_OPTIMIZE_DATACORED_TABLES));
513        options.set_table_remove_duplicated_entries(self.bool(TABLE_REMOVE_DUPLICATED_ENTRIES));
514        options.set_table_remove_itm_entries(self.bool(TABLE_REMOVE_ITM_ENTRIES));
515        options.set_table_remove_itnr_entries(self.bool(TABLE_REMOVE_ITNR_ENTRIES));
516        options.set_table_remove_empty_file(self.bool(TABLE_REMOVE_EMPTY_FILE));
517        options.set_text_remove_unused_xml_map_folders(self.bool(TEXT_REMOVE_UNUSED_XML_MAP_FOLDERS));
518        options.set_text_remove_unused_xml_prefab_folder(self.bool(TEXT_REMOVE_UNUSED_XML_PREFAB_FOLDER));
519        options.set_text_remove_agf_files(self.bool(TEXT_REMOVE_AGF_FILES));
520        options.set_text_remove_model_statistics_files(self.bool(TEXT_REMOVE_MODEL_STATISTICS_FILES));
521        options.set_pts_remove_unused_art_sets(self.bool(PTS_REMOVE_UNUSED_ART_SETS));
522        options.set_pts_remove_unused_variants(self.bool(PTS_REMOVE_UNUSED_VARIANTS));
523        options.set_pts_remove_empty_masks(self.bool(PTS_REMOVE_EMPTY_MASKS));
524        options.set_pts_remove_empty_file(self.bool(PTS_REMOVE_EMPTY_FILE));
525
526        options
527    }
528
529    /// This function returns the path where the db files from the assembly kit are stored.
530    pub fn assembly_kit_path(&self, game: &GameInfo) -> Result<PathBuf> {
531        let version = *game.raw_db_version();
532        match version {
533
534            // Post-Shogun 2 games.
535            2 | 1 => {
536                let mut base_path = self.path_buf(&format!("{}_assembly_kit", game.key()));
537                base_path.push("raw_data/db");
538                Ok(base_path)
539            }
540
541            0 => {
542                let base_path = old_ak_files_path()?.join(game.key());
543                Ok(base_path)
544            },
545
546            // Shogun 2/Older games
547            _ => Err(RLibError::AssemblyKitUnsupportedVersion(version).into())
548        }
549    }
550}
551
552//-------------------------------------------------------------------------------//
553//                             Extra Helpers
554//-------------------------------------------------------------------------------//
555
556/// This function returns RPFM's default config path, ignoring any custom-folder redirect.
557///
558/// Note: On `Debug´ mode this project is the project from where you execute one of RPFM's programs, which should be the root of the repo.
559pub fn default_config_path() -> Result<PathBuf> {
560
561    // On debug builds we use the local folder as the config folder.
562    if cfg!(debug_assertions) {
563        std::env::current_dir().map_err(From::from)
564    } else {
565        match ProjectDirs::from(ORG_DOMAIN, ORG_NAME, APP_NAME) {
566            Some(proj_dirs) => Ok(proj_dirs.config_dir().to_path_buf()),
567            None => Err(anyhow!("Failed to get the config path."))
568        }
569    }
570}
571
572/// This function returns the active config path: the user's custom folder if one is set, or the default otherwise.
573///
574/// All other config sub-paths derive from this, so setting a custom folder relocates RPFM's whole config tree.
575pub fn config_path() -> Result<PathBuf> {
576    match custom_config_path()? {
577        Some(path) => Ok(path),
578        None => default_config_path(),
579    }
580}
581
582/// This function returns the user-configured custom config folder, or `None` if RPFM uses the default one.
583///
584/// The custom path is read from the [`CONFIG_REDIRECT_FILE_NAME`] file inside the default config path.
585pub fn custom_config_path() -> Result<Option<PathBuf>> {
586    let redirect_file = default_config_path()?.join(CONFIG_REDIRECT_FILE_NAME);
587    if !redirect_file.is_file() {
588        return Ok(None);
589    }
590
591    let raw = std::fs::read_to_string(&redirect_file)?;
592    let trimmed = raw.trim();
593    if trimmed.is_empty() {
594        Ok(None)
595    } else {
596        Ok(Some(PathBuf::from(trimmed)))
597    }
598}
599
600/// This function sets (or clears, when passed `None`) the custom config folder and initializes it.
601///
602/// The choice is persisted to the redirect file in the default config path. The new folder is created and
603/// populated right away, but the running program keeps using the old one until it's restarted.
604pub fn set_custom_config_path(path: Option<&Path>) -> Result<()> {
605
606    // The redirect file always lives in the default path, so make sure that one exists first.
607    let default_path = default_config_path()?;
608    DirBuilder::new().recursive(true).create(&default_path)?;
609    let redirect_file = default_path.join(CONFIG_REDIRECT_FILE_NAME);
610
611    match path {
612        Some(path) if !path.as_os_str().is_empty() => {
613            DirBuilder::new().recursive(true).create(path)?;
614            std::fs::write(&redirect_file, path.to_string_lossy().as_bytes())?;
615        }
616        _ => if redirect_file.is_file() {
617            std::fs::remove_file(&redirect_file)?;
618        }
619    }
620
621    init_config_path()
622}
623
624/// This function returns the path where crash logs are stored.
625pub fn error_path() -> Result<PathBuf> {
626    Ok(config_path()?.join("error"))
627}
628
629/// Function to initialize the config folder, so RPFM can use it to store his stuff.
630///
631/// This can fail, so if this fails, better stop the program and check why it failed.
632#[must_use = "Many things depend on this folder existing. So better check this worked."]
633pub fn init_config_path() -> Result<()> {
634
635    let config_path = config_path()?;
636    DirBuilder::new().recursive(true).create(&config_path)?;
637    DirBuilder::new().recursive(true).create(backup_autosave_path()?)?;
638    DirBuilder::new().recursive(true).create(error_path()?)?;
639    DirBuilder::new().recursive(true).create(schemas_path()?)?;
640    DirBuilder::new().recursive(true).create(table_patches_path()?)?;
641    DirBuilder::new().recursive(true).create(table_profiles_path()?)?;
642    DirBuilder::new().recursive(true).create(scripts_path()?)?;
643    DirBuilder::new().recursive(true).create(old_ak_files_path()?)?;
644
645    // Schema patches need their file existing to even save.
646    let games = SupportedGames::default();
647    for game in games.games_sorted() {
648        let path = table_patches_path().unwrap().join(game.schema_file_name());
649        if !path.is_file() {
650            let base: HashMap<String, DefinitionPatch> = HashMap::new();
651            let mut file = BufWriter::new(File::create(path)?);
652            let config = PrettyConfig::default();
653            file.write_all(to_string_pretty(&base, config)?.as_bytes())?;
654        }
655    }
656
657    /*
658    #[cfg(feature = "support_model_renderer")] {
659        let assets_path = format!("{}/assets/", rpfm_ui_common::ASSETS_PATH.to_string_lossy());
660        if !PathBuf::from(&assets_path).is_dir() {
661            DirBuilder::new().recursive(true).create(&assets_path)?;
662        }
663
664        unsafe {crate::ffi::set_asset_folder(&assets_path); }
665
666        let log_path = config_path.to_string_lossy();
667        unsafe {crate::ffi::set_log_folder(&log_path); }
668    }*/
669
670    Ok(())
671}
672
673/// This function returns the schema path.
674pub fn schemas_path() -> Result<PathBuf> {
675    Ok(config_path()?.join(SCHEMA_FOLDER))
676}
677
678/// Folder under [`config_path`] where user-side schema patches live.
679pub fn table_patches_path() -> Result<PathBuf> {
680    Ok(config_path()?.join(TABLE_PATCHES_FOLDER))
681}
682
683/// Folder under [`config_path`] where saved table view profiles (column
684/// orders, filters, hidden columns) are persisted.
685pub fn table_profiles_path() -> Result<PathBuf> {
686    Ok(config_path()?.join(TABLE_PROFILES_FOLDER))
687}
688
689/// This function returns the lua autogen path.
690pub fn lua_autogen_base_path() -> Result<PathBuf> {
691    Ok(config_path()?.join(LUA_AUTOGEN_FOLDER))
692}
693
694/// This function returns the lua autogen path for a specific game.
695pub fn lua_autogen_game_path(game: &GameInfo) -> Result<PathBuf> {
696    match game.lua_autogen_folder() {
697        Some(folder) => Ok(config_path()?.join(LUA_AUTOGEN_FOLDER).join(folder)),
698        None => Err(anyhow!("Lua Autogen not available for this game."))
699    }
700}
701
702/// This function returns the autosave path.
703pub fn backup_autosave_path() -> Result<PathBuf> {
704    Ok(config_path()?.join("autosaves"))
705}
706
707/// This function returns the dependencies path.
708pub fn dependencies_cache_path() -> Result<PathBuf> {
709    Ok(config_path()?.join(DEPENDENCIES_FOLDER))
710}
711
712/// Folder under [`config_path`] where the user drops plugin scripts shown in the
713/// PackFile contents context menu.
714pub fn scripts_path() -> Result<PathBuf> {
715    Ok(config_path()?.join(SCRIPTS_FOLDER))
716}
717
718/// Folder under [`config_path`] holding archived Empire/Napoleon Assembly Kit
719/// definitions (no AK was ever shipped for these games, so RPFM bundles a
720/// frozen copy via the `old_ak_files` submodule).
721pub fn old_ak_files_path() -> Result<PathBuf> {
722    Ok(config_path()?.join("old_ak_files"))
723}
724
725/// Folder under [`config_path`] where the user's local mod translations are
726/// stored (one JSON per pack/language).
727pub fn translations_local_path() -> Result<PathBuf> {
728    Ok(config_path()?.join(TRANSLATIONS_LOCAL_FOLDER))
729}
730
731/// Folder under [`config_path`] where the local clone of the Translation Hub
732/// repository is mirrored.
733pub fn translations_remote_path() -> Result<PathBuf> {
734    Ok(config_path()?.join(TRANSLATIONS_REMOTE_FOLDER))
735}
736
737/// Recursively deletes the config folder, then re-runs [`init_config_path`] to recreate
738/// the standard sub-folders. Refuses to delete anything outside
739/// [`config_path`].
740///
741/// Used by the "reset settings" / "clear caches" actions in the UI.
742pub fn clear_config_path(path: &Path) -> Result<()> {
743    if path.exists() && path.is_dir() && path.starts_with(config_path()?) {
744        std::fs::remove_dir_all(path)?;
745        init_config_path()
746    } else {
747        Err(anyhow!("Path is not a valid directory to clear or does not exist"))
748    }
749}