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