1mod config;
35#[cfg(feature = "lua")]
36pub mod lua;
37mod platform_paths;
38
39pub use config::{
40 ConfigChainEntry, ConfigChainStatus, OpenMWConfiguration,
41 directorysetting::DirectorySetting,
42 encodingsetting::{EncodingSetting, EncodingType},
43 error::ConfigError,
44 filesetting::FileSetting,
45 gamesetting::GameSettingType,
46 genericsetting::GenericSetting,
47};
48
49#[cfg(feature = "lua")]
50pub use lua::create_lua_module;
51
52pub(crate) trait GameSetting: std::fmt::Display {
53 fn meta(&self) -> &GameSettingMeta;
54}
55
56#[derive(Debug, Clone, Eq, PartialEq)]
62pub struct GameSettingMeta {
63 source_config: std::path::PathBuf,
64 comment: String,
65}
66
67impl GameSettingMeta {
68 #[must_use]
69 pub fn source_config(&self) -> &std::path::Path {
70 &self.source_config
71 }
72
73 #[must_use]
74 pub fn comment(&self) -> &str {
75 &self.comment
76 }
77}
78
79const NO_CONFIG_DIR: &str = "FAILURE: COULD NOT READ CONFIG DIRECTORY";
80const NO_LOCAL_DIR: &str = "FAILURE: COULD NOT READ LOCAL DIRECTORY";
81const NO_GLOBAL_DIR: &str = "FAILURE: COULD NOT READ GLOBAL DIRECTORY";
82const DEFAULT_FLATPAK_APP_ID: &str = "org.openmw.OpenMW";
83
84fn has_flatpak_info_file() -> bool {
85 use std::sync::OnceLock;
86
87 static HAS_FLATPAK_INFO: OnceLock<bool> = OnceLock::new();
88 *HAS_FLATPAK_INFO.get_or_init(|| std::path::Path::new("/.flatpak-info").exists())
89}
90
91fn flatpak_mode_enabled() -> bool {
92 #[cfg(not(target_os = "linux"))]
93 {
94 return false;
95 }
96
97 #[cfg(target_os = "linux")]
98 {
99 if std::env::var_os("OPENMW_CONFIG_USING_FLATPAK").is_some() {
100 return true;
101 }
102
103 std::env::var_os("FLATPAK_ID").is_some() || has_flatpak_info_file()
104 }
105}
106
107fn flatpak_app_id() -> String {
108 std::env::var("OPENMW_FLATPAK_ID")
109 .ok()
110 .filter(|value| !value.trim().is_empty())
111 .or_else(|| {
112 std::env::var("FLATPAK_ID")
113 .ok()
114 .filter(|value| !value.trim().is_empty())
115 })
116 .unwrap_or_else(|| DEFAULT_FLATPAK_APP_ID.to_string())
117}
118
119fn flatpak_userconfig_path() -> Result<std::path::PathBuf, ConfigError> {
120 platform_paths::home_dir().map(|home| {
121 home.join(".var")
122 .join("app")
123 .join(flatpak_app_id())
124 .join("config")
125 .join("openmw")
126 })
127}
128
129fn flatpak_userdata_path() -> Result<std::path::PathBuf, ConfigError> {
130 platform_paths::home_dir().map(|home| {
131 home.join(".var")
132 .join("app")
133 .join(flatpak_app_id())
134 .join("data")
135 .join("openmw")
136 })
137}
138
139pub fn try_default_config_path() -> Result<std::path::PathBuf, ConfigError> {
151 #[cfg(target_os = "android")]
152 return Ok(std::path::PathBuf::from(
153 "/storage/emulated/0/Alpha3/config",
154 ));
155
156 #[cfg(not(target_os = "android"))]
157 {
158 if flatpak_mode_enabled() {
159 return flatpak_userconfig_path();
160 }
161
162 platform_paths::config_dir().map_err(|_| ConfigError::PlatformPathUnavailable("config"))
163 }
164}
165
166#[must_use]
174pub fn default_config_path() -> std::path::PathBuf {
175 try_default_config_path().expect(NO_CONFIG_DIR)
176}
177
178pub fn try_default_userdata_path() -> Result<std::path::PathBuf, ConfigError> {
190 #[cfg(target_os = "android")]
191 return Ok(std::path::PathBuf::from("/storage/emulated/0/Alpha3"));
192
193 #[cfg(not(target_os = "android"))]
194 {
195 if flatpak_mode_enabled() {
196 return flatpak_userdata_path();
197 }
198
199 platform_paths::data_dir().map_err(|_| ConfigError::PlatformPathUnavailable("userdata"))
200 }
201}
202
203#[must_use]
211pub fn default_userdata_path() -> std::path::PathBuf {
212 try_default_userdata_path().expect("FAILURE: COULD NOT READ USERDATA DIRECTORY")
213}
214
215#[must_use]
220pub fn default_data_local_path() -> std::path::PathBuf {
221 default_userdata_path().join("data")
222}
223
224pub fn try_default_local_path() -> Result<std::path::PathBuf, ConfigError> {
234 let exe = std::env::current_exe()?;
235
236 #[cfg(target_os = "macos")]
237 {
238 if let Some(macos_dir) = exe.parent()
239 && macos_dir.file_name() == Some(std::ffi::OsStr::new("MacOS"))
240 && let Some(contents_dir) = macos_dir.parent()
241 && contents_dir.file_name() == Some(std::ffi::OsStr::new("Contents"))
242 {
243 return Ok(contents_dir.join("Resources"));
244 }
245 }
246
247 exe.parent()
248 .map(std::path::Path::to_path_buf)
249 .ok_or(ConfigError::PlatformPathUnavailable("local"))
250}
251
252#[must_use]
257pub fn default_local_path() -> std::path::PathBuf {
258 try_default_local_path().expect(NO_LOCAL_DIR)
259}
260
261pub fn try_default_global_path() -> Result<std::path::PathBuf, ConfigError> {
275 if let Ok(value) = std::env::var("OPENMW_GLOBAL_PATH")
276 && !value.trim().is_empty()
277 {
278 return Ok(std::path::PathBuf::from(value));
279 }
280
281 if cfg!(windows) {
282 return Err(ConfigError::PlatformPathUnavailable("global"));
283 }
284
285 if flatpak_mode_enabled() {
290 return Ok(std::path::PathBuf::from("/app/share/games"));
291 }
292
293 if cfg!(target_os = "macos") {
294 return Ok(std::path::PathBuf::from("/Library/Application Support"));
295 }
296
297 Ok(std::path::PathBuf::from("/usr/share/games"))
298}
299
300#[must_use]
305pub fn default_global_path() -> std::path::PathBuf {
306 try_default_global_path().expect(NO_GLOBAL_DIR)
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use std::ffi::OsString;
313 use std::sync::Mutex;
314
315 static ENV_LOCK: Mutex<()> = Mutex::new(());
316
317 fn snapshot_env(keys: &[&str]) -> Vec<(String, Option<OsString>)> {
318 keys.iter()
319 .map(|key| ((*key).to_string(), std::env::var_os(key)))
320 .collect()
321 }
322
323 fn restore_env(snapshot: Vec<(String, Option<OsString>)>) {
324 for (key, value) in snapshot {
325 unsafe {
327 if let Some(value) = value {
328 std::env::set_var(&key, value);
329 } else {
330 std::env::remove_var(&key);
331 }
332 }
333 }
334 }
335
336 #[test]
337 fn test_default_data_local_path_is_userdata_data_child() {
338 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
339 let snapshot = snapshot_env(&[
340 "OPENMW_CONFIG_USING_FLATPAK",
341 "OPENMW_FLATPAK_ID",
342 "FLATPAK_ID",
343 "OPENMW_GLOBAL_PATH",
344 ]);
345
346 unsafe {
348 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
349 std::env::remove_var("OPENMW_FLATPAK_ID");
350 std::env::remove_var("FLATPAK_ID");
351 std::env::remove_var("OPENMW_GLOBAL_PATH");
352 }
353
354 assert_eq!(
355 default_data_local_path(),
356 default_userdata_path().join("data")
357 );
358
359 restore_env(snapshot);
360 }
361
362 #[test]
363 #[cfg(windows)]
364 fn test_windows_default_paths_contract() {
365 let cfg = default_config_path();
366 let cfg_str = cfg.to_string_lossy().to_lowercase();
367 assert!(cfg_str.contains("my games"));
368 assert!(cfg_str.contains("openmw"));
369 assert_eq!(default_userdata_path(), cfg);
370 }
371
372 #[test]
373 fn test_try_default_config_path_returns_path_or_error() {
374 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
375 let snapshot = snapshot_env(&[
376 "OPENMW_CONFIG_USING_FLATPAK",
377 "OPENMW_FLATPAK_ID",
378 "FLATPAK_ID",
379 ]);
380 let _ = try_default_config_path();
381 restore_env(snapshot);
382 }
383
384 #[test]
385 fn test_try_default_local_path_returns_path_or_error() {
386 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
387 let snapshot = snapshot_env(&[
388 "OPENMW_CONFIG_USING_FLATPAK",
389 "OPENMW_FLATPAK_ID",
390 "FLATPAK_ID",
391 ]);
392 let _ = try_default_local_path();
393 restore_env(snapshot);
394 }
395
396 #[test]
397 #[cfg(target_os = "linux")]
398 fn test_flatpak_env_flag_forces_flatpak_paths() {
399 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
400 let Ok(home) = platform_paths::home_dir() else {
401 return;
402 };
403
404 unsafe {
406 std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "bananas");
407 std::env::remove_var("OPENMW_FLATPAK_ID");
408 std::env::remove_var("FLATPAK_ID");
409 }
410
411 let cfg = try_default_config_path().expect("flatpak config path should resolve");
412 let data = try_default_userdata_path().expect("flatpak userdata path should resolve");
413
414 assert_eq!(
415 cfg,
416 home.join(".var")
417 .join("app")
418 .join(DEFAULT_FLATPAK_APP_ID)
419 .join("config")
420 .join("openmw")
421 );
422 assert_eq!(
423 data,
424 home.join(".var")
425 .join("app")
426 .join(DEFAULT_FLATPAK_APP_ID)
427 .join("data")
428 .join("openmw")
429 );
430
431 unsafe {
433 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
434 }
435 }
436
437 #[test]
438 #[cfg(target_os = "linux")]
439 fn test_flatpak_app_id_override_precedence() {
440 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
441 let Ok(home) = platform_paths::home_dir() else {
442 return;
443 };
444
445 unsafe {
447 std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "enabled");
448 std::env::set_var("OPENMW_FLATPAK_ID", "org.example.Override");
449 std::env::set_var("FLATPAK_ID", "org.example.ShouldNotWin");
450 }
451
452 let cfg = try_default_config_path().expect("flatpak config path should resolve");
453 assert_eq!(
454 cfg,
455 home.join(".var")
456 .join("app")
457 .join("org.example.Override")
458 .join("config")
459 .join("openmw")
460 );
461
462 unsafe {
464 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
465 std::env::remove_var("OPENMW_FLATPAK_ID");
466 std::env::remove_var("FLATPAK_ID");
467 }
468 }
469
470 #[test]
471 #[cfg(target_os = "linux")]
472 fn test_flatpak_auto_detect_via_flatpak_id() {
473 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
474 let Ok(home) = platform_paths::home_dir() else {
475 return;
476 };
477
478 unsafe {
480 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
481 std::env::remove_var("OPENMW_FLATPAK_ID");
482 std::env::set_var("FLATPAK_ID", "org.example.AutoDetect");
483 }
484
485 let data = try_default_userdata_path().expect("flatpak userdata path should resolve");
486 assert_eq!(
487 data,
488 home.join(".var")
489 .join("app")
490 .join("org.example.AutoDetect")
491 .join("data")
492 .join("openmw")
493 );
494
495 unsafe {
497 std::env::remove_var("FLATPAK_ID");
498 }
499 }
500
501 #[test]
502 fn test_global_path_env_override_has_precedence() {
503 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
504 let expected = std::path::PathBuf::from("/opt/openmw/global");
505
506 unsafe {
508 std::env::set_var("OPENMW_GLOBAL_PATH", expected.as_os_str());
509 }
510
511 assert_eq!(
512 try_default_global_path().expect("global override should be used"),
513 expected
514 );
515
516 unsafe {
518 std::env::remove_var("OPENMW_GLOBAL_PATH");
519 }
520 }
521
522 #[test]
523 #[cfg(not(windows))]
524 fn test_global_path_default_is_platform_or_flatpak_value() {
525 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
526
527 unsafe {
529 std::env::remove_var("OPENMW_GLOBAL_PATH");
530 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
531 std::env::remove_var("FLATPAK_ID");
532 }
533
534 if cfg!(target_os = "macos") {
535 assert_eq!(
536 try_default_global_path().expect("macOS global path should resolve"),
537 std::path::PathBuf::from("/Library/Application Support")
538 );
539 } else if flatpak_mode_enabled() {
540 assert_eq!(
541 try_default_global_path().expect("flatpak global path should resolve"),
542 std::path::PathBuf::from("/app/share/games")
543 );
544 } else {
545 assert_eq!(
546 try_default_global_path().expect("unix global path should resolve"),
547 std::path::PathBuf::from("/usr/share/games")
548 );
549 }
550 }
551
552 #[test]
553 #[cfg(windows)]
554 fn test_global_path_is_unavailable_on_windows_without_override() {
555 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
556
557 unsafe {
559 std::env::remove_var("OPENMW_GLOBAL_PATH");
560 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
561 std::env::remove_var("FLATPAK_ID");
562 }
563
564 assert!(matches!(
565 try_default_global_path(),
566 Err(ConfigError::PlatformPathUnavailable("global"))
567 ));
568 }
569
570 #[test]
571 #[cfg(not(target_os = "linux"))]
572 fn test_flatpak_mode_is_ignored_off_linux() {
573 let _guard = ENV_LOCK.lock().expect("env lock poisoned");
574
575 unsafe {
577 std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "1");
578 std::env::set_var("FLATPAK_ID", "org.example.Flatpak");
579 std::env::remove_var("OPENMW_GLOBAL_PATH");
580 }
581
582 assert!(!flatpak_mode_enabled());
583
584 assert_eq!(
585 try_default_config_path().ok(),
586 platform_paths::config_dir().ok()
587 );
588 assert_eq!(
589 try_default_userdata_path().ok(),
590 platform_paths::data_dir().ok()
591 );
592
593 if cfg!(windows) {
594 assert!(matches!(
595 try_default_global_path(),
596 Err(ConfigError::PlatformPathUnavailable("global"))
597 ));
598 } else if cfg!(target_os = "macos") {
599 assert_eq!(
600 try_default_global_path().expect("macOS global path should resolve"),
601 std::path::PathBuf::from("/Library/Application Support")
602 );
603 }
604
605 unsafe {
607 std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
608 std::env::remove_var("FLATPAK_ID");
609 std::env::remove_var("OPENMW_GLOBAL_PATH");
610 }
611 }
612}