mod config;
#[cfg(feature = "lua")]
pub mod lua;
mod platform_paths;
pub use config::{
ConfigChainEntry, ConfigChainStatus, OpenMWConfiguration,
directorysetting::DirectorySetting,
encodingsetting::{EncodingSetting, EncodingType},
error::ConfigError,
filesetting::FileSetting,
gamesetting::GameSettingType,
genericsetting::GenericSetting,
};
#[cfg(feature = "lua")]
pub use lua::create_lua_module;
pub(crate) trait GameSetting: std::fmt::Display {
fn meta(&self) -> &GameSettingMeta;
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct GameSettingMeta {
source_config: std::path::PathBuf,
comment: String,
}
impl GameSettingMeta {
#[must_use]
pub fn source_config(&self) -> &std::path::Path {
&self.source_config
}
#[must_use]
pub fn comment(&self) -> &str {
&self.comment
}
}
const NO_CONFIG_DIR: &str = "FAILURE: COULD NOT READ CONFIG DIRECTORY";
const NO_LOCAL_DIR: &str = "FAILURE: COULD NOT READ LOCAL DIRECTORY";
const NO_GLOBAL_DIR: &str = "FAILURE: COULD NOT READ GLOBAL DIRECTORY";
const DEFAULT_FLATPAK_APP_ID: &str = "org.openmw.OpenMW";
fn has_flatpak_info_file() -> bool {
use std::sync::OnceLock;
static HAS_FLATPAK_INFO: OnceLock<bool> = OnceLock::new();
*HAS_FLATPAK_INFO.get_or_init(|| std::path::Path::new("/.flatpak-info").exists())
}
fn flatpak_mode_enabled() -> bool {
#[cfg(not(target_os = "linux"))]
{
return false;
}
#[cfg(target_os = "linux")]
{
if std::env::var_os("OPENMW_CONFIG_USING_FLATPAK").is_some() {
return true;
}
std::env::var_os("FLATPAK_ID").is_some() || has_flatpak_info_file()
}
}
fn flatpak_app_id() -> String {
std::env::var("OPENMW_FLATPAK_ID")
.ok()
.filter(|value| !value.trim().is_empty())
.or_else(|| {
std::env::var("FLATPAK_ID")
.ok()
.filter(|value| !value.trim().is_empty())
})
.unwrap_or_else(|| DEFAULT_FLATPAK_APP_ID.to_string())
}
fn flatpak_userconfig_path() -> Result<std::path::PathBuf, ConfigError> {
platform_paths::home_dir().map(|home| {
home.join(".var")
.join("app")
.join(flatpak_app_id())
.join("config")
.join("openmw")
})
}
fn flatpak_userdata_path() -> Result<std::path::PathBuf, ConfigError> {
platform_paths::home_dir().map(|home| {
home.join(".var")
.join("app")
.join(flatpak_app_id())
.join("data")
.join("openmw")
})
}
pub fn try_default_config_path() -> Result<std::path::PathBuf, ConfigError> {
#[cfg(target_os = "android")]
return Ok(std::path::PathBuf::from(
"/storage/emulated/0/Alpha3/config",
));
#[cfg(not(target_os = "android"))]
{
if flatpak_mode_enabled() {
return flatpak_userconfig_path();
}
platform_paths::config_dir().map_err(|_| ConfigError::PlatformPathUnavailable("config"))
}
}
#[must_use]
pub fn default_config_path() -> std::path::PathBuf {
try_default_config_path().expect(NO_CONFIG_DIR)
}
pub fn try_default_userdata_path() -> Result<std::path::PathBuf, ConfigError> {
#[cfg(target_os = "android")]
return Ok(std::path::PathBuf::from("/storage/emulated/0/Alpha3"));
#[cfg(not(target_os = "android"))]
{
if flatpak_mode_enabled() {
return flatpak_userdata_path();
}
platform_paths::data_dir().map_err(|_| ConfigError::PlatformPathUnavailable("userdata"))
}
}
#[must_use]
pub fn default_userdata_path() -> std::path::PathBuf {
try_default_userdata_path().expect("FAILURE: COULD NOT READ USERDATA DIRECTORY")
}
#[must_use]
pub fn default_data_local_path() -> std::path::PathBuf {
default_userdata_path().join("data")
}
pub fn try_default_local_path() -> Result<std::path::PathBuf, ConfigError> {
let exe = std::env::current_exe()?;
#[cfg(target_os = "macos")]
{
if let Some(macos_dir) = exe.parent()
&& macos_dir.file_name() == Some(std::ffi::OsStr::new("MacOS"))
&& let Some(contents_dir) = macos_dir.parent()
&& contents_dir.file_name() == Some(std::ffi::OsStr::new("Contents"))
{
return Ok(contents_dir.join("Resources"));
}
}
exe.parent()
.map(std::path::Path::to_path_buf)
.ok_or(ConfigError::PlatformPathUnavailable("local"))
}
#[must_use]
pub fn default_local_path() -> std::path::PathBuf {
try_default_local_path().expect(NO_LOCAL_DIR)
}
pub fn try_default_global_path() -> Result<std::path::PathBuf, ConfigError> {
if let Ok(value) = std::env::var("OPENMW_GLOBAL_PATH")
&& !value.trim().is_empty()
{
return Ok(std::path::PathBuf::from(value));
}
if cfg!(windows) {
return Err(ConfigError::PlatformPathUnavailable("global"));
}
if flatpak_mode_enabled() {
return Ok(std::path::PathBuf::from("/app/share/games"));
}
if cfg!(target_os = "macos") {
return Ok(std::path::PathBuf::from("/Library/Application Support"));
}
Ok(std::path::PathBuf::from("/usr/share/games"))
}
#[must_use]
pub fn default_global_path() -> std::path::PathBuf {
try_default_global_path().expect(NO_GLOBAL_DIR)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn snapshot_env(keys: &[&str]) -> Vec<(String, Option<OsString>)> {
keys.iter()
.map(|key| ((*key).to_string(), std::env::var_os(key)))
.collect()
}
fn restore_env(snapshot: Vec<(String, Option<OsString>)>) {
for (key, value) in snapshot {
unsafe {
if let Some(value) = value {
std::env::set_var(&key, value);
} else {
std::env::remove_var(&key);
}
}
}
}
#[test]
fn test_default_data_local_path_is_userdata_data_child() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let snapshot = snapshot_env(&[
"OPENMW_CONFIG_USING_FLATPAK",
"OPENMW_FLATPAK_ID",
"FLATPAK_ID",
"OPENMW_GLOBAL_PATH",
]);
unsafe {
std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
std::env::remove_var("OPENMW_FLATPAK_ID");
std::env::remove_var("FLATPAK_ID");
std::env::remove_var("OPENMW_GLOBAL_PATH");
}
assert_eq!(
default_data_local_path(),
default_userdata_path().join("data")
);
restore_env(snapshot);
}
#[test]
#[cfg(windows)]
fn test_windows_default_paths_contract() {
let cfg = default_config_path();
let cfg_str = cfg.to_string_lossy().to_lowercase();
assert!(cfg_str.contains("my games"));
assert!(cfg_str.contains("openmw"));
assert_eq!(default_userdata_path(), cfg);
}
#[test]
fn test_try_default_config_path_returns_path_or_error() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let snapshot = snapshot_env(&[
"OPENMW_CONFIG_USING_FLATPAK",
"OPENMW_FLATPAK_ID",
"FLATPAK_ID",
]);
let _ = try_default_config_path();
restore_env(snapshot);
}
#[test]
fn test_try_default_local_path_returns_path_or_error() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let snapshot = snapshot_env(&[
"OPENMW_CONFIG_USING_FLATPAK",
"OPENMW_FLATPAK_ID",
"FLATPAK_ID",
]);
let _ = try_default_local_path();
restore_env(snapshot);
}
#[test]
#[cfg(target_os = "linux")]
fn test_flatpak_env_flag_forces_flatpak_paths() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let Ok(home) = platform_paths::home_dir() else {
return;
};
unsafe {
std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "bananas");
std::env::remove_var("OPENMW_FLATPAK_ID");
std::env::remove_var("FLATPAK_ID");
}
let cfg = try_default_config_path().expect("flatpak config path should resolve");
let data = try_default_userdata_path().expect("flatpak userdata path should resolve");
assert_eq!(
cfg,
home.join(".var")
.join("app")
.join(DEFAULT_FLATPAK_APP_ID)
.join("config")
.join("openmw")
);
assert_eq!(
data,
home.join(".var")
.join("app")
.join(DEFAULT_FLATPAK_APP_ID)
.join("data")
.join("openmw")
);
unsafe {
std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_flatpak_app_id_override_precedence() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let Ok(home) = platform_paths::home_dir() else {
return;
};
unsafe {
std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "enabled");
std::env::set_var("OPENMW_FLATPAK_ID", "org.example.Override");
std::env::set_var("FLATPAK_ID", "org.example.ShouldNotWin");
}
let cfg = try_default_config_path().expect("flatpak config path should resolve");
assert_eq!(
cfg,
home.join(".var")
.join("app")
.join("org.example.Override")
.join("config")
.join("openmw")
);
unsafe {
std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
std::env::remove_var("OPENMW_FLATPAK_ID");
std::env::remove_var("FLATPAK_ID");
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_flatpak_auto_detect_via_flatpak_id() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let Ok(home) = platform_paths::home_dir() else {
return;
};
unsafe {
std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
std::env::remove_var("OPENMW_FLATPAK_ID");
std::env::set_var("FLATPAK_ID", "org.example.AutoDetect");
}
let data = try_default_userdata_path().expect("flatpak userdata path should resolve");
assert_eq!(
data,
home.join(".var")
.join("app")
.join("org.example.AutoDetect")
.join("data")
.join("openmw")
);
unsafe {
std::env::remove_var("FLATPAK_ID");
}
}
#[test]
fn test_global_path_env_override_has_precedence() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let expected = std::path::PathBuf::from("/opt/openmw/global");
unsafe {
std::env::set_var("OPENMW_GLOBAL_PATH", expected.as_os_str());
}
assert_eq!(
try_default_global_path().expect("global override should be used"),
expected
);
unsafe {
std::env::remove_var("OPENMW_GLOBAL_PATH");
}
}
#[test]
#[cfg(not(windows))]
fn test_global_path_default_is_platform_or_flatpak_value() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
unsafe {
std::env::remove_var("OPENMW_GLOBAL_PATH");
std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
std::env::remove_var("FLATPAK_ID");
}
if cfg!(target_os = "macos") {
assert_eq!(
try_default_global_path().expect("macOS global path should resolve"),
std::path::PathBuf::from("/Library/Application Support")
);
} else if flatpak_mode_enabled() {
assert_eq!(
try_default_global_path().expect("flatpak global path should resolve"),
std::path::PathBuf::from("/app/share/games")
);
} else {
assert_eq!(
try_default_global_path().expect("unix global path should resolve"),
std::path::PathBuf::from("/usr/share/games")
);
}
}
#[test]
#[cfg(windows)]
fn test_global_path_is_unavailable_on_windows_without_override() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
unsafe {
std::env::remove_var("OPENMW_GLOBAL_PATH");
std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
std::env::remove_var("FLATPAK_ID");
}
assert!(matches!(
try_default_global_path(),
Err(ConfigError::PlatformPathUnavailable("global"))
));
}
#[test]
#[cfg(not(target_os = "linux"))]
fn test_flatpak_mode_is_ignored_off_linux() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
unsafe {
std::env::set_var("OPENMW_CONFIG_USING_FLATPAK", "1");
std::env::set_var("FLATPAK_ID", "org.example.Flatpak");
std::env::remove_var("OPENMW_GLOBAL_PATH");
}
assert!(!flatpak_mode_enabled());
assert_eq!(
try_default_config_path().ok(),
platform_paths::config_dir().ok()
);
assert_eq!(
try_default_userdata_path().ok(),
platform_paths::data_dir().ok()
);
if cfg!(windows) {
assert!(matches!(
try_default_global_path(),
Err(ConfigError::PlatformPathUnavailable("global"))
));
} else if cfg!(target_os = "macos") {
assert_eq!(
try_default_global_path().expect("macOS global path should resolve"),
std::path::PathBuf::from("/Library/Application Support")
);
}
unsafe {
std::env::remove_var("OPENMW_CONFIG_USING_FLATPAK");
std::env::remove_var("FLATPAK_ID");
std::env::remove_var("OPENMW_GLOBAL_PATH");
}
}
}