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 DEPENDENCIES_FOLDER: &str = "dependencies";
43const TABLE_PATCHES_FOLDER: &str = "table_patches";
44const TABLE_PROFILES_FOLDER: &str = "table_profiles";
45const TRANSLATIONS_LOCAL_FOLDER: &str = "translations_local";
46const TRANSLATIONS_REMOTE_FOLDER: &str = "translations_remote";
47
48#[macro_export]
62macro_rules! set_batch {
63 ($( $rtype:ident, $id:literal, $source:expr), *) => {
64 {
65 let mut set = SETTINGS.write().unwrap();
66 set.set_block_write(true);
67 $(
68 let _ = set.$rtype($id, $source);
69 )*
70 set.set_block_write(false);
71 let _ = set.write();
72 }
73 };
74}
75
76#[derive(Clone, Debug, Default, Serialize, Deserialize)]
92pub struct Settings {
93
94 #[serde(skip_serializing, skip_deserializing)]
97 pub block_write: bool,
98
99 pub bool: HashMap<String, bool>,
101 pub i32: HashMap<String, i32>,
103 pub f32: HashMap<String, f32>,
105 pub string: HashMap<String, String>,
108 pub raw_data: HashMap<String, Vec<u8>>,
110 pub vec_string: HashMap<String, Vec<String>>
112}
113
114impl Settings {
119
120 pub fn init(as_new: bool) -> Result<Self> {
129 let mut settings = if !as_new {
130 match Settings::read() {
131 Ok(settings) => settings,
132 Err(error) => {
133
134 if let Ok(config) = config_path() {
137 let settings_path = config.join(SETTINGS_FILE_NAME);
138 if settings_path.exists() {
139 let backup_path = config.join(format!("{SETTINGS_FILE_NAME}.bak"));
140 let _ = std::fs::copy(&settings_path, &backup_path);
141 }
142 }
143
144 rpfm_telemetry::warn!("Failed to read settings file, using defaults. Error: {error}");
145 Settings::default()
146 }
147 }
148 } else {
149 Settings::default()
150 };
151
152 settings.set_block_write(true);
153
154 settings.initialize_string(MYMOD_BASE_PATH, "");
155 settings.initialize_string(SECONDARY_PATH, "");
156
157 let supported_games = SupportedGames::default();
158 for game in &supported_games.games() {
159 let game_key = game.key();
160
161 let current_path = settings.string(game_key);
163 if current_path.is_empty() {
164 if current_path.contains("\\") {
165 let _ = settings.set_string(game_key, ¤t_path.replace("\\", "/"));
166 }
167
168 let game_path = if let Ok(Some(game_path)) = game.find_game_install_location() {
169 game_path.to_string_lossy().replace("\\", "/")
170 } else {
171 String::new()
172 };
173
174 if !game_path.is_empty() {
176 let _ = settings.set_string(game_key, &game_path);
177 } else {
178 settings.initialize_string(game_key, &game_path);
179 }
180 }
181
182 if game_key != KEY_EMPIRE &&
183 game_key != KEY_NAPOLEON &&
184 game_key != KEY_ARENA {
185
186 let ak_key = game_key.to_owned() + ASSEMBLY_KIT_SUFFIX;
188 let current_path = settings.string(&ak_key);
189
190 if current_path.is_empty() {
191 let ak_path = if let Ok(Some(ak_path)) = game.find_assembly_kit_install_location() {
192 ak_path.join("assembly_kit").to_string_lossy().replace("\\", "/")
193 } else {
194 String::new()
195 };
196
197 if current_path.contains("\\") {
199 let _ = settings.set_string(&ak_key, ¤t_path.replace("\\", "/"));
200 }
201
202 if !ak_path.is_empty() && game_key != KEY_SHOGUN_2 {
204 let _ = settings.set_string(&ak_key, &ak_path);
205 } else {
206 settings.initialize_string(&ak_key, &ak_path);
207 }
208 }
209 }
210 }
211
212 settings.initialize_bool(IMPORT_FROM_QT, false);
214
215 settings.initialize_string(DEFAULT_GAME, KEY_WARHAMMER_3);
217 settings.initialize_string(LANGUAGE, "English_en");
218 settings.initialize_i32(AUTOSAVE_AMOUNT, 10);
220 settings.initialize_i32(AUTOSAVE_INTERVAL, 5);
221
222 settings.initialize_bool(START_MAXIMIZED, false);
233 settings.initialize_bool(ALLOW_EDITING_OF_CA_PACKFILES, false);
234 settings.initialize_bool(CHECK_UPDATES_ON_START, true);
235 settings.initialize_bool(CHECK_SCHEMA_UPDATES_ON_START, true);
236 settings.initialize_bool(CHECK_LUA_AUTOGEN_UPDATES_ON_START, true);
237 settings.initialize_bool(CHECK_OLD_AK_UPDATES_ON_START, true);
238 settings.initialize_bool(USE_LAZY_LOADING, true);
239 settings.initialize_bool(DISABLE_UUID_REGENERATION_ON_DB_TABLES, true);
240 settings.initialize_bool(PACKFILE_TREEVIEW_RESIZE_TO_FIT, false);
241 settings.initialize_bool(EXPAND_TREEVIEW_WHEN_ADDING_ITEMS, true);
242 settings.initialize_bool(USE_RIGHT_SIZE_MARKERS, false);
243 settings.initialize_bool(DISABLE_FILE_PREVIEWS, false);
244 settings.initialize_bool(INCLUDE_BASE_FOLDER_ON_ADD_FROM_FOLDER, true);
245 settings.initialize_bool(DELETE_EMPTY_FOLDERS_ON_DELETE, true);
246 settings.initialize_bool(AUTOSAVE_FOLDER_SIZE_WARNING_TRIGGERED, false);
247 settings.initialize_bool(IGNORE_GAME_FILES_IN_AK, false);
248 settings.initialize_bool(ENABLE_MULTIFOLDER_FILEPICKER, false);
249 settings.initialize_bool(ENABLE_PACK_CONTENTS_DRAG_AND_DROP, true);
250
251 settings.initialize_bool(ADJUST_COLUMNS_TO_CONTENT, true);
253 settings.initialize_bool(EXTEND_LAST_COLUMN_ON_TABLES, true);
254 settings.initialize_bool(DISABLE_COMBOS_ON_TABLES, false);
255 settings.initialize_bool(TIGHT_TABLE_MODE, false);
256 settings.initialize_bool(TABLE_RESIZE_ON_EDIT, false);
257 settings.initialize_bool(TABLES_USE_OLD_COLUMN_ORDER, true);
258 settings.initialize_bool(TABLES_USE_OLD_COLUMN_ORDER_FOR_TSV, true);
259 settings.initialize_bool(ENABLE_LOOKUPS, true);
260 settings.initialize_bool(ENABLE_ICONS, true);
261 settings.initialize_bool(ENABLE_DIFF_MARKERS, true);
262 settings.initialize_bool(HIDE_UNUSED_COLUMNS, true);
263
264 settings.initialize_bool(CHECK_FOR_MISSING_TABLE_DEFINITIONS, false);
266 settings.initialize_bool(ENABLE_DEBUG_MENU, false);
267 settings.initialize_bool(ENABLE_UNIT_EDITOR, false);
268 settings.initialize_bool(ENABLE_ESF_EDITOR, false);
269 settings.initialize_bool(USE_DEBUG_VIEW_UNIT_VARIANT, false);
270 settings.initialize_bool(ENABLE_RENDERER, true);
271
272 settings.initialize_bool(DIAGNOSTICS_TRIGGER_ON_OPEN, true);
274 settings.initialize_bool(DIAGNOSTICS_TRIGGER_ON_TABLE_EDIT, true);
275
276 settings.initialize_bool(ENABLE_USAGE_TELEMETRY, true);
278 settings.initialize_bool(ENABLE_CRASH_REPORTS, true);
279
280 settings.initialize_string(AI_API_URL, "https://api.openai.com/v1/chat/completions");
281 settings.initialize_string(AI_API_KEY, "");
282 settings.initialize_string(AI_MODEL, "gpt-4o-mini");
283 settings.initialize_string(DEEPL_API_KEY, "");
284
285 settings.initialize_vec_string(RECENT_FILE_LIST, &[]);
286
287 let opt = OptimizerOptions::default();
303 settings.initialize_bool(PACK_REMOVE_ITM_FILES, *opt.pack_remove_itm_files());
304 settings.initialize_bool(PACK_APPLY_COMPRESSION, *opt.pack_apply_compression());
305 settings.initialize_bool(DB_IMPORT_DATACORES_INTO_TWAD_KEY_DELETES, *opt.db_import_datacores_into_twad_key_deletes());
306 settings.initialize_bool(DB_OPTIMIZE_DATACORED_TABLES, *opt.db_optimize_datacored_tables());
307 settings.initialize_bool(TABLE_REMOVE_DUPLICATED_ENTRIES, *opt.table_remove_duplicated_entries());
308 settings.initialize_bool(TABLE_REMOVE_ITM_ENTRIES, *opt.table_remove_itm_entries());
309 settings.initialize_bool(TABLE_REMOVE_ITNR_ENTRIES, *opt.table_remove_itnr_entries());
310 settings.initialize_bool(TABLE_REMOVE_EMPTY_FILE, *opt.table_remove_empty_file());
311 settings.initialize_bool(TEXT_REMOVE_UNUSED_XML_MAP_FOLDERS, *opt.text_remove_unused_xml_map_folders());
312 settings.initialize_bool(TEXT_REMOVE_UNUSED_XML_PREFAB_FOLDER, *opt.text_remove_unused_xml_prefab_folder());
313 settings.initialize_bool(TEXT_REMOVE_AGF_FILES, *opt.text_remove_agf_files());
314 settings.initialize_bool(TEXT_REMOVE_MODEL_STATISTICS_FILES, *opt.text_remove_model_statistics_files());
315 settings.initialize_bool(PTS_REMOVE_UNUSED_ART_SETS, *opt.pts_remove_unused_art_sets());
316 settings.initialize_bool(PTS_REMOVE_UNUSED_VARIANTS, *opt.pts_remove_unused_variants());
317 settings.initialize_bool(PTS_REMOVE_EMPTY_MASKS, *opt.pts_remove_empty_masks());
318 settings.initialize_bool(PTS_REMOVE_EMPTY_FILE, *opt.pts_remove_empty_file());
319
320 settings.set_block_write(false);
321
322 settings.write()?;
323
324 Ok(settings)
325 }
326
327 pub fn read() -> Result<Self> {
332 let mut data = vec![];
333 let mut file = BufReader::new(File::open(config_path()?.join(SETTINGS_FILE_NAME))?);
334 file.read_to_end(&mut data)?;
335
336 serde_json::from_slice(&data).map_err(From::from)
337 }
338
339 pub fn write(&self) -> Result<()> {
341 if self.block_write {
342 return Ok(());
343 }
344
345 let mut file = BufWriter::new(File::create(config_path()?.join(SETTINGS_FILE_NAME))?);
346 file.write_all(serde_json::to_string_pretty(self)?.as_bytes()).map_err(From::from)
347 }
348
349 pub fn set_block_write(&mut self, status: bool) {
351 self.block_write = status;
352 }
353
354 pub fn bool(&self, setting: &str) -> bool {
356 self.bool.get(setting).copied().unwrap_or_default()
357 }
358
359 pub fn i32(&self, setting: &str) -> i32 {
361 self.i32.get(setting).copied().unwrap_or_default()
362 }
363
364 pub fn f32(&self, setting: &str) -> f32 {
366 self.f32.get(setting).copied().unwrap_or_default()
367 }
368
369 pub fn string(&self, setting: &str) -> String {
371 self.string.get(setting).map(|x| x.to_owned()).unwrap_or_default()
372 }
373
374 pub fn path_buf(&self, setting: &str) -> PathBuf {
378 self.string.get(setting).map(PathBuf::from).unwrap_or_default()
379 }
380
381 pub fn raw_data(&self, setting: &str) -> Vec<u8> {
383 self.raw_data.get(setting).map(|x| x.to_vec()).unwrap_or_default()
384 }
385
386 pub fn vec_string(&self, setting: &str) -> Vec<String> {
388 self.vec_string.get(setting).map(|x| x.to_vec()).unwrap_or_default()
389 }
390
391 pub fn set_bool(&mut self, setting: &str, value: bool) -> Result<()> {
393 self.bool.insert(setting.to_owned(), value);
394 self.write()
395 }
396
397 pub fn set_i32(&mut self, setting: &str, value: i32) -> Result<()> {
399 self.i32.insert(setting.to_owned(), value);
400 self.write()
401 }
402
403 pub fn set_f32(&mut self, setting: &str, value: f32) -> Result<()> {
405 self.f32.insert(setting.to_owned(), value);
406 self.write()
407 }
408
409 pub fn set_string(&mut self, setting: &str, value: &str) -> Result<()> {
411 self.string.insert(setting.to_owned(), value.to_owned());
412 self.write()
413 }
414
415 pub fn set_path_buf(&mut self, setting: &str, value: &Path) -> Result<()> {
418 self.string.insert(setting.to_owned(), value.to_string_lossy().to_string());
419 self.write()
420 }
421
422 pub fn set_raw_data(&mut self, setting: &str, value: &[u8]) -> Result<()> {
424 self.raw_data.insert(setting.to_owned(), value.to_vec());
425 self.write()
426 }
427
428 pub fn set_vec_string(&mut self, setting: &str, value: &[String]) -> Result<()> {
430 self.vec_string.insert(setting.to_owned(), value.to_vec());
431 self.write()
432 }
433
434 pub fn initialize_bool(&mut self, setting: &str, value: bool) {
437 if !self.bool.contains_key(setting) {
438 self.bool.insert(setting.to_owned(), value);
439 }
440 }
441
442 pub fn initialize_i32(&mut self, setting: &str, value: i32) {
444 if !self.i32.contains_key(setting) {
445 self.i32.insert(setting.to_owned(), value);
446 }
447 }
448
449 pub fn initialize_f32(&mut self, setting: &str, value: f32) {
451 if !self.f32.contains_key(setting) {
452 self.f32.insert(setting.to_owned(), value);
453 }
454 }
455
456 pub fn initialize_string(&mut self, setting: &str, value: &str) {
458 if !self.string.contains_key(setting) {
459 self.string.insert(setting.to_owned(), value.to_owned());
460 }
461 }
462
463 pub fn initialize_path_buf(&mut self, setting: &str, value: &Path) {
465 if !self.string.contains_key(setting) {
466 self.string.insert(setting.to_owned(), value.to_string_lossy().to_string());
467 }
468 }
469
470 pub fn initialize_raw_data(&mut self, setting: &str, value: &[u8]) {
472 if !self.raw_data.contains_key(setting) {
473 self.raw_data.insert(setting.to_owned(), value.to_vec());
474 }
475 }
476
477 pub fn initialize_vec_string(&mut self, setting: &str, value: &[String]) {
479 if !self.vec_string.contains_key(setting) {
480 self.vec_string.insert(setting.to_owned(), value.to_vec());
481 }
482 }
483
484 pub fn optimizer_options(&self) -> OptimizerOptions {
488 let mut options = OptimizerOptions::default();
489
490 options.set_pack_remove_itm_files(self.bool(PACK_REMOVE_ITM_FILES));
491 options.set_pack_apply_compression(self.bool(PACK_APPLY_COMPRESSION));
492 options.set_db_import_datacores_into_twad_key_deletes(self.bool(DB_IMPORT_DATACORES_INTO_TWAD_KEY_DELETES));
493 options.set_db_optimize_datacored_tables(self.bool(DB_OPTIMIZE_DATACORED_TABLES));
494 options.set_table_remove_duplicated_entries(self.bool(TABLE_REMOVE_DUPLICATED_ENTRIES));
495 options.set_table_remove_itm_entries(self.bool(TABLE_REMOVE_ITM_ENTRIES));
496 options.set_table_remove_itnr_entries(self.bool(TABLE_REMOVE_ITNR_ENTRIES));
497 options.set_table_remove_empty_file(self.bool(TABLE_REMOVE_EMPTY_FILE));
498 options.set_text_remove_unused_xml_map_folders(self.bool(TEXT_REMOVE_UNUSED_XML_MAP_FOLDERS));
499 options.set_text_remove_unused_xml_prefab_folder(self.bool(TEXT_REMOVE_UNUSED_XML_PREFAB_FOLDER));
500 options.set_text_remove_agf_files(self.bool(TEXT_REMOVE_AGF_FILES));
501 options.set_text_remove_model_statistics_files(self.bool(TEXT_REMOVE_MODEL_STATISTICS_FILES));
502 options.set_pts_remove_unused_art_sets(self.bool(PTS_REMOVE_UNUSED_ART_SETS));
503 options.set_pts_remove_unused_variants(self.bool(PTS_REMOVE_UNUSED_VARIANTS));
504 options.set_pts_remove_empty_masks(self.bool(PTS_REMOVE_EMPTY_MASKS));
505 options.set_pts_remove_empty_file(self.bool(PTS_REMOVE_EMPTY_FILE));
506
507 options
508 }
509
510 pub fn assembly_kit_path(&self, game: &GameInfo) -> Result<PathBuf> {
512 let version = *game.raw_db_version();
513 match version {
514
515 2 | 1 => {
517 let mut base_path = self.path_buf(&format!("{}_assembly_kit", game.key()));
518 base_path.push("raw_data/db");
519 Ok(base_path)
520 }
521
522 0 => {
523 let base_path = old_ak_files_path()?.join(game.key());
524 Ok(base_path)
525 },
526
527 _ => Err(RLibError::AssemblyKitUnsupportedVersion(version).into())
529 }
530 }
531}
532
533pub fn config_path() -> Result<PathBuf> {
541
542 if cfg!(debug_assertions) {
544 std::env::current_dir().map_err(From::from)
545 } else {
546 match ProjectDirs::from(ORG_DOMAIN, ORG_NAME, APP_NAME) {
547 Some(proj_dirs) => Ok(proj_dirs.config_dir().to_path_buf()),
548 None => Err(anyhow!("Failed to get the config path."))
549 }
550 }
551}
552
553pub fn error_path() -> Result<PathBuf> {
555 Ok(config_path()?.join("error"))
556}
557
558#[must_use = "Many things depend on this folder existing. So better check this worked."]
562pub fn init_config_path() -> Result<()> {
563
564 let config_path = config_path()?;
565 DirBuilder::new().recursive(true).create(&config_path)?;
566 DirBuilder::new().recursive(true).create(backup_autosave_path()?)?;
567 DirBuilder::new().recursive(true).create(error_path()?)?;
568 DirBuilder::new().recursive(true).create(schemas_path()?)?;
569 DirBuilder::new().recursive(true).create(table_patches_path()?)?;
570 DirBuilder::new().recursive(true).create(table_profiles_path()?)?;
571 DirBuilder::new().recursive(true).create(old_ak_files_path()?)?;
572
573 let games = SupportedGames::default();
575 for game in games.games_sorted() {
576 let path = table_patches_path().unwrap().join(game.schema_file_name());
577 if !path.is_file() {
578 let base: HashMap<String, DefinitionPatch> = HashMap::new();
579 let mut file = BufWriter::new(File::create(path)?);
580 let config = PrettyConfig::default();
581 file.write_all(to_string_pretty(&base, config)?.as_bytes())?;
582 }
583 }
584
585 Ok(())
599}
600
601pub fn schemas_path() -> Result<PathBuf> {
603 Ok(config_path()?.join(SCHEMA_FOLDER))
604}
605
606pub fn table_patches_path() -> Result<PathBuf> {
608 Ok(config_path()?.join(TABLE_PATCHES_FOLDER))
609}
610
611pub fn table_profiles_path() -> Result<PathBuf> {
614 Ok(config_path()?.join(TABLE_PROFILES_FOLDER))
615}
616
617pub fn lua_autogen_base_path() -> Result<PathBuf> {
619 Ok(config_path()?.join(LUA_AUTOGEN_FOLDER))
620}
621
622pub fn lua_autogen_game_path(game: &GameInfo) -> Result<PathBuf> {
624 match game.lua_autogen_folder() {
625 Some(folder) => Ok(config_path()?.join(LUA_AUTOGEN_FOLDER).join(folder)),
626 None => Err(anyhow!("Lua Autogen not available for this game."))
627 }
628}
629
630pub fn backup_autosave_path() -> Result<PathBuf> {
632 Ok(config_path()?.join("autosaves"))
633}
634
635pub fn dependencies_cache_path() -> Result<PathBuf> {
637 Ok(config_path()?.join(DEPENDENCIES_FOLDER))
638}
639
640pub fn old_ak_files_path() -> Result<PathBuf> {
644 Ok(config_path()?.join("old_ak_files"))
645}
646
647pub fn translations_local_path() -> Result<PathBuf> {
650 Ok(config_path()?.join(TRANSLATIONS_LOCAL_FOLDER))
651}
652
653pub fn translations_remote_path() -> Result<PathBuf> {
656 Ok(config_path()?.join(TRANSLATIONS_REMOTE_FOLDER))
657}
658
659pub fn clear_config_path(path: &Path) -> Result<()> {
665 if path.exists() && path.is_dir() && path.starts_with(config_path()?) {
666 std::fs::remove_dir_all(path)?;
667 init_config_path()
668 } else {
669 Err(anyhow!("Path is not a valid directory to clear or does not exist"))
670 }
671}