1mod config;
51#[cfg(feature = "lua")]
52pub mod lua;
53mod platform_paths;
54
55pub use config::{
56 ConfigChainEntry, ConfigChainStatus, OpenMWConfiguration,
57 directorysetting::DirectorySetting,
58 encodingsetting::{EncodingSetting, EncodingType},
59 error::ConfigError,
60 filesetting::FileSetting,
61 gamesetting::GameSettingType,
62 genericsetting::GenericSetting,
63};
64
65#[cfg(feature = "lua")]
66pub use lua::create_lua_module;
67
68pub(crate) trait GameSetting: std::fmt::Display {
69 fn meta(&self) -> &GameSettingMeta;
70}
71
72#[derive(Debug, Clone, Eq, PartialEq)]
78pub struct GameSettingMeta {
79 source_config: std::path::PathBuf,
80 comment: String,
81}
82
83impl GameSettingMeta {
84 #[must_use]
85 pub fn source_config(&self) -> &std::path::Path {
86 &self.source_config
87 }
88
89 #[must_use]
90 pub fn comment(&self) -> &str {
91 &self.comment
92 }
93}
94
95const NO_CONFIG_DIR: &str = "FAILURE: COULD NOT READ CONFIG DIRECTORY";
96const NO_LOCAL_DIR: &str = "FAILURE: COULD NOT READ LOCAL DIRECTORY";
97const NO_GLOBAL_DIR: &str = "FAILURE: COULD NOT READ GLOBAL DIRECTORY";
98const NO_GLOBAL_CONFIG_DIR: &str = "FAILURE: COULD NOT READ GLOBAL CONFIG DIRECTORY";
99const DEFAULT_FLATPAK_APP_ID: &str = "org.openmw.OpenMW";
100
101#[cfg(target_os = "linux")]
102fn has_flatpak_info_file() -> bool {
103 use std::sync::OnceLock;
104
105 static HAS_FLATPAK_INFO: OnceLock<bool> = OnceLock::new();
106 *HAS_FLATPAK_INFO.get_or_init(|| std::path::Path::new("/.flatpak-info").exists())
107}
108
109fn flatpak_mode_enabled() -> bool {
110 #[cfg(not(target_os = "linux"))]
111 {
112 return false;
113 }
114
115 #[cfg(target_os = "linux")]
116 {
117 if std::env::var_os("OPENMW_CONFIG_USING_FLATPAK").is_some() {
118 return true;
119 }
120
121 std::env::var_os("FLATPAK_ID").is_some() || has_flatpak_info_file()
122 }
123}
124
125fn flatpak_app_id() -> String {
126 std::env::var("OPENMW_FLATPAK_ID")
127 .ok()
128 .filter(|value| !value.trim().is_empty())
129 .or_else(|| {
130 std::env::var("FLATPAK_ID")
131 .ok()
132 .filter(|value| !value.trim().is_empty())
133 })
134 .unwrap_or_else(|| DEFAULT_FLATPAK_APP_ID.to_string())
135}
136
137fn flatpak_userconfig_path() -> Result<std::path::PathBuf, ConfigError> {
138 platform_paths::home_dir().map(|home| {
139 home.join(".var")
140 .join("app")
141 .join(flatpak_app_id())
142 .join("config")
143 .join("openmw")
144 })
145}
146
147fn flatpak_userdata_path() -> Result<std::path::PathBuf, ConfigError> {
148 platform_paths::home_dir().map(|home| {
149 home.join(".var")
150 .join("app")
151 .join(flatpak_app_id())
152 .join("data")
153 .join("openmw")
154 })
155}
156
157pub fn try_default_config_path() -> Result<std::path::PathBuf, ConfigError> {
169 #[cfg(target_os = "android")]
170 return Ok(std::path::PathBuf::from(
171 "/storage/emulated/0/Alpha3/config",
172 ));
173
174 #[cfg(not(target_os = "android"))]
175 {
176 if flatpak_mode_enabled() {
177 return flatpak_userconfig_path();
178 }
179
180 platform_paths::config_dir().map_err(|_| ConfigError::PlatformPathUnavailable("config"))
181 }
182}
183
184#[must_use]
192pub fn default_config_path() -> std::path::PathBuf {
193 try_default_config_path().expect(NO_CONFIG_DIR)
194}
195
196pub fn try_default_user_config_file() -> Result<std::path::PathBuf, ConfigError> {
205 try_default_config_path().map(|path| path.join("openmw.cfg"))
206}
207
208#[must_use]
213pub fn default_user_config_file() -> std::path::PathBuf {
214 try_default_user_config_file().expect(NO_CONFIG_DIR)
215}
216
217pub fn try_default_userdata_path() -> Result<std::path::PathBuf, ConfigError> {
229 #[cfg(target_os = "android")]
230 return Ok(std::path::PathBuf::from("/storage/emulated/0/Alpha3"));
231
232 #[cfg(not(target_os = "android"))]
233 {
234 if flatpak_mode_enabled() {
235 return flatpak_userdata_path();
236 }
237
238 platform_paths::data_dir().map_err(|_| ConfigError::PlatformPathUnavailable("userdata"))
239 }
240}
241
242#[must_use]
250pub fn default_userdata_path() -> std::path::PathBuf {
251 try_default_userdata_path().expect("FAILURE: COULD NOT READ USERDATA DIRECTORY")
252}
253
254#[must_use]
259pub fn default_data_local_path() -> std::path::PathBuf {
260 default_userdata_path().join("data")
261}
262
263pub fn try_default_local_path() -> Result<std::path::PathBuf, ConfigError> {
273 let exe = std::env::current_exe()?;
274
275 #[cfg(target_os = "macos")]
276 {
277 if let Some(macos_dir) = exe.parent()
278 && macos_dir.file_name() == Some(std::ffi::OsStr::new("MacOS"))
279 && let Some(contents_dir) = macos_dir.parent()
280 && contents_dir.file_name() == Some(std::ffi::OsStr::new("Contents"))
281 {
282 return Ok(contents_dir.join("Resources"));
283 }
284 }
285
286 exe.parent()
287 .map(std::path::Path::to_path_buf)
288 .ok_or(ConfigError::PlatformPathUnavailable("local"))
289}
290
291#[must_use]
296pub fn default_local_path() -> std::path::PathBuf {
297 try_default_local_path().expect(NO_LOCAL_DIR)
298}
299
300pub fn try_default_root_config_path() -> Result<std::path::PathBuf, ConfigError> {
310 let local_dir = try_default_local_path()?;
311 let local = local_dir.join("openmw.cfg");
312 if local.is_file() {
313 return Ok(local);
314 }
315
316 let global_config_dir = try_default_global_config_path()?;
317 let global = global_config_dir.join("openmw.cfg");
318 if global.is_file() {
319 return Ok(global);
320 }
321
322 Err(ConfigError::CannotFindRootConfig { local, global })
323}
324
325pub fn try_default_root_or_user_config_path() -> Result<std::path::PathBuf, ConfigError> {
334 match try_default_root_config_path() {
335 Ok(path) => Ok(path),
336 Err(ConfigError::CannotFindRootConfig { local, global }) => {
337 let user = try_default_user_config_file()?;
338
339 if user.is_file() {
340 Ok(user)
341 } else {
342 Err(ConfigError::CannotFindAnyConfig {
343 local,
344 global,
345 user,
346 })
347 }
348 }
349 Err(error) => Err(error),
350 }
351}
352
353#[must_use]
358pub fn default_root_or_user_config_path() -> std::path::PathBuf {
359 try_default_root_or_user_config_path().expect("FAILURE: COULD NOT FIND ROOT OR USER CONFIG")
360}
361
362#[must_use]
367pub fn default_root_config_path() -> std::path::PathBuf {
368 try_default_root_config_path().expect("FAILURE: COULD NOT FIND ROOT CONFIG")
369}
370
371#[cfg(test)]
372pub(crate) fn discover_root_config_path(
373 local_dir: &std::path::Path,
374 global_config_dir: &std::path::Path,
375) -> Result<std::path::PathBuf, ConfigError> {
376 let local = local_dir.join("openmw.cfg");
377 if local.is_file() {
378 return Ok(local);
379 }
380
381 let global = global_config_dir.join("openmw.cfg");
382 if global.is_file() {
383 return Ok(global);
384 }
385
386 Err(ConfigError::CannotFindRootConfig { local, global })
387}
388
389pub fn try_default_global_config_path() -> Result<std::path::PathBuf, ConfigError> {
402 if let Ok(value) = std::env::var("OPENMW_GLOBAL_CONFIG_PATH")
403 && !value.trim().is_empty()
404 {
405 return Ok(std::path::PathBuf::from(value));
406 }
407
408 if cfg!(windows) || cfg!(target_os = "macos") {
409 return Err(ConfigError::PlatformPathUnavailable("global_config"));
410 }
411
412 if flatpak_mode_enabled() {
413 return Ok(std::path::PathBuf::from("/app/etc/openmw"));
414 }
415
416 Ok(std::path::PathBuf::from("/etc/openmw"))
417}
418
419#[must_use]
426pub fn default_global_config_path() -> std::path::PathBuf {
427 try_default_global_config_path().expect(NO_GLOBAL_CONFIG_DIR)
428}
429
430pub fn try_default_global_path() -> Result<std::path::PathBuf, ConfigError> {
444 if let Ok(value) = std::env::var("OPENMW_GLOBAL_PATH")
445 && !value.trim().is_empty()
446 {
447 return Ok(std::path::PathBuf::from(value));
448 }
449
450 if cfg!(windows) {
451 return Err(ConfigError::PlatformPathUnavailable("global"));
452 }
453
454 if flatpak_mode_enabled() {
459 return Ok(std::path::PathBuf::from("/app/share/games"));
460 }
461
462 if cfg!(target_os = "macos") {
463 return Ok(std::path::PathBuf::from("/Library/Application Support"));
464 }
465
466 Ok(std::path::PathBuf::from("/usr/share/games"))
467}
468
469#[must_use]
474pub fn default_global_path() -> std::path::PathBuf {
475 try_default_global_path().expect(NO_GLOBAL_DIR)
476}
477
478#[cfg(test)]
479pub(crate) fn test_env_lock() -> std::sync::MutexGuard<'static, ()> {
480 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
481 LOCK.get_or_init(|| std::sync::Mutex::new(()))
482 .lock()
483 .unwrap_or_else(std::sync::PoisonError::into_inner)
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use std::ffi::OsString;
490
491 fn snapshot_env(keys: &[&str]) -> Vec<(String, Option<OsString>)> {
492 keys.iter()
493 .map(|key| ((*key).to_string(), std::env::var_os(key)))
494 .collect()
495 }
496
497 fn restore_env(snapshot: Vec<(String, Option<OsString>)>) {
498 for (key, value) in snapshot {
499 unsafe {
501 if let Some(value) = value {
502 std::env::set_var(&key, value);
503 } else {
504 std::env::remove_var(&key);
505 }
506 }
507 }
508 }
509
510 fn unique_temp_dir(name: &str) -> std::path::PathBuf {
511 std::env::temp_dir().join(format!(
512 "openmw_config_{name}_{}_{}",
513 std::process::id(),
514 std::time::SystemTime::now()
515 .duration_since(std::time::UNIX_EPOCH)
516 .unwrap()
517 .as_nanos()
518 ))
519 }
520
521 #[test]
522 fn test_default_data_local_path_is_userdata_data_child() {
523 let _guard = crate::test_env_lock();
524 let snapshot = snapshot_env(&[
525 "OPENMW_CONFIG_USING_FLATPAK",
526 "OPENMW_FLATPAK_ID",
527 "FLATPAK_ID",
528 "OPENMW_GLOBAL_PATH",
529 ]);
530
531 unsafe {
533 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
534 std::env::remove_var("OPENMW_FLATPAK_ID");
535 std::env::remove_var("FLATPAK_ID");
536 std::env::remove_var("OPENMW_GLOBAL_PATH");
537 }
538
539 assert_eq!(
540 default_data_local_path(),
541 default_userdata_path().join("data")
542 );
543
544 restore_env(snapshot);
545 }
546
547 #[test]
548 #[cfg(windows)]
549 fn test_windows_default_paths_contract() {
550 let cfg = default_config_path();
551 let cfg_str = cfg.to_string_lossy().to_lowercase();
552 assert!(cfg_str.contains("my games"));
553 assert!(cfg_str.contains("openmw"));
554 assert_eq!(default_userdata_path(), cfg);
555 }
556
557 #[test]
558 fn test_try_default_config_path_returns_path_or_error() {
559 let _guard = crate::test_env_lock();
560 let snapshot = snapshot_env(&[
561 "OPENMW_CONFIG_USING_FLATPAK",
562 "OPENMW_FLATPAK_ID",
563 "FLATPAK_ID",
564 ]);
565 let _ = try_default_config_path();
566 restore_env(snapshot);
567 }
568
569 #[test]
570 fn test_try_default_local_path_returns_path_or_error() {
571 let _guard = crate::test_env_lock();
572 let snapshot = snapshot_env(&[
573 "OPENMW_CONFIG_USING_FLATPAK",
574 "OPENMW_FLATPAK_ID",
575 "FLATPAK_ID",
576 ]);
577 let _ = try_default_local_path();
578 restore_env(snapshot);
579 }
580
581 #[test]
582 #[cfg(target_os = "linux")]
583 fn test_flatpak_env_flag_forces_flatpak_paths() {
584 let _guard = crate::test_env_lock();
585 let Ok(home) = platform_paths::home_dir() else {
586 return;
587 };
588
589 unsafe {
591 std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "bananas");
592 std::env::remove_var("OPENMW_FLATPAK_ID");
593 std::env::remove_var("FLATPAK_ID");
594 }
595
596 let cfg = try_default_config_path().expect("flatpak config path should resolve");
597 let data = try_default_userdata_path().expect("flatpak userdata path should resolve");
598
599 assert_eq!(
600 cfg,
601 home.join(".var")
602 .join("app")
603 .join(DEFAULT_FLATPAK_APP_ID)
604 .join("config")
605 .join("openmw")
606 );
607 assert_eq!(
608 data,
609 home.join(".var")
610 .join("app")
611 .join(DEFAULT_FLATPAK_APP_ID)
612 .join("data")
613 .join("openmw")
614 );
615
616 unsafe {
618 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
619 }
620 }
621
622 #[test]
623 #[cfg(target_os = "linux")]
624 fn test_flatpak_app_id_override_precedence() {
625 let _guard = crate::test_env_lock();
626 let Ok(home) = platform_paths::home_dir() else {
627 return;
628 };
629
630 unsafe {
632 std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "enabled");
633 std::env::set_var("OPENMW_FLATPAK_ID", "org.example.Override");
634 std::env::set_var("FLATPAK_ID", "org.example.ShouldNotWin");
635 }
636
637 let cfg = try_default_config_path().expect("flatpak config path should resolve");
638 assert_eq!(
639 cfg,
640 home.join(".var")
641 .join("app")
642 .join("org.example.Override")
643 .join("config")
644 .join("openmw")
645 );
646
647 unsafe {
649 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
650 std::env::remove_var("OPENMW_FLATPAK_ID");
651 std::env::remove_var("FLATPAK_ID");
652 }
653 }
654
655 #[test]
656 #[cfg(target_os = "linux")]
657 fn test_flatpak_auto_detect_via_flatpak_id() {
658 let _guard = crate::test_env_lock();
659 let Ok(home) = platform_paths::home_dir() else {
660 return;
661 };
662
663 unsafe {
665 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
666 std::env::remove_var("OPENMW_FLATPAK_ID");
667 std::env::set_var("FLATPAK_ID", "org.example.AutoDetect");
668 }
669
670 let data = try_default_userdata_path().expect("flatpak userdata path should resolve");
671 assert_eq!(
672 data,
673 home.join(".var")
674 .join("app")
675 .join("org.example.AutoDetect")
676 .join("data")
677 .join("openmw")
678 );
679
680 unsafe {
682 std::env::remove_var("FLATPAK_ID");
683 }
684 }
685
686 #[test]
687 fn test_global_path_env_override_has_precedence() {
688 let _guard = crate::test_env_lock();
689 let expected = std::path::PathBuf::from("/opt/openmw/global");
690
691 unsafe {
693 std::env::set_var("OPENMW_GLOBAL_PATH", expected.as_os_str());
694 }
695
696 assert_eq!(
697 try_default_global_path().expect("global override should be used"),
698 expected
699 );
700
701 unsafe {
703 std::env::remove_var("OPENMW_GLOBAL_PATH");
704 }
705 }
706
707 #[test]
708 fn test_root_config_discovery_uses_local_before_global_config() {
709 let local_dir = unique_temp_dir("root_local");
710 let global_dir = unique_temp_dir("root_global");
711 std::fs::create_dir_all(&local_dir).unwrap();
712 std::fs::create_dir_all(&global_dir).unwrap();
713 let local_cfg = local_dir.join("openmw.cfg");
714 let global_cfg = global_dir.join("openmw.cfg");
715 std::fs::write(&local_cfg, "content=Local.esm\n").unwrap();
716 std::fs::write(&global_cfg, "content=Global.esm\n").unwrap();
717
718 assert_eq!(
719 discover_root_config_path(&local_dir, &global_dir).unwrap(),
720 local_cfg
721 );
722
723 let _ = std::fs::remove_file(global_cfg);
724 let _ = std::fs::remove_file(local_cfg);
725 let _ = std::fs::remove_dir_all(global_dir);
726 let _ = std::fs::remove_dir_all(local_dir);
727 }
728
729 #[test]
730 fn test_root_config_discovery_uses_global_config_when_local_missing() {
731 let local_dir = unique_temp_dir("root_local_missing");
732 let global_dir = unique_temp_dir("root_global_present");
733 std::fs::create_dir_all(&local_dir).unwrap();
734 std::fs::create_dir_all(&global_dir).unwrap();
735 let global_cfg = global_dir.join("openmw.cfg");
736 std::fs::write(&global_cfg, "content=Global.esm\n").unwrap();
737
738 assert_eq!(
739 discover_root_config_path(&local_dir, &global_dir).unwrap(),
740 global_cfg
741 );
742
743 let _ = std::fs::remove_file(global_dir.join("openmw.cfg"));
744 let _ = std::fs::remove_dir_all(global_dir);
745 let _ = std::fs::remove_dir_all(local_dir);
746 }
747
748 #[test]
749 fn test_root_config_discovery_errors_without_user_fallback() {
750 let local_dir = unique_temp_dir("root_none_local");
751 let global_dir = unique_temp_dir("root_none_global");
752 std::fs::create_dir_all(&local_dir).unwrap();
753 std::fs::create_dir_all(&global_dir).unwrap();
754
755 assert!(matches!(
756 discover_root_config_path(&local_dir, &global_dir),
757 Err(ConfigError::CannotFindRootConfig { .. })
758 ));
759
760 let _ = std::fs::remove_dir_all(global_dir);
761 let _ = std::fs::remove_dir_all(local_dir);
762 }
763
764 #[test]
765 #[cfg(target_os = "linux")]
766 fn test_global_config_path_is_not_global_data_token_path() {
767 let _guard = crate::test_env_lock();
768
769 unsafe {
771 std::env::remove_var("OPENMW_GLOBAL_PATH");
772 std::env::remove_var("OPENMW_GLOBAL_CONFIG_PATH");
773 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
774 std::env::remove_var("FLATPAK_ID");
775 }
776
777 assert_eq!(
778 try_default_global_path().unwrap(),
779 std::path::PathBuf::from("/usr/share/games")
780 );
781 assert_eq!(
782 try_default_global_config_path().unwrap(),
783 std::path::PathBuf::from("/etc/openmw")
784 );
785 }
786
787 #[test]
788 fn test_global_config_path_env_override_has_precedence() {
789 let _guard = crate::test_env_lock();
790 let expected = std::path::PathBuf::from("/opt/openmw/etc/openmw");
791
792 unsafe {
794 std::env::set_var("OPENMW_GLOBAL_CONFIG_PATH", expected.as_os_str());
795 }
796
797 assert_eq!(
798 try_default_global_config_path().expect("global config override should be used"),
799 expected
800 );
801
802 unsafe {
804 std::env::remove_var("OPENMW_GLOBAL_CONFIG_PATH");
805 }
806 }
807
808 #[test]
809 #[cfg(not(windows))]
810 fn test_global_path_default_is_platform_or_flatpak_value() {
811 let _guard = crate::test_env_lock();
812
813 unsafe {
815 std::env::remove_var("OPENMW_GLOBAL_PATH");
816 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
817 std::env::remove_var("FLATPAK_ID");
818 }
819
820 if cfg!(target_os = "macos") {
821 assert_eq!(
822 try_default_global_path().expect("macOS global path should resolve"),
823 std::path::PathBuf::from("/Library/Application Support")
824 );
825 } else if flatpak_mode_enabled() {
826 assert_eq!(
827 try_default_global_path().expect("flatpak global path should resolve"),
828 std::path::PathBuf::from("/app/share/games")
829 );
830 } else {
831 assert_eq!(
832 try_default_global_path().expect("unix global path should resolve"),
833 std::path::PathBuf::from("/usr/share/games")
834 );
835 }
836 }
837
838 #[test]
839 #[cfg(windows)]
840 fn test_global_path_is_unavailable_on_windows_without_override() {
841 let _guard = crate::test_env_lock();
842
843 unsafe {
845 std::env::remove_var("OPENMW_GLOBAL_PATH");
846 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
847 std::env::remove_var("FLATPAK_ID");
848 }
849
850 assert!(matches!(
851 try_default_global_path(),
852 Err(ConfigError::PlatformPathUnavailable("global"))
853 ));
854 }
855
856 #[test]
857 #[cfg(not(target_os = "linux"))]
858 fn test_flatpak_mode_is_ignored_off_linux() {
859 let _guard = crate::test_env_lock();
860
861 unsafe {
863 std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "1");
864 std::env::set_var("FLATPAK_ID", "org.example.Flatpak");
865 std::env::remove_var("OPENMW_GLOBAL_PATH");
866 }
867
868 assert!(!flatpak_mode_enabled());
869
870 assert_eq!(
871 try_default_config_path().ok(),
872 platform_paths::config_dir().ok()
873 );
874 assert_eq!(
875 try_default_userdata_path().ok(),
876 platform_paths::data_dir().ok()
877 );
878
879 if cfg!(windows) {
880 assert!(matches!(
881 try_default_global_path(),
882 Err(ConfigError::PlatformPathUnavailable("global"))
883 ));
884 } else if cfg!(target_os = "macos") {
885 assert_eq!(
886 try_default_global_path().expect("macOS global path should resolve"),
887 std::path::PathBuf::from("/Library/Application Support")
888 );
889 }
890
891 unsafe {
893 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
894 std::env::remove_var("FLATPAK_ID");
895 std::env::remove_var("OPENMW_GLOBAL_PATH");
896 }
897 }
898}