1use 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
42const CONFIG_REDIRECT_FILE_NAME: &str = "config_folder.txt";
44
45const DEPENDENCIES_FOLDER: &str = "dependencies";
46
47const 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#[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#[derive(Clone, Debug, Default, Serialize, Deserialize)]
99pub struct Settings {
100
101 #[serde(skip_serializing, skip_deserializing)]
104 pub block_write: bool,
105
106 pub bool: HashMap<String, bool>,
108 pub i32: HashMap<String, i32>,
110 pub f32: HashMap<String, f32>,
112 pub string: HashMap<String, String>,
115 pub raw_data: HashMap<String, Vec<u8>>,
117 pub vec_string: HashMap<String, Vec<String>>
119}
120
121impl Settings {
126
127 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 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 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, ¤t_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 !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 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 if current_path.contains("\\") {
206 let _ = settings.set_string(&ak_key, ¤t_path.replace("\\", "/"));
207 }
208
209 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 settings.initialize_bool(IMPORT_FROM_QT, false);
221
222 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_i32(AUTOSAVE_AMOUNT, 10);
228 settings.initialize_i32(AUTOSAVE_INTERVAL, 5);
229
230 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 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 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 settings.initialize_bool(DIAGNOSTICS_TRIGGER_ON_OPEN, true);
286 settings.initialize_bool(DIAGNOSTICS_TRIGGER_ON_TABLE_EDIT, true);
287
288 settings.initialize_bool(ENABLE_USAGE_TELEMETRY, true);
290 settings.initialize_bool(ENABLE_CRASH_REPORTS, true);
291
292 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 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 if let Err(error) = settings.write() {
341 rpfm_telemetry::warn!("Failed to persist settings file, continuing with in-memory settings. Error: {error}");
342 }
343
344 Ok(settings)
345 }
346
347 pub fn read() -> Result<Self> {
352 let mut data = vec![];
353 let mut file = BufReader::new(File::open(config_path()?.join(SETTINGS_FILE_NAME))?);
354 file.read_to_end(&mut data)?;
355
356 serde_json::from_slice(&data).map_err(From::from)
357 }
358
359 pub fn write(&self) -> Result<()> {
361 if self.block_write {
362 return Ok(());
363 }
364
365 let mut file = BufWriter::new(File::create(config_path()?.join(SETTINGS_FILE_NAME))?);
366 file.write_all(serde_json::to_string_pretty(self)?.as_bytes()).map_err(From::from)
367 }
368
369 pub fn set_block_write(&mut self, status: bool) {
371 self.block_write = status;
372 }
373
374 pub fn bool(&self, setting: &str) -> bool {
376 self.bool.get(setting).copied().unwrap_or_default()
377 }
378
379 pub fn i32(&self, setting: &str) -> i32 {
381 self.i32.get(setting).copied().unwrap_or_default()
382 }
383
384 pub fn f32(&self, setting: &str) -> f32 {
386 self.f32.get(setting).copied().unwrap_or_default()
387 }
388
389 pub fn string(&self, setting: &str) -> String {
391 self.string.get(setting).map(|x| x.to_owned()).unwrap_or_default()
392 }
393
394 pub fn path_buf(&self, setting: &str) -> PathBuf {
398 self.string.get(setting).map(PathBuf::from).unwrap_or_default()
399 }
400
401 pub fn raw_data(&self, setting: &str) -> Vec<u8> {
403 self.raw_data.get(setting).map(|x| x.to_vec()).unwrap_or_default()
404 }
405
406 pub fn vec_string(&self, setting: &str) -> Vec<String> {
408 self.vec_string.get(setting).map(|x| x.to_vec()).unwrap_or_default()
409 }
410
411 pub fn set_bool(&mut self, setting: &str, value: bool) -> Result<()> {
413 self.bool.insert(setting.to_owned(), value);
414 self.write()
415 }
416
417 pub fn set_i32(&mut self, setting: &str, value: i32) -> Result<()> {
419 self.i32.insert(setting.to_owned(), value);
420 self.write()
421 }
422
423 pub fn set_f32(&mut self, setting: &str, value: f32) -> Result<()> {
425 self.f32.insert(setting.to_owned(), value);
426 self.write()
427 }
428
429 pub fn set_string(&mut self, setting: &str, value: &str) -> Result<()> {
431 self.string.insert(setting.to_owned(), value.to_owned());
432 self.write()
433 }
434
435 pub fn set_path_buf(&mut self, setting: &str, value: &Path) -> Result<()> {
438 self.string.insert(setting.to_owned(), value.to_string_lossy().to_string());
439 self.write()
440 }
441
442 pub fn set_raw_data(&mut self, setting: &str, value: &[u8]) -> Result<()> {
444 self.raw_data.insert(setting.to_owned(), value.to_vec());
445 self.write()
446 }
447
448 pub fn set_vec_string(&mut self, setting: &str, value: &[String]) -> Result<()> {
450 self.vec_string.insert(setting.to_owned(), value.to_vec());
451 self.write()
452 }
453
454 pub fn initialize_bool(&mut self, setting: &str, value: bool) {
457 if !self.bool.contains_key(setting) {
458 self.bool.insert(setting.to_owned(), value);
459 }
460 }
461
462 pub fn initialize_i32(&mut self, setting: &str, value: i32) {
464 if !self.i32.contains_key(setting) {
465 self.i32.insert(setting.to_owned(), value);
466 }
467 }
468
469 pub fn initialize_f32(&mut self, setting: &str, value: f32) {
471 if !self.f32.contains_key(setting) {
472 self.f32.insert(setting.to_owned(), value);
473 }
474 }
475
476 pub fn initialize_string(&mut self, setting: &str, value: &str) {
478 if !self.string.contains_key(setting) {
479 self.string.insert(setting.to_owned(), value.to_owned());
480 }
481 }
482
483 pub fn initialize_path_buf(&mut self, setting: &str, value: &Path) {
485 if !self.string.contains_key(setting) {
486 self.string.insert(setting.to_owned(), value.to_string_lossy().to_string());
487 }
488 }
489
490 pub fn initialize_raw_data(&mut self, setting: &str, value: &[u8]) {
492 if !self.raw_data.contains_key(setting) {
493 self.raw_data.insert(setting.to_owned(), value.to_vec());
494 }
495 }
496
497 pub fn initialize_vec_string(&mut self, setting: &str, value: &[String]) {
499 if !self.vec_string.contains_key(setting) {
500 self.vec_string.insert(setting.to_owned(), value.to_vec());
501 }
502 }
503
504 pub fn optimizer_options(&self) -> OptimizerOptions {
508 let mut options = OptimizerOptions::default();
509
510 options.set_pack_remove_itm_files(self.bool(PACK_REMOVE_ITM_FILES));
511 options.set_pack_apply_compression(self.bool(PACK_APPLY_COMPRESSION));
512 options.set_pack_remove_duplicated_files(self.bool(PACK_REMOVE_DUPLICATED_FILES));
513 options.set_db_import_datacores_into_twad_key_deletes(self.bool(DB_IMPORT_DATACORES_INTO_TWAD_KEY_DELETES));
514 options.set_db_optimize_datacored_tables(self.bool(DB_OPTIMIZE_DATACORED_TABLES));
515 options.set_table_remove_duplicated_entries(self.bool(TABLE_REMOVE_DUPLICATED_ENTRIES));
516 options.set_table_remove_itm_entries(self.bool(TABLE_REMOVE_ITM_ENTRIES));
517 options.set_table_remove_itnr_entries(self.bool(TABLE_REMOVE_ITNR_ENTRIES));
518 options.set_table_remove_empty_file(self.bool(TABLE_REMOVE_EMPTY_FILE));
519 options.set_text_remove_unused_xml_map_folders(self.bool(TEXT_REMOVE_UNUSED_XML_MAP_FOLDERS));
520 options.set_text_remove_unused_xml_prefab_folder(self.bool(TEXT_REMOVE_UNUSED_XML_PREFAB_FOLDER));
521 options.set_text_remove_agf_files(self.bool(TEXT_REMOVE_AGF_FILES));
522 options.set_text_remove_model_statistics_files(self.bool(TEXT_REMOVE_MODEL_STATISTICS_FILES));
523 options.set_pts_remove_unused_art_sets(self.bool(PTS_REMOVE_UNUSED_ART_SETS));
524 options.set_pts_remove_unused_variants(self.bool(PTS_REMOVE_UNUSED_VARIANTS));
525 options.set_pts_remove_empty_masks(self.bool(PTS_REMOVE_EMPTY_MASKS));
526 options.set_pts_remove_empty_file(self.bool(PTS_REMOVE_EMPTY_FILE));
527
528 options
529 }
530
531 pub fn assembly_kit_path(&self, game: &GameInfo) -> Result<PathBuf> {
533 let version = *game.raw_db_version();
534 match version {
535
536 2 | 1 => {
538 let mut base_path = self.path_buf(&format!("{}_assembly_kit", game.key()));
539 base_path.push("raw_data/db");
540 Ok(base_path)
541 }
542
543 0 => {
544 let base_path = old_ak_files_path()?.join(game.key());
545 Ok(base_path)
546 },
547
548 _ => Err(RLibError::AssemblyKitUnsupportedVersion(version).into())
550 }
551 }
552}
553
554pub fn default_config_path() -> Result<PathBuf> {
562
563 if cfg!(debug_assertions) {
565 std::env::current_dir().map_err(From::from)
566 } else {
567 match ProjectDirs::from(ORG_DOMAIN, ORG_NAME, APP_NAME) {
568 Some(proj_dirs) => Ok(proj_dirs.config_dir().to_path_buf()),
569 None => Err(anyhow!("Failed to get the config path."))
570 }
571 }
572}
573
574pub fn config_path() -> Result<PathBuf> {
578 match custom_config_path()? {
579 Some(path) => Ok(path),
580 None => default_config_path(),
581 }
582}
583
584pub fn custom_config_path() -> Result<Option<PathBuf>> {
588 let redirect_file = default_config_path()?.join(CONFIG_REDIRECT_FILE_NAME);
589 if !redirect_file.is_file() {
590 return Ok(None);
591 }
592
593 let raw = std::fs::read_to_string(&redirect_file)?;
594 let trimmed = raw.trim();
595 if trimmed.is_empty() {
596 Ok(None)
597 } else {
598 Ok(Some(PathBuf::from(trimmed)))
599 }
600}
601
602pub fn set_custom_config_path(path: Option<&Path>) -> Result<()> {
607
608 let default_path = default_config_path()?;
610 DirBuilder::new().recursive(true).create(&default_path)?;
611 let redirect_file = default_path.join(CONFIG_REDIRECT_FILE_NAME);
612
613 match path {
614 Some(path) if !path.as_os_str().is_empty() => {
615 DirBuilder::new().recursive(true).create(path)?;
616 std::fs::write(&redirect_file, path.to_string_lossy().as_bytes())?;
617 }
618 _ => if redirect_file.is_file() {
619 std::fs::remove_file(&redirect_file)?;
620 }
621 }
622
623 init_config_path()
624}
625
626pub fn error_path() -> Result<PathBuf> {
628 Ok(config_path()?.join("error"))
629}
630
631#[must_use = "Many things depend on this folder existing. So better check this worked."]
635pub fn init_config_path() -> Result<()> {
636
637 let config_path = config_path()?;
638 DirBuilder::new().recursive(true).create(&config_path)?;
639 DirBuilder::new().recursive(true).create(backup_autosave_path()?)?;
640 DirBuilder::new().recursive(true).create(error_path()?)?;
641 DirBuilder::new().recursive(true).create(schemas_path()?)?;
642 DirBuilder::new().recursive(true).create(table_patches_path()?)?;
643 DirBuilder::new().recursive(true).create(table_profiles_path()?)?;
644 DirBuilder::new().recursive(true).create(scripts_path()?)?;
645 DirBuilder::new().recursive(true).create(old_ak_files_path()?)?;
646
647 let games = SupportedGames::default();
649 for game in games.games_sorted() {
650 let path = table_patches_path().unwrap().join(game.schema_file_name());
651 if !path.is_file() {
652 let base: HashMap<String, DefinitionPatch> = HashMap::new();
653 let mut file = BufWriter::new(File::create(path)?);
654 let config = PrettyConfig::default();
655 file.write_all(to_string_pretty(&base, config)?.as_bytes())?;
656 }
657 }
658
659 Ok(())
673}
674
675pub fn schemas_path() -> Result<PathBuf> {
677 Ok(config_path()?.join(SCHEMA_FOLDER))
678}
679
680pub fn table_patches_path() -> Result<PathBuf> {
682 Ok(config_path()?.join(TABLE_PATCHES_FOLDER))
683}
684
685pub fn table_profiles_path() -> Result<PathBuf> {
688 Ok(config_path()?.join(TABLE_PROFILES_FOLDER))
689}
690
691pub fn lua_autogen_base_path() -> Result<PathBuf> {
693 Ok(config_path()?.join(LUA_AUTOGEN_FOLDER))
694}
695
696pub fn lua_autogen_game_path(game: &GameInfo) -> Result<PathBuf> {
698 match game.lua_autogen_folder() {
699 Some(folder) => Ok(config_path()?.join(LUA_AUTOGEN_FOLDER).join(folder)),
700 None => Err(anyhow!("Lua Autogen not available for this game."))
701 }
702}
703
704pub fn backup_autosave_path() -> Result<PathBuf> {
706 Ok(config_path()?.join("autosaves"))
707}
708
709pub fn dependencies_cache_path() -> Result<PathBuf> {
711 Ok(config_path()?.join(DEPENDENCIES_FOLDER))
712}
713
714pub fn scripts_path() -> Result<PathBuf> {
717 Ok(config_path()?.join(SCRIPTS_FOLDER))
718}
719
720pub fn old_ak_files_path() -> Result<PathBuf> {
724 Ok(config_path()?.join("old_ak_files"))
725}
726
727pub fn translations_local_path() -> Result<PathBuf> {
730 Ok(config_path()?.join(TRANSLATIONS_LOCAL_FOLDER))
731}
732
733pub fn translations_remote_path() -> Result<PathBuf> {
736 Ok(config_path()?.join(TRANSLATIONS_REMOTE_FOLDER))
737}
738
739pub fn clear_config_path(path: &Path) -> Result<()> {
745 if path.exists() && path.is_dir() && path.starts_with(config_path()?) {
746 std::fs::remove_dir_all(path)?;
747 init_config_path()
748 } else {
749 Err(anyhow!("Path is not a valid directory to clear or does not exist"))
750 }
751}