Skip to main content

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