use std::cmp::Ordering;
use std::ffi::OsStr;
use std::fs::{read_dir, DirEntry, File, FileType};
use std::io::{BufRead, BufReader};
use std::iter::once;
use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;
use crate::enums::{Error, GameId, LoadOrderMethod};
use crate::ini::{test_files, use_my_games_directory};
use crate::is_enderal;
use crate::load_order::{
AsteriskBasedLoadOrder, OpenMWLoadOrder, TextfileBasedLoadOrder, TimestampBasedLoadOrder,
WritableLoadOrder,
};
use crate::openmw_config;
use crate::plugin::{has_plugin_extension, ActiveState, Plugin};
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct GameSettings {
id: GameId,
game_path: PathBuf,
plugins_directory: PathBuf,
plugins_file_path: PathBuf,
my_games_path: PathBuf,
load_order_path: Option<PathBuf>,
implicitly_active_plugins: Vec<String>,
early_loading_plugins: Vec<String>,
test_files: Vec<String>,
additional_plugins_directories: Vec<PathBuf>,
}
const SKYRIM_HARDCODED_PLUGINS: &[&str] = &["Skyrim.esm"];
const SKYRIM_SE_HARDCODED_PLUGINS: &[&str] = &[
"Skyrim.esm",
"Update.esm",
"Dawnguard.esm",
"HearthFires.esm",
"Dragonborn.esm",
];
const SKYRIM_VR_HARDCODED_PLUGINS: &[&str] = &[
"Skyrim.esm",
"Update.esm",
"Dawnguard.esm",
"HearthFires.esm",
"Dragonborn.esm",
"SkyrimVR.esm",
];
const FALLOUT4_HARDCODED_PLUGINS: &[&str] = &[
"Fallout4.esm",
"DLCRobot.esm",
"DLCworkshop01.esm",
"DLCCoast.esm",
"DLCworkshop02.esm",
"DLCworkshop03.esm",
"DLCNukaWorld.esm",
"DLCUltraHighResolution.esm",
];
const FALLOUT4VR_HARDCODED_PLUGINS: &[&str] = &["Fallout4.esm", "Fallout4_VR.esm"];
pub(crate) const STARFIELD_HARDCODED_PLUGINS: &[&str] = &[
"Starfield.esm",
"Constellation.esm",
"OldMars.esm",
"ShatteredSpace.esm",
"SFBGS003.esm",
"SFBGS004.esm",
"SFBGS006.esm",
"SFBGS007.esm",
"SFBGS008.esm",
"SFBGS00D.esm",
"SFBGS047.esm",
"SFBGS050.esm",
];
const OPENMW_HARDCODED_PLUGINS: &[&str] = &["builtin.omwscripts"];
const MS_FO4_FAR_HARBOR_PATH: &str = "../../Fallout 4- Far Harbor (PC)/Content/Data";
const MS_FO4_NUKA_WORLD_PATH: &str = "../../Fallout 4- Nuka-World (PC)/Content/Data";
const MS_FO4_AUTOMATRON_PATH: &str = "../../Fallout 4- Automatron (PC)/Content/Data";
const MS_FO4_TEXTURE_PACK_PATH: &str = "../../Fallout 4- High Resolution Texture Pack/Content/Data";
const MS_FO4_WASTELAND_PATH: &str = "../../Fallout 4- Wasteland Workshop (PC)/Content/Data";
const MS_FO4_CONTRAPTIONS_PATH: &str = "../../Fallout 4- Contraptions Workshop (PC)/Content/Data";
const MS_FO4_VAULT_TEC_PATH: &str = "../../Fallout 4- Vault-Tec Workshop (PC)/Content/Data";
const PLUGINS_TXT: &str = "Plugins.txt";
const OBLIVION_REMASTERED_RELATIVE_DATA_PATH: &str = "OblivionRemastered/Content/Dev/ObvData/Data";
struct ImplicitlyActivePlugins {
early_loading_plugins: Vec<String>,
test_files: Vec<String>,
all: Vec<String>,
}
impl GameSettings {
pub fn new(game_id: GameId, game_path: &Path) -> Result<GameSettings, Error> {
let local_path = local_path(game_id, game_path)?.unwrap_or_default();
GameSettings::with_local_path(game_id, game_path, &local_path)
}
pub fn with_local_path(
game_id: GameId,
game_path: &Path,
local_path: &Path,
) -> Result<GameSettings, Error> {
let my_games_path = my_games_path(game_id, game_path, local_path)?.unwrap_or_default();
GameSettings::with_local_and_my_games_paths(game_id, game_path, local_path, my_games_path)
}
pub(crate) fn with_local_and_my_games_paths(
game_id: GameId,
game_path: &Path,
local_path: &Path,
my_games_path: PathBuf,
) -> Result<GameSettings, Error> {
let plugins_file_path = plugins_file_path(game_id, game_path, local_path)?;
let load_order_path = load_order_path(game_id, local_path, &plugins_file_path);
let plugins_directory = plugins_directory(game_id, game_path, local_path)?;
let additional_plugins_directories =
additional_plugins_directories(game_id, game_path, &my_games_path)?;
let ImplicitlyActivePlugins {
early_loading_plugins,
test_files,
all: implicitly_active_plugins,
} = GameSettings::load_implicitly_active_plugins(
game_id,
game_path,
&my_games_path,
&plugins_directory,
&additional_plugins_directories,
)?;
Ok(GameSettings {
id: game_id,
game_path: game_path.to_path_buf(),
plugins_directory,
plugins_file_path,
load_order_path,
my_games_path,
implicitly_active_plugins,
early_loading_plugins,
test_files,
additional_plugins_directories,
})
}
pub fn id(&self) -> GameId {
self.id
}
pub(crate) fn supports_blueprint_ships_plugins(&self) -> bool {
self.id == GameId::Starfield
}
pub fn load_order_method(&self) -> LoadOrderMethod {
match self.id {
GameId::OpenMW => LoadOrderMethod::OpenMW,
GameId::Morrowind | GameId::Oblivion | GameId::Fallout3 | GameId::FalloutNV => {
LoadOrderMethod::Timestamp
}
GameId::Skyrim | GameId::OblivionRemastered => LoadOrderMethod::Textfile,
GameId::SkyrimSE
| GameId::SkyrimVR
| GameId::Fallout4
| GameId::Fallout4VR
| GameId::Starfield => LoadOrderMethod::Asterisk,
}
}
pub fn into_load_order(self) -> Box<dyn WritableLoadOrder + Send + Sync + 'static> {
match self.load_order_method() {
LoadOrderMethod::Asterisk => Box::new(AsteriskBasedLoadOrder::new(self)),
LoadOrderMethod::Textfile => Box::new(TextfileBasedLoadOrder::new(self)),
LoadOrderMethod::Timestamp => Box::new(TimestampBasedLoadOrder::new(self)),
LoadOrderMethod::OpenMW => Box::new(OpenMWLoadOrder::new(self)),
}
}
#[deprecated = "The master file is not necessarily of any significance: you should probably use early_loading_plugins() instead."]
pub fn master_file(&self) -> &'static str {
match self.id {
GameId::Morrowind | GameId::OpenMW => "Morrowind.esm",
GameId::Oblivion | GameId::OblivionRemastered => "Oblivion.esm",
GameId::Skyrim | GameId::SkyrimSE | GameId::SkyrimVR => "Skyrim.esm",
GameId::Fallout3 => "Fallout3.esm",
GameId::FalloutNV => "FalloutNV.esm",
GameId::Fallout4 | GameId::Fallout4VR => "Fallout4.esm",
GameId::Starfield => "Starfield.esm",
}
}
pub fn implicitly_active_plugins(&self) -> &[String] {
&self.implicitly_active_plugins
}
pub fn is_implicitly_active(&self, plugin: &str) -> bool {
use unicase::eq;
self.implicitly_active_plugins()
.iter()
.any(|p| eq(p.as_str(), plugin))
}
pub fn early_loading_plugins(&self) -> &[String] {
&self.early_loading_plugins
}
pub fn loads_early(&self, plugin: &str) -> bool {
use unicase::eq;
self.early_loading_plugins()
.iter()
.any(|p| eq(p.as_str(), plugin))
}
pub(crate) fn test_files(&self) -> &[String] {
&self.test_files
}
pub fn plugins_directory(&self) -> PathBuf {
self.plugins_directory.clone()
}
pub fn active_plugins_file(&self) -> &PathBuf {
&self.plugins_file_path
}
pub fn load_order_file(&self) -> Option<&PathBuf> {
self.load_order_path.as_ref()
}
pub fn additional_plugins_directories(&self) -> &[PathBuf] {
&self.additional_plugins_directories
}
pub fn set_additional_plugins_directories(&mut self, paths: Vec<PathBuf>) {
self.additional_plugins_directories = paths;
}
pub(crate) fn find_plugins(&self) -> Vec<PathBuf> {
let main_dir_iter = once(&self.plugins_directory);
let other_directories_iter = self.additional_plugins_directories.iter();
if self.id == GameId::OpenMW {
find_plugins_in_directories(main_dir_iter.chain(other_directories_iter), self.id)
} else {
find_plugins_in_directories(other_directories_iter.chain(main_dir_iter), self.id)
}
}
pub(crate) fn game_path(&self) -> &Path {
&self.game_path
}
pub fn plugin_path(&self, plugin_name: &str) -> PathBuf {
plugin_path(
self.id,
plugin_name,
&self.plugins_directory,
&self.additional_plugins_directories,
)
}
pub fn refresh_implicitly_active_plugins(&mut self) -> Result<(), Error> {
let ImplicitlyActivePlugins {
early_loading_plugins,
test_files,
all: implicitly_active_plugins,
} = GameSettings::load_implicitly_active_plugins(
self.id,
&self.game_path,
&self.my_games_path,
&self.plugins_directory,
&self.additional_plugins_directories,
)?;
self.early_loading_plugins = early_loading_plugins;
self.implicitly_active_plugins = implicitly_active_plugins;
self.test_files = test_files;
Ok(())
}
fn load_implicitly_active_plugins(
game_id: GameId,
game_path: &Path,
my_games_path: &Path,
plugins_directory: &Path,
additional_plugins_directories: &[PathBuf],
) -> Result<ImplicitlyActivePlugins, Error> {
let mut test_files = test_files(game_id, game_path, my_games_path)?;
if matches!(
game_id,
GameId::Fallout4 | GameId::Fallout4VR | GameId::Starfield
) {
test_files.retain(|f| {
let path = plugin_path(
game_id,
f,
plugins_directory,
additional_plugins_directories,
);
Plugin::with_path(&path, game_id, ActiveState::Inactive).is_ok()
});
}
let early_loading_plugins =
early_loading_plugins(game_id, game_path, my_games_path, !test_files.is_empty())?;
let implicitly_active_plugins =
implicitly_active_plugins(game_id, game_path, &early_loading_plugins, &test_files)?;
Ok(ImplicitlyActivePlugins {
early_loading_plugins,
test_files,
all: implicitly_active_plugins,
})
}
}
#[cfg(windows)]
fn local_path(game_id: GameId, game_path: &Path) -> Result<Option<PathBuf>, Error> {
if game_id == GameId::OpenMW {
return openmw_config::user_config_dir(game_path).map(Some);
}
let Some(local_app_data_path) = dirs::data_local_dir() else {
return Err(Error::NoLocalAppData);
};
match appdata_folder_name(game_id, game_path) {
Some(x) => Ok(Some(local_app_data_path.join(x))),
None => Ok(None),
}
}
#[cfg(not(windows))]
fn local_path(game_id: GameId, game_path: &Path) -> Result<Option<PathBuf>, Error> {
if game_id == GameId::OpenMW {
openmw_config::user_config_dir(game_path).map(Some)
} else if appdata_folder_name(game_id, game_path).is_none() {
Ok(None)
} else {
Err(Error::NoLocalAppData)
}
}
fn appdata_folder_name(game_id: GameId, game_path: &Path) -> Option<&'static str> {
match game_id {
GameId::Morrowind | GameId::OpenMW | GameId::OblivionRemastered => None,
GameId::Oblivion => Some("Oblivion"),
GameId::Skyrim => Some(skyrim_appdata_folder_name(game_path)),
GameId::SkyrimSE => Some(skyrim_se_appdata_folder_name(game_path)),
GameId::SkyrimVR => Some("Skyrim VR"),
GameId::Fallout3 => Some("Fallout3"),
GameId::FalloutNV => Some(falloutnv_appdata_folder_name(game_path)),
GameId::Fallout4 => Some(fallout4_appdata_folder_name(game_path)),
GameId::Fallout4VR => Some("Fallout4VR"),
GameId::Starfield => Some("Starfield"),
}
}
fn skyrim_appdata_folder_name(game_path: &Path) -> &'static str {
if is_enderal(game_path) {
"enderal"
} else {
"Skyrim"
}
}
fn skyrim_se_appdata_folder_name(game_path: &Path) -> &'static str {
let is_gog_install = game_path.join("Galaxy64.dll").exists();
if is_enderal(game_path) {
if is_gog_install {
"Enderal Special Edition GOG"
} else {
"Enderal Special Edition"
}
} else if is_gog_install {
"Skyrim Special Edition GOG"
} else if game_path.join("EOSSDK-Win64-Shipping.dll").exists() {
"Skyrim Special Edition EPIC"
} else if is_microsoft_store_install(GameId::SkyrimSE, game_path) {
"Skyrim Special Edition MS"
} else {
"Skyrim Special Edition"
}
}
fn falloutnv_appdata_folder_name(game_path: &Path) -> &'static str {
if game_path.join("EOSSDK-Win32-Shipping.dll").exists() {
"FalloutNV_Epic"
} else {
"FalloutNV"
}
}
fn fallout4_appdata_folder_name(game_path: &Path) -> &'static str {
if is_microsoft_store_install(GameId::Fallout4, game_path) {
"Fallout4 MS"
} else if game_path.join("EOSSDK-Win64-Shipping.dll").exists() {
"Fallout4 EPIC"
} else {
"Fallout4"
}
}
fn my_games_path(
game_id: GameId,
game_path: &Path,
local_path: &Path,
) -> Result<Option<PathBuf>, Error> {
if game_id == GameId::OpenMW {
return Ok(Some(local_path.to_path_buf()));
}
my_games_folder_name(game_id, game_path)
.map(|folder| {
documents_path(local_path)
.map(|d| d.join("My Games").join(folder))
.ok_or_else(|| Error::NoDocumentsPath)
})
.transpose()
}
fn my_games_folder_name(game_id: GameId, game_path: &Path) -> Option<&'static str> {
match game_id {
GameId::OpenMW => Some("OpenMW"),
GameId::OblivionRemastered => Some("Oblivion Remastered"),
GameId::Skyrim => Some(skyrim_my_games_folder_name(game_path)),
_ => appdata_folder_name(game_id, game_path),
}
}
fn skyrim_my_games_folder_name(game_path: &Path) -> &'static str {
if is_enderal(game_path) {
"Enderal"
} else {
"Skyrim"
}
}
fn is_microsoft_store_install(game_id: GameId, game_path: &Path) -> bool {
const APPX_MANIFEST: &str = "appxmanifest.xml";
match game_id {
GameId::Morrowind | GameId::Oblivion | GameId::Fallout3 | GameId::FalloutNV => game_path
.parent()
.is_some_and(|parent| parent.join(APPX_MANIFEST).exists()),
GameId::SkyrimSE | GameId::Fallout4 | GameId::Starfield | GameId::OblivionRemastered => {
game_path.join(APPX_MANIFEST).exists()
}
_ => false,
}
}
#[cfg(windows)]
fn documents_path(_local_path: &Path) -> Option<PathBuf> {
dirs::document_dir()
}
#[cfg(not(windows))]
fn documents_path(local_path: &Path) -> Option<PathBuf> {
local_path
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.map(|p| p.join("Documents"))
.or_else(|| {
Some(local_path.join("../../../Documents"))
})
}
fn plugins_directory(
game_id: GameId,
game_path: &Path,
local_path: &Path,
) -> Result<PathBuf, Error> {
match game_id {
GameId::OpenMW => openmw_config::resources_vfs_path(game_path, local_path),
GameId::Morrowind => Ok(game_path.join("Data Files")),
GameId::OblivionRemastered => Ok(game_path.join(OBLIVION_REMASTERED_RELATIVE_DATA_PATH)),
_ => Ok(game_path.join("Data")),
}
}
fn additional_plugins_directories(
game_id: GameId,
game_path: &Path,
my_games_path: &Path,
) -> Result<Vec<PathBuf>, Error> {
if game_id == GameId::Fallout4 && is_microsoft_store_install(game_id, game_path) {
Ok(vec![
game_path.join(MS_FO4_AUTOMATRON_PATH),
game_path.join(MS_FO4_NUKA_WORLD_PATH),
game_path.join(MS_FO4_WASTELAND_PATH),
game_path.join(MS_FO4_TEXTURE_PACK_PATH),
game_path.join(MS_FO4_VAULT_TEC_PATH),
game_path.join(MS_FO4_FAR_HARBOR_PATH),
game_path.join(MS_FO4_CONTRAPTIONS_PATH),
])
} else if game_id == GameId::Starfield {
if is_microsoft_store_install(game_id, game_path) {
Ok(vec![
my_games_path.join("Data"),
game_path.join("../../Old Mars/Content/Data"),
game_path.join("../../Shattered Space/Content/Data"),
])
} else {
Ok(vec![my_games_path.join("Data")])
}
} else if game_id == GameId::OpenMW {
openmw_config::additional_data_paths(game_path, my_games_path)
} else {
Ok(Vec::new())
}
}
fn load_order_path(
game_id: GameId,
local_path: &Path,
plugins_file_path: &Path,
) -> Option<PathBuf> {
const LOADORDER_TXT: &str = "loadorder.txt";
match game_id {
GameId::Skyrim => Some(local_path.join(LOADORDER_TXT)),
GameId::OblivionRemastered => plugins_file_path.parent().map(|p| p.join(LOADORDER_TXT)),
_ => None,
}
}
fn plugins_file_path(
game_id: GameId,
game_path: &Path,
local_path: &Path,
) -> Result<PathBuf, Error> {
match game_id {
GameId::OpenMW => Ok(local_path.join("openmw.cfg")),
GameId::Morrowind => Ok(game_path.join("Morrowind.ini")),
GameId::Oblivion => oblivion_plugins_file_path(game_path, local_path),
GameId::OblivionRemastered => Ok(game_path
.join(OBLIVION_REMASTERED_RELATIVE_DATA_PATH)
.join(PLUGINS_TXT)),
_ => Ok(local_path.join(PLUGINS_TXT)),
}
}
fn oblivion_plugins_file_path(game_path: &Path, local_path: &Path) -> Result<PathBuf, Error> {
let ini_path = game_path.join("Oblivion.ini");
let parent_path = if use_my_games_directory(&ini_path)? {
local_path
} else {
game_path
};
Ok(parent_path.join(PLUGINS_TXT))
}
fn ccc_file_paths(game_id: GameId, game_path: &Path, my_games_path: &Path) -> Vec<PathBuf> {
match game_id {
GameId::Fallout4 => vec![game_path.join("Fallout4.ccc")],
GameId::SkyrimSE => vec![game_path.join("Skyrim.ccc")],
GameId::Starfield => vec![
my_games_path.join("Starfield.ccc"),
game_path.join("Starfield.ccc"),
],
_ => vec![],
}
}
fn hardcoded_plugins(game_id: GameId) -> &'static [&'static str] {
match game_id {
GameId::Skyrim => SKYRIM_HARDCODED_PLUGINS,
GameId::SkyrimSE => SKYRIM_SE_HARDCODED_PLUGINS,
GameId::SkyrimVR => SKYRIM_VR_HARDCODED_PLUGINS,
GameId::Fallout4 => FALLOUT4_HARDCODED_PLUGINS,
GameId::Fallout4VR => FALLOUT4VR_HARDCODED_PLUGINS,
GameId::Starfield => STARFIELD_HARDCODED_PLUGINS,
GameId::OpenMW => OPENMW_HARDCODED_PLUGINS,
_ => &[],
}
}
fn find_nam_plugins(plugins_path: &Path) -> Result<Vec<String>, Error> {
let mut plugin_names = Vec::new();
if !plugins_path.exists() {
return Ok(plugin_names);
}
let dir_iter = plugins_path
.read_dir()
.map_err(|e| Error::IoError(plugins_path.to_path_buf(), e))?
.filter_map(Result::ok)
.filter(|e| is_file_type_supported(e, GameId::FalloutNV))
.filter(|e| {
e.path()
.extension()
.unwrap_or_default()
.eq_ignore_ascii_case("nam")
});
for entry in dir_iter {
let file_name = entry.file_name();
let plugin = Path::new(&file_name).with_extension("esp");
if let Some(esp) = plugin.to_str() {
plugin_names.push(esp.to_owned());
}
let master = Path::new(&file_name).with_extension("esm");
if let Some(esm) = master.to_str() {
plugin_names.push(esm.to_owned());
}
}
Ok(plugin_names)
}
fn is_file_type_supported(dir_entry: &DirEntry, game_id: GameId) -> bool {
dir_entry
.file_type()
.map(|f| keep_file_type(f, game_id))
.unwrap_or(false)
}
#[cfg(unix)]
#[expect(
clippy::filetype_is_file,
reason = "Only files and symlinks are supported"
)]
fn keep_file_type(f: FileType, _game_id: GameId) -> bool {
f.is_file() || f.is_symlink()
}
#[cfg(windows)]
#[expect(
clippy::filetype_is_file,
reason = "Only files and sometimes symlinks are supported"
)]
fn keep_file_type(f: FileType, game_id: GameId) -> bool {
if matches!(game_id, GameId::OblivionRemastered | GameId::OpenMW) {
f.is_file() || f.is_symlink()
} else {
f.is_file()
}
}
fn early_loading_plugins(
game_id: GameId,
game_path: &Path,
my_games_path: &Path,
has_test_files: bool,
) -> Result<Vec<String>, Error> {
let mut plugin_names: Vec<String> = hardcoded_plugins(game_id)
.iter()
.map(|s| (*s).to_owned())
.collect();
if matches!(game_id, GameId::Fallout4 | GameId::Starfield) && has_test_files {
return Ok(plugin_names);
}
for file_path in ccc_file_paths(game_id, game_path, my_games_path) {
if file_path.exists() {
let reader =
BufReader::new(File::open(&file_path).map_err(|e| Error::IoError(file_path, e))?);
let lines = reader
.lines()
.filter_map(|line| line.ok().filter(|l| !l.is_empty()));
plugin_names.extend(lines);
break;
}
}
if game_id == GameId::OpenMW {
plugin_names.extend(openmw_config::non_user_active_plugin_names(game_path)?);
}
deduplicate(&mut plugin_names);
Ok(plugin_names)
}
fn implicitly_active_plugins(
game_id: GameId,
game_path: &Path,
early_loading_plugins: &[String],
test_files: &[String],
) -> Result<Vec<String>, Error> {
let mut plugin_names = Vec::new();
plugin_names.extend_from_slice(early_loading_plugins);
plugin_names.extend_from_slice(test_files);
if game_id == GameId::FalloutNV {
let nam_plugins = find_nam_plugins(&game_path.join("Data"))?;
plugin_names.extend(nam_plugins);
} else if game_id == GameId::Skyrim {
plugin_names.push("Update.esm".to_owned());
}
deduplicate(&mut plugin_names);
Ok(plugin_names)
}
fn deduplicate(plugin_names: &mut Vec<String>) {
let mut set = std::collections::HashSet::new();
plugin_names.retain(|e| set.insert(unicase::UniCase::new(e.clone())));
}
fn find_map_path(directory: &Path, plugin_name: &str, game_id: GameId) -> Option<PathBuf> {
if game_id.allow_plugin_ghosting() {
use crate::ghostable_path::GhostablePath;
directory.join(plugin_name).resolve_path().ok()
} else {
let path = directory.join(plugin_name);
path.exists().then_some(path)
}
}
fn pick_plugin_path<'a>(
game_id: GameId,
plugin_name: &str,
plugins_directory: &Path,
mut dir_iter: impl Iterator<Item = &'a PathBuf>,
) -> PathBuf {
dir_iter
.find_map(|d| find_map_path(d, plugin_name, game_id))
.unwrap_or_else(|| plugins_directory.join(plugin_name))
}
fn plugin_path(
game_id: GameId,
plugin_name: &str,
plugins_directory: &Path,
additional_plugins_directories: &[PathBuf],
) -> PathBuf {
if game_id == GameId::Starfield {
use crate::ghostable_path::GhostablePath;
let path = plugins_directory.join(plugin_name);
if path.resolve_path().is_err() {
return path;
}
}
match game_id {
GameId::OpenMW => pick_plugin_path(
game_id,
plugin_name,
plugins_directory,
additional_plugins_directories.iter().rev(),
),
_ => pick_plugin_path(
game_id,
plugin_name,
plugins_directory,
additional_plugins_directories.iter(),
),
}
}
fn sort_plugins_dir_entries(a: &DirEntry, b: &DirEntry) -> Ordering {
let m_a = get_target_modified_timestamp(a);
let m_b = get_target_modified_timestamp(b);
match m_a.cmp(&m_b) {
Ordering::Equal => compare_uppercased_filenames(&a.file_name(), &b.file_name()).reverse(),
x => x,
}
}
fn compare_uppercased_filenames(a: &OsStr, b: &OsStr) -> Ordering {
match (a.to_str(), b.to_str()) {
(Some(a), Some(b)) => a.to_uppercase().cmp(&b.to_uppercase()),
_ => a.cmp(b),
}
}
fn get_target_modified_timestamp(entry: &DirEntry) -> Option<SystemTime> {
let metadata = if entry.file_type().is_ok_and(|f| f.is_symlink()) {
entry.path().metadata()
} else {
entry.metadata()
};
metadata.and_then(|m| m.modified()).ok()
}
fn sort_plugins_dir_entries_openmw(a: &DirEntry, b: &DirEntry) -> Ordering {
if a.path().parent() == b.path().parent() {
a.file_name().cmp(&b.file_name())
} else {
Ordering::Equal
}
}
fn find_plugins_in_directories<'a>(
directories_iter: impl Iterator<Item = &'a PathBuf>,
game_id: GameId,
) -> Vec<PathBuf> {
let mut dir_entries: Vec<_> = directories_iter
.flat_map(read_dir)
.flatten()
.filter_map(Result::ok)
.filter(|e| is_file_type_supported(e, game_id))
.filter(|e| {
e.file_name()
.to_str()
.is_some_and(|f| has_plugin_extension(f, game_id))
})
.collect();
let compare = match game_id {
GameId::OpenMW => sort_plugins_dir_entries_openmw,
_ => sort_plugins_dir_entries,
};
dir_entries.sort_by(compare);
dir_entries.into_iter().map(|e| e.path()).collect()
}
#[cfg(test)]
mod tests {
#[cfg(windows)]
use std::env;
use std::{fs::create_dir_all, io::Write};
use tempfile::tempdir;
use crate::tests::{copy_to_dir, set_file_timestamps, symlink_file, NON_ASCII};
use super::*;
fn game_with_generic_paths(game_id: GameId) -> GameSettings {
GameSettings::with_local_and_my_games_paths(
game_id,
&PathBuf::from("game"),
&PathBuf::from("local"),
PathBuf::from("my games"),
)
.unwrap()
}
fn game_with_game_path(game_id: GameId, game_path: &Path) -> GameSettings {
GameSettings::with_local_and_my_games_paths(
game_id,
game_path,
&PathBuf::default(),
PathBuf::default(),
)
.unwrap()
}
fn game_with_ccc_plugins(
game_id: GameId,
game_path: &Path,
plugin_names: &[&str],
) -> GameSettings {
let ccc_path = &ccc_file_paths(game_id, game_path, &PathBuf::new())[0];
create_ccc_file(ccc_path, plugin_names);
game_with_game_path(game_id, game_path)
}
fn create_ccc_file(path: &Path, plugin_names: &[&str]) {
create_dir_all(path.parent().unwrap()).unwrap();
let mut file = File::create(path).unwrap();
for plugin_name in plugin_names {
writeln!(file, "{plugin_name}").unwrap();
}
}
fn generate_file_file_type() -> FileType {
let tmp_dir = tempdir().unwrap();
let file_path = tmp_dir.path().join("file");
File::create(&file_path).unwrap();
let file_file_type = file_path.metadata().unwrap().file_type();
assert!(file_file_type.is_file());
file_file_type
}
fn generate_symlink_file_type() -> FileType {
let tmp_dir = tempdir().unwrap();
let file_path = tmp_dir.path().join("file");
let symlink_path = tmp_dir.path().join("symlink");
File::create(&file_path).unwrap();
symlink_file(&file_path, &symlink_path);
let symlink_file_type = symlink_path.symlink_metadata().unwrap().file_type();
assert!(symlink_file_type.is_symlink());
symlink_file_type
}
#[test]
#[cfg(windows)]
fn new_should_determine_correct_local_path_on_windows() {
let settings = GameSettings::new(GameId::Skyrim, Path::new("game")).unwrap();
let local_app_data = env::var("LOCALAPPDATA").unwrap();
let local_app_data_path = Path::new(&local_app_data);
assert_eq!(
local_app_data_path.join("Skyrim").join("Plugins.txt"),
*settings.active_plugins_file()
);
assert_eq!(
&local_app_data_path.join("Skyrim").join("loadorder.txt"),
*settings.load_order_file().as_ref().unwrap()
);
}
#[test]
#[cfg(windows)]
fn new_should_determine_correct_local_path_for_openmw() {
let tmp_dir = tempdir().unwrap();
let global_cfg_path = tmp_dir.path().join("openmw.cfg");
std::fs::write(&global_cfg_path, "config=local").unwrap();
let settings = GameSettings::new(GameId::OpenMW, tmp_dir.path()).unwrap();
assert_eq!(
&tmp_dir.path().join("local/openmw.cfg"),
settings.active_plugins_file()
);
assert_eq!(tmp_dir.path().join("local"), settings.my_games_path);
}
#[test]
fn new_should_use_an_empty_local_path_for_morrowind() {
let settings = GameSettings::new(GameId::Morrowind, Path::new("game")).unwrap();
assert_eq!(PathBuf::new(), settings.my_games_path);
}
#[test]
#[cfg(not(windows))]
fn new_should_determine_correct_local_path_for_openmw_on_linux() {
let config_path = Path::new("/etc/openmw");
let settings = GameSettings::new(GameId::OpenMW, Path::new("game")).unwrap();
assert_eq!(
&config_path.join("openmw.cfg"),
settings.active_plugins_file()
);
assert_eq!(config_path, settings.my_games_path);
}
#[test]
fn id_should_be_the_id_the_struct_was_created_with() {
let settings = game_with_generic_paths(GameId::Morrowind);
assert_eq!(GameId::Morrowind, settings.id());
}
#[test]
fn supports_blueprint_ships_plugins_should_be_true_for_starfield_only() {
let mut settings = game_with_generic_paths(GameId::Morrowind);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::OpenMW);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::Oblivion);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::OblivionRemastered);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::Skyrim);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::SkyrimSE);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::SkyrimVR);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::Fallout3);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::FalloutNV);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::Fallout4);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::Fallout4VR);
assert!(!settings.supports_blueprint_ships_plugins());
settings = game_with_generic_paths(GameId::Starfield);
assert!(settings.supports_blueprint_ships_plugins());
}
#[test]
fn load_order_method_should_be_timestamp_for_tes3_tes4_fo3_and_fonv() {
let mut settings = game_with_generic_paths(GameId::Morrowind);
assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
settings = game_with_generic_paths(GameId::Oblivion);
assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
settings = game_with_generic_paths(GameId::Fallout3);
assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
settings = game_with_generic_paths(GameId::FalloutNV);
assert_eq!(LoadOrderMethod::Timestamp, settings.load_order_method());
}
#[test]
fn load_order_method_should_be_textfile_for_tes5() {
let settings = game_with_generic_paths(GameId::Skyrim);
assert_eq!(LoadOrderMethod::Textfile, settings.load_order_method());
}
#[test]
fn load_order_method_should_be_asterisk_for_tes5se_tes5vr_fo4_fo4vr_and_starfield() {
let mut settings = game_with_generic_paths(GameId::SkyrimSE);
assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
settings = game_with_generic_paths(GameId::SkyrimVR);
assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
settings = game_with_generic_paths(GameId::Fallout4);
assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
settings = game_with_generic_paths(GameId::Fallout4VR);
assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
settings = game_with_generic_paths(GameId::Starfield);
assert_eq!(LoadOrderMethod::Asterisk, settings.load_order_method());
}
#[test]
fn load_order_method_should_be_openmw_for_openmw() {
let settings = game_with_generic_paths(GameId::OpenMW);
assert_eq!(LoadOrderMethod::OpenMW, settings.load_order_method());
}
#[test]
#[expect(deprecated)]
fn master_file_should_be_mapped_from_game_id() {
let mut settings = game_with_generic_paths(GameId::OpenMW);
assert_eq!("Morrowind.esm", settings.master_file());
settings = game_with_generic_paths(GameId::Morrowind);
assert_eq!("Morrowind.esm", settings.master_file());
settings = game_with_generic_paths(GameId::Oblivion);
assert_eq!("Oblivion.esm", settings.master_file());
settings = game_with_generic_paths(GameId::Skyrim);
assert_eq!("Skyrim.esm", settings.master_file());
settings = game_with_generic_paths(GameId::SkyrimSE);
assert_eq!("Skyrim.esm", settings.master_file());
settings = game_with_generic_paths(GameId::SkyrimVR);
assert_eq!("Skyrim.esm", settings.master_file());
settings = game_with_generic_paths(GameId::Fallout3);
assert_eq!("Fallout3.esm", settings.master_file());
settings = game_with_generic_paths(GameId::FalloutNV);
assert_eq!("FalloutNV.esm", settings.master_file());
settings = game_with_generic_paths(GameId::Fallout4);
assert_eq!("Fallout4.esm", settings.master_file());
settings = game_with_generic_paths(GameId::Fallout4VR);
assert_eq!("Fallout4.esm", settings.master_file());
settings = game_with_generic_paths(GameId::Starfield);
assert_eq!("Starfield.esm", settings.master_file());
}
#[test]
fn appdata_folder_name_should_be_mapped_from_game_id() {
let game_path = Path::new("");
assert!(appdata_folder_name(GameId::OpenMW, game_path).is_none());
assert!(appdata_folder_name(GameId::Morrowind, game_path).is_none());
let mut folder = appdata_folder_name(GameId::Oblivion, game_path).unwrap();
assert_eq!("Oblivion", folder);
folder = appdata_folder_name(GameId::Skyrim, game_path).unwrap();
assert_eq!("Skyrim", folder);
folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
assert_eq!("Skyrim Special Edition", folder);
folder = appdata_folder_name(GameId::SkyrimVR, game_path).unwrap();
assert_eq!("Skyrim VR", folder);
folder = appdata_folder_name(GameId::Fallout3, game_path).unwrap();
assert_eq!("Fallout3", folder);
folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
assert_eq!("FalloutNV", folder);
folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
assert_eq!("Fallout4", folder);
folder = appdata_folder_name(GameId::Fallout4VR, game_path).unwrap();
assert_eq!("Fallout4VR", folder);
folder = appdata_folder_name(GameId::Starfield, game_path).unwrap();
assert_eq!("Starfield", folder);
}
#[test]
fn appdata_folder_name_for_skyrim_se_should_have_gog_suffix_if_galaxy_dll_is_in_game_path() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
assert_eq!("Skyrim Special Edition", folder);
let dll_path = game_path.join("Galaxy64.dll");
File::create(&dll_path).unwrap();
folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
assert_eq!("Skyrim Special Edition GOG", folder);
}
#[test]
fn appdata_folder_name_for_skyrim_se_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
assert_eq!("Skyrim Special Edition", folder);
let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
File::create(&dll_path).unwrap();
folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
assert_eq!("Skyrim Special Edition EPIC", folder);
}
#[test]
fn appdata_folder_name_for_skyrim_se_should_have_ms_suffix_if_appxmanifest_xml_is_in_game_path()
{
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let mut folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
assert_eq!("Skyrim Special Edition", folder);
let dll_path = game_path.join("appxmanifest.xml");
File::create(&dll_path).unwrap();
folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
assert_eq!("Skyrim Special Edition MS", folder);
}
#[test]
fn appdata_folder_name_for_skyrim_se_prefers_gog_suffix_over_epic_suffix() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let dll_path = game_path.join("Galaxy64.dll");
File::create(&dll_path).unwrap();
let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
File::create(&dll_path).unwrap();
let folder = appdata_folder_name(GameId::SkyrimSE, game_path).unwrap();
assert_eq!("Skyrim Special Edition GOG", folder);
}
#[test]
fn appdata_folder_name_for_fallout_nv_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let mut folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
assert_eq!("FalloutNV", folder);
let dll_path = game_path.join("EOSSDK-Win32-Shipping.dll");
File::create(&dll_path).unwrap();
folder = appdata_folder_name(GameId::FalloutNV, game_path).unwrap();
assert_eq!("FalloutNV_Epic", folder);
}
#[test]
fn appdata_folder_name_for_fallout4_should_have_ms_suffix_if_appxmanifest_xml_is_in_game_path()
{
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let mut folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
assert_eq!("Fallout4", folder);
let dll_path = game_path.join("appxmanifest.xml");
File::create(&dll_path).unwrap();
folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
assert_eq!("Fallout4 MS", folder);
}
#[test]
fn appdata_folder_name_for_fallout4_should_have_epic_suffix_if_eossdk_dll_is_in_game_path() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let mut folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
assert_eq!("Fallout4", folder);
let dll_path = game_path.join("EOSSDK-Win64-Shipping.dll");
File::create(&dll_path).unwrap();
folder = appdata_folder_name(GameId::Fallout4, game_path).unwrap();
assert_eq!("Fallout4 EPIC", folder);
}
#[test]
#[cfg(windows)]
fn my_games_path_should_be_in_documents_path_on_windows() {
let empty_path = Path::new("");
let parent_path = dirs::document_dir().unwrap().join("My Games");
let path = my_games_path(GameId::Morrowind, empty_path, empty_path).unwrap();
assert!(path.is_none());
let path = my_games_path(GameId::Oblivion, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Oblivion"), path);
let path = my_games_path(GameId::Skyrim, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Skyrim"), path);
let path = my_games_path(GameId::SkyrimSE, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Skyrim Special Edition"), path);
let path = my_games_path(GameId::SkyrimVR, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Skyrim VR"), path);
let path = my_games_path(GameId::Fallout3, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Fallout3"), path);
let path = my_games_path(GameId::FalloutNV, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("FalloutNV"), path);
let path = my_games_path(GameId::Fallout4, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Fallout4"), path);
let path = my_games_path(GameId::Fallout4VR, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Fallout4VR"), path);
let path = my_games_path(GameId::Starfield, empty_path, empty_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Starfield"), path);
}
#[test]
#[cfg(not(windows))]
fn my_games_path_should_be_relative_to_local_path_on_linux() {
let empty_path = Path::new("");
let local_path = Path::new("wineprefix/drive_c/Users/user/AppData/Local/Game");
let parent_path = Path::new("wineprefix/drive_c/Users/user/Documents/My Games");
let path = my_games_path(GameId::Morrowind, empty_path, local_path).unwrap();
assert!(path.is_none());
let path = my_games_path(GameId::Oblivion, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Oblivion"), path);
let path = my_games_path(GameId::Skyrim, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Skyrim"), path);
let path = my_games_path(GameId::SkyrimSE, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Skyrim Special Edition"), path);
let path = my_games_path(GameId::SkyrimVR, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Skyrim VR"), path);
let path = my_games_path(GameId::Fallout3, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Fallout3"), path);
let path = my_games_path(GameId::FalloutNV, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("FalloutNV"), path);
let path = my_games_path(GameId::Fallout4, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Fallout4"), path);
let path = my_games_path(GameId::Fallout4VR, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Fallout4VR"), path);
let path = my_games_path(GameId::Starfield, empty_path, local_path)
.unwrap()
.unwrap();
assert_eq!(parent_path.join("Starfield"), path);
}
#[test]
#[cfg(windows)]
fn my_games_path_should_be_local_path_for_openmw() {
let local_path = Path::new("path/to/local");
let path = my_games_path(GameId::OpenMW, Path::new(""), local_path)
.unwrap()
.unwrap();
assert_eq!(local_path, path);
}
#[test]
fn plugins_directory_should_be_mapped_from_game_id() {
let data_path = Path::new("Data");
let empty_path = Path::new("");
let closure = |game_id| plugins_directory(game_id, empty_path, empty_path).unwrap();
assert_eq!(Path::new("resources/vfs"), closure(GameId::OpenMW));
assert_eq!(Path::new("Data Files"), closure(GameId::Morrowind));
assert_eq!(data_path, closure(GameId::Oblivion));
assert_eq!(data_path, closure(GameId::Skyrim));
assert_eq!(data_path, closure(GameId::SkyrimSE));
assert_eq!(data_path, closure(GameId::SkyrimVR));
assert_eq!(data_path, closure(GameId::Fallout3));
assert_eq!(data_path, closure(GameId::FalloutNV));
assert_eq!(data_path, closure(GameId::Fallout4));
assert_eq!(data_path, closure(GameId::Fallout4VR));
assert_eq!(data_path, closure(GameId::Starfield));
}
#[test]
fn active_plugins_file_should_be_mapped_from_game_id() {
let mut settings = game_with_generic_paths(GameId::OpenMW);
assert_eq!(
Path::new("local/openmw.cfg"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::Morrowind);
assert_eq!(
Path::new("game/Morrowind.ini"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::Oblivion);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::Skyrim);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::SkyrimSE);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::SkyrimVR);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::Fallout3);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::FalloutNV);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::Fallout4);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::Fallout4VR);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
settings = game_with_generic_paths(GameId::Starfield);
assert_eq!(
Path::new("local/Plugins.txt"),
settings.active_plugins_file()
);
}
#[test]
fn active_plugins_file_should_be_in_game_path_for_oblivion_if_ini_setting_is_not_1() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let ini_path = game_path.join("Oblivion.ini");
std::fs::write(ini_path, "[General]\nbUseMyGamesDirectory=0\n").unwrap();
let settings = game_with_game_path(GameId::Oblivion, game_path);
assert_eq!(
game_path.join("Plugins.txt"),
*settings.active_plugins_file()
);
}
#[test]
#[cfg(not(windows))]
fn find_nam_plugins_should_also_find_symlinks_to_nam_plugins() {
let tmp_dir = tempdir().unwrap();
let data_path = tmp_dir.path().join("Data");
let other_path = tmp_dir.path().join("other");
create_dir_all(&data_path).unwrap();
create_dir_all(&other_path).unwrap();
File::create(data_path.join("plugin1.nam")).unwrap();
let original = other_path.join("plugin2.NAM");
File::create(&original).unwrap();
symlink_file(&original, &data_path.join("plugin2.NAM"));
let mut plugins = find_nam_plugins(&data_path).unwrap();
plugins.sort();
let expected_plugins = vec!["plugin1.esm", "plugin1.esp", "plugin2.esm", "plugin2.esp"];
assert_eq!(expected_plugins, plugins);
}
#[test]
fn keep_file_type_should_return_true_for_files_for_all_games() {
let file = generate_file_file_type();
assert!(keep_file_type(file, GameId::Morrowind));
assert!(keep_file_type(file, GameId::OpenMW));
assert!(keep_file_type(file, GameId::Oblivion));
assert!(keep_file_type(file, GameId::OblivionRemastered));
assert!(keep_file_type(file, GameId::Skyrim));
assert!(keep_file_type(file, GameId::SkyrimSE));
assert!(keep_file_type(file, GameId::SkyrimVR));
assert!(keep_file_type(file, GameId::Fallout3));
assert!(keep_file_type(file, GameId::FalloutNV));
assert!(keep_file_type(file, GameId::Fallout4));
assert!(keep_file_type(file, GameId::Fallout4VR));
assert!(keep_file_type(file, GameId::Starfield));
}
#[test]
#[cfg(not(windows))]
fn keep_file_type_should_return_true_for_symlinks_for_all_games_on_linux() {
let symlink = generate_symlink_file_type();
assert!(keep_file_type(symlink, GameId::Morrowind));
assert!(keep_file_type(symlink, GameId::OpenMW));
assert!(keep_file_type(symlink, GameId::Oblivion));
assert!(keep_file_type(symlink, GameId::OblivionRemastered));
assert!(keep_file_type(symlink, GameId::Skyrim));
assert!(keep_file_type(symlink, GameId::SkyrimSE));
assert!(keep_file_type(symlink, GameId::SkyrimVR));
assert!(keep_file_type(symlink, GameId::Fallout3));
assert!(keep_file_type(symlink, GameId::FalloutNV));
assert!(keep_file_type(symlink, GameId::Fallout4));
assert!(keep_file_type(symlink, GameId::Fallout4VR));
assert!(keep_file_type(symlink, GameId::Starfield));
}
#[test]
#[cfg(windows)]
fn keep_file_type_should_return_true_for_symlinks_for_openmw_and_oblivion_remastered_on_windows(
) {
let symlink = generate_symlink_file_type();
assert!(!keep_file_type(symlink, GameId::Morrowind));
assert!(keep_file_type(symlink, GameId::OpenMW));
assert!(!keep_file_type(symlink, GameId::Oblivion));
assert!(keep_file_type(symlink, GameId::OblivionRemastered));
assert!(!keep_file_type(symlink, GameId::Skyrim));
assert!(!keep_file_type(symlink, GameId::SkyrimSE));
assert!(!keep_file_type(symlink, GameId::SkyrimVR));
assert!(!keep_file_type(symlink, GameId::Fallout3));
assert!(!keep_file_type(symlink, GameId::FalloutNV));
assert!(!keep_file_type(symlink, GameId::Fallout4));
assert!(!keep_file_type(symlink, GameId::Fallout4VR));
assert!(!keep_file_type(symlink, GameId::Starfield));
}
#[test]
fn early_loading_plugins_should_be_mapped_from_game_id() {
let mut settings = game_with_generic_paths(GameId::Skyrim);
let mut plugins = vec!["Skyrim.esm"];
assert_eq!(plugins, settings.early_loading_plugins());
settings = game_with_generic_paths(GameId::SkyrimSE);
plugins = vec![
"Skyrim.esm",
"Update.esm",
"Dawnguard.esm",
"HearthFires.esm",
"Dragonborn.esm",
];
assert_eq!(plugins, settings.early_loading_plugins());
settings = game_with_generic_paths(GameId::SkyrimVR);
plugins = vec![
"Skyrim.esm",
"Update.esm",
"Dawnguard.esm",
"HearthFires.esm",
"Dragonborn.esm",
"SkyrimVR.esm",
];
assert_eq!(plugins, settings.early_loading_plugins());
settings = game_with_generic_paths(GameId::Fallout4);
plugins = vec![
"Fallout4.esm",
"DLCRobot.esm",
"DLCworkshop01.esm",
"DLCCoast.esm",
"DLCworkshop02.esm",
"DLCworkshop03.esm",
"DLCNukaWorld.esm",
"DLCUltraHighResolution.esm",
];
assert_eq!(plugins, settings.early_loading_plugins());
settings = game_with_generic_paths(GameId::OpenMW);
plugins = vec!["builtin.omwscripts"];
assert_eq!(plugins, settings.early_loading_plugins());
settings = game_with_generic_paths(GameId::Morrowind);
assert!(settings.early_loading_plugins().is_empty());
settings = game_with_generic_paths(GameId::Oblivion);
assert!(settings.early_loading_plugins().is_empty());
settings = game_with_generic_paths(GameId::Fallout3);
assert!(settings.early_loading_plugins().is_empty());
settings = game_with_generic_paths(GameId::FalloutNV);
assert!(settings.early_loading_plugins().is_empty());
settings = game_with_generic_paths(GameId::Fallout4VR);
plugins = vec!["Fallout4.esm", "Fallout4_VR.esm"];
assert_eq!(plugins, settings.early_loading_plugins());
settings = game_with_generic_paths(GameId::Starfield);
plugins = vec![
"Starfield.esm",
"Constellation.esm",
"OldMars.esm",
"ShatteredSpace.esm",
"SFBGS003.esm",
"SFBGS004.esm",
"SFBGS006.esm",
"SFBGS007.esm",
"SFBGS008.esm",
"SFBGS00D.esm",
"SFBGS047.esm",
"SFBGS050.esm",
];
assert_eq!(plugins, settings.early_loading_plugins());
}
#[test]
fn early_loading_plugins_should_include_plugins_loaded_from_ccc_file() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let mut plugins = vec![
"Skyrim.esm",
"Update.esm",
"Dawnguard.esm",
"HearthFires.esm",
"Dragonborn.esm",
"ccBGSSSE002-ExoticArrows.esl",
"ccBGSSSE003-Zombies.esl",
"ccBGSSSE004-RuinsEdge.esl",
"ccBGSSSE006-StendarsHammer.esl",
"ccBGSSSE007-Chrysamere.esl",
"ccBGSSSE010-PetDwarvenArmoredMudcrab.esl",
"ccBGSSSE014-SpellPack01.esl",
"ccBGSSSE019-StaffofSheogorath.esl",
"ccMTYSSE001-KnightsoftheNine.esl",
"ccQDRSSE001-SurvivalMode.esl",
];
let mut settings = game_with_ccc_plugins(GameId::SkyrimSE, game_path, &plugins[5..]);
assert_eq!(plugins, settings.early_loading_plugins());
plugins = vec![
"Fallout4.esm",
"DLCRobot.esm",
"DLCworkshop01.esm",
"DLCCoast.esm",
"DLCworkshop02.esm",
"DLCworkshop03.esm",
"DLCNukaWorld.esm",
"DLCUltraHighResolution.esm",
"ccBGSFO4001-PipBoy(Black).esl",
"ccBGSFO4002-PipBoy(Blue).esl",
"ccBGSFO4003-PipBoy(Camo01).esl",
"ccBGSFO4004-PipBoy(Camo02).esl",
"ccBGSFO4006-PipBoy(Chrome).esl",
"ccBGSFO4012-PipBoy(Red).esl",
"ccBGSFO4014-PipBoy(White).esl",
"ccBGSFO4016-Prey.esl",
"ccBGSFO4017-Mauler.esl",
"ccBGSFO4018-GaussRiflePrototype.esl",
"ccBGSFO4019-ChineseStealthArmor.esl",
"ccBGSFO4020-PowerArmorSkin(Black).esl",
"ccBGSFO4022-PowerArmorSkin(Camo01).esl",
"ccBGSFO4023-PowerArmorSkin(Camo02).esl",
"ccBGSFO4025-PowerArmorSkin(Chrome).esl",
"ccBGSFO4038-HorseArmor.esl",
"ccBGSFO4039-TunnelSnakes.esl",
"ccBGSFO4041-DoomMarineArmor.esl",
"ccBGSFO4042-BFG.esl",
"ccBGSFO4043-DoomChainsaw.esl",
"ccBGSFO4044-HellfirePowerArmor.esl",
"ccFSVFO4001-ModularMilitaryBackpack.esl",
"ccFSVFO4002-MidCenturyModern.esl",
"ccFRSFO4001-HandmadeShotgun.esl",
"ccEEJFO4001-DecorationPack.esl",
];
settings = game_with_ccc_plugins(GameId::Fallout4, game_path, &plugins[8..]);
assert_eq!(plugins, settings.early_loading_plugins());
}
#[test]
fn early_loading_plugins_should_use_the_starfield_ccc_file_in_game_path() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path().join("game");
let my_games_path = tmp_dir.path().join("my games");
create_ccc_file(&game_path.join("Starfield.ccc"), &["test.esm"]);
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Starfield,
&game_path,
&PathBuf::default(),
my_games_path,
)
.unwrap();
let expected = &[
"Starfield.esm",
"Constellation.esm",
"OldMars.esm",
"ShatteredSpace.esm",
"SFBGS003.esm",
"SFBGS004.esm",
"SFBGS006.esm",
"SFBGS007.esm",
"SFBGS008.esm",
"SFBGS00D.esm",
"SFBGS047.esm",
"SFBGS050.esm",
"test.esm",
];
assert_eq!(expected, settings.early_loading_plugins());
}
#[test]
fn early_loading_plugins_should_use_the_starfield_ccc_file_in_my_games_path() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path().join("game");
let my_games_path = tmp_dir.path().join("my games");
create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test.esm"]);
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Starfield,
&game_path,
&PathBuf::default(),
my_games_path,
)
.unwrap();
let expected = &[
"Starfield.esm",
"Constellation.esm",
"OldMars.esm",
"ShatteredSpace.esm",
"SFBGS003.esm",
"SFBGS004.esm",
"SFBGS006.esm",
"SFBGS007.esm",
"SFBGS008.esm",
"SFBGS00D.esm",
"SFBGS047.esm",
"SFBGS050.esm",
"test.esm",
];
assert_eq!(expected, settings.early_loading_plugins());
}
#[test]
fn early_loading_plugins_should_use_the_first_ccc_file_that_exists() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path().join("game");
let my_games_path = tmp_dir.path().join("my games");
create_ccc_file(&game_path.join("Starfield.ccc"), &["test1.esm"]);
create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test2.esm"]);
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Starfield,
&game_path,
&PathBuf::default(),
my_games_path,
)
.unwrap();
let expected = &[
"Starfield.esm",
"Constellation.esm",
"OldMars.esm",
"ShatteredSpace.esm",
"SFBGS003.esm",
"SFBGS004.esm",
"SFBGS006.esm",
"SFBGS007.esm",
"SFBGS008.esm",
"SFBGS00D.esm",
"SFBGS047.esm",
"SFBGS050.esm",
"test2.esm",
];
assert_eq!(expected, settings.early_loading_plugins());
}
#[test]
fn early_loading_plugins_should_not_include_cc_plugins_for_fallout4_if_test_files_are_configured(
) {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
create_ccc_file(
&game_path.join("Fallout4.ccc"),
&["ccBGSFO4001-PipBoy(Black).esl"],
);
let ini_path = game_path.join("Fallout4.ini");
std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp\n").unwrap();
copy_to_dir(
"Blank.esp",
&game_path.join("Data"),
"Blank.esp",
GameId::Fallout4,
);
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Fallout4,
game_path,
&PathBuf::default(),
game_path.to_path_buf(),
)
.unwrap();
assert_eq!(FALLOUT4_HARDCODED_PLUGINS, settings.early_loading_plugins());
}
#[test]
fn early_loading_plugins_should_not_include_cc_plugins_for_starfield_if_test_files_are_configured(
) {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let my_games_path = tmp_dir.path().join("my games");
create_ccc_file(&my_games_path.join("Starfield.ccc"), &["test.esp"]);
let ini_path = game_path.join("Starfield.ini");
std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp\n").unwrap();
copy_to_dir(
"Blank.esp",
&game_path.join("Data"),
"Blank.esp",
GameId::Starfield,
);
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Starfield,
game_path,
&PathBuf::default(),
my_games_path,
)
.unwrap();
assert!(!settings.loads_early("test.esp"));
}
#[test]
fn early_loading_plugins_should_include_plugins_from_global_config_for_openmw() {
let tmp_dir = tempdir().unwrap();
let global_cfg_path = tmp_dir.path().join("openmw.cfg");
std::fs::write(
&global_cfg_path,
"config=local\ncontent=test.esm\ncontent=test.esp",
)
.unwrap();
let settings = game_with_game_path(GameId::OpenMW, tmp_dir.path());
let expected = &["builtin.omwscripts", "test.esm", "test.esp"];
assert_eq!(expected, settings.early_loading_plugins());
}
#[test]
fn early_loading_plugins_should_ignore_later_duplicate_entries() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let my_games_path = tmp_dir.path().join("my games");
create_ccc_file(
&my_games_path.join("Starfield.ccc"),
&["Starfield.esm", "test.esm"],
);
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Starfield,
game_path,
&PathBuf::default(),
my_games_path,
)
.unwrap();
let expected = &[
"Starfield.esm",
"Constellation.esm",
"OldMars.esm",
"ShatteredSpace.esm",
"SFBGS003.esm",
"SFBGS004.esm",
"SFBGS006.esm",
"SFBGS007.esm",
"SFBGS008.esm",
"SFBGS00D.esm",
"SFBGS047.esm",
"SFBGS050.esm",
"test.esm",
];
assert_eq!(expected, settings.early_loading_plugins());
}
#[test]
fn implicitly_active_plugins_should_include_early_loading_plugins() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let settings = game_with_game_path(GameId::SkyrimSE, game_path);
assert_eq!(
settings.early_loading_plugins(),
settings.implicitly_active_plugins()
);
}
#[test]
fn implicitly_active_plugins_should_include_test_files() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let ini_path = game_path.join("Skyrim.ini");
std::fs::write(&ini_path, "[General]\nsTestFile1=plugin.esp\n").unwrap();
let settings = GameSettings::with_local_and_my_games_paths(
GameId::SkyrimSE,
game_path,
&PathBuf::default(),
game_path.to_path_buf(),
)
.unwrap();
let mut expected_plugins = settings.early_loading_plugins().to_vec();
expected_plugins.push("plugin.esp".to_owned());
assert_eq!(expected_plugins, settings.implicitly_active_plugins());
}
#[test]
fn implicitly_active_plugins_should_only_include_valid_test_files_for_fallout4() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let ini_path = game_path.join("Fallout4.ini");
std::fs::write(
&ini_path,
"[General]\nsTestFile1=plugin.esp\nsTestFile2=Blank.esp",
)
.unwrap();
copy_to_dir(
"Blank.esp",
&game_path.join("Data"),
"Blank.esp",
GameId::Fallout4,
);
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Fallout4,
game_path,
&PathBuf::default(),
game_path.to_path_buf(),
)
.unwrap();
let mut expected_plugins = settings.early_loading_plugins().to_vec();
expected_plugins.push("Blank.esp".to_owned());
assert_eq!(expected_plugins, settings.implicitly_active_plugins());
}
#[test]
fn implicitly_active_plugins_should_only_include_valid_test_files_for_fallout4vr() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let ini_path = game_path.join("Fallout4VR.ini");
std::fs::write(
&ini_path,
"[General]\nsTestFile1=plugin.esp\nsTestFile2=Blank.esp",
)
.unwrap();
copy_to_dir(
"Blank.esp",
&game_path.join("Data"),
"Blank.esp",
GameId::Fallout4VR,
);
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Fallout4VR,
game_path,
&PathBuf::default(),
game_path.to_path_buf(),
)
.unwrap();
let mut expected_plugins = settings.early_loading_plugins().to_vec();
expected_plugins.push("Blank.esp".to_owned());
assert_eq!(expected_plugins, settings.implicitly_active_plugins());
}
#[test]
fn implicitly_active_plugins_should_include_plugins_with_nam_files_for_fallout_nv() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let data_path = game_path.join("Data");
create_dir_all(&data_path).unwrap();
File::create(data_path.join("plugin1.nam")).unwrap();
File::create(data_path.join("plugin2.NAM")).unwrap();
let settings = game_with_game_path(GameId::FalloutNV, game_path);
let expected_plugins = vec!["plugin1.esm", "plugin1.esp", "plugin2.esm", "plugin2.esp"];
let mut plugins = settings.implicitly_active_plugins().to_vec();
plugins.sort();
assert_eq!(expected_plugins, plugins);
}
#[test]
fn implicitly_active_plugins_should_include_update_esm_for_skyrim() {
let settings = game_with_generic_paths(GameId::Skyrim);
let plugins = settings.implicitly_active_plugins();
assert!(plugins.contains(&"Update.esm".to_owned()));
}
#[test]
fn implicitly_active_plugins_should_include_plugins_with_nam_files_for_games_other_than_fallout_nv(
) {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let data_path = game_path.join("Data");
create_dir_all(&data_path).unwrap();
File::create(data_path.join("plugin.nam")).unwrap();
let settings = game_with_game_path(GameId::Fallout3, game_path);
assert!(settings.implicitly_active_plugins().is_empty());
}
#[test]
fn implicitly_active_plugins_should_not_include_case_insensitive_duplicates() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let ini_path = game_path.join("Fallout4.ini");
std::fs::write(&ini_path, "[General]\nsTestFile1=fallout4.esm\n").unwrap();
let settings = GameSettings::with_local_and_my_games_paths(
GameId::Fallout4,
game_path,
&PathBuf::default(),
game_path.to_path_buf(),
)
.unwrap();
assert_eq!(
settings.early_loading_plugins(),
settings.implicitly_active_plugins()
);
}
#[test]
fn is_implicitly_active_should_return_true_iff_the_plugin_is_implicitly_active() {
let settings = game_with_generic_paths(GameId::Skyrim);
assert!(settings.is_implicitly_active("Update.esm"));
assert!(!settings.is_implicitly_active("Test.esm"));
}
#[test]
fn is_implicitly_active_should_match_case_insensitively() {
let settings = game_with_generic_paths(GameId::Skyrim);
assert!(settings.is_implicitly_active("update.esm"));
}
#[test]
fn loads_early_should_return_true_iff_the_plugin_loads_early() {
let settings = game_with_generic_paths(GameId::SkyrimSE);
assert!(settings.loads_early("Dawnguard.esm"));
assert!(!settings.loads_early("Test.esm"));
}
#[test]
fn loads_early_should_match_case_insensitively() {
let settings = game_with_generic_paths(GameId::SkyrimSE);
assert!(settings.loads_early("dawnguard.esm"));
}
#[test]
fn plugins_folder_should_be_a_child_of_the_game_path() {
let settings = game_with_generic_paths(GameId::Skyrim);
assert_eq!(Path::new("game/Data"), settings.plugins_directory());
}
#[test]
fn load_order_file_should_be_in_local_path_for_skyrim_and_none_for_other_games() {
let mut settings = game_with_generic_paths(GameId::Skyrim);
assert_eq!(
Path::new("local/loadorder.txt"),
settings.load_order_file().unwrap()
);
settings = game_with_generic_paths(GameId::SkyrimSE);
assert!(settings.load_order_file().is_none());
settings = game_with_generic_paths(GameId::OpenMW);
assert!(settings.load_order_file().is_none());
settings = game_with_generic_paths(GameId::Morrowind);
assert!(settings.load_order_file().is_none());
settings = game_with_generic_paths(GameId::Oblivion);
assert!(settings.load_order_file().is_none());
settings = game_with_generic_paths(GameId::Fallout3);
assert!(settings.load_order_file().is_none());
settings = game_with_generic_paths(GameId::FalloutNV);
assert!(settings.load_order_file().is_none());
settings = game_with_generic_paths(GameId::Fallout4);
assert!(settings.load_order_file().is_none());
}
#[test]
fn additional_plugins_directories_should_be_empty_if_game_is_not_fallout4_or_starfield() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
File::create(game_path.join("appxmanifest.xml")).unwrap();
let game_ids = [
GameId::Morrowind,
GameId::Oblivion,
GameId::Skyrim,
GameId::SkyrimSE,
GameId::SkyrimVR,
GameId::Fallout3,
GameId::FalloutNV,
];
for game_id in game_ids {
let settings = game_with_game_path(game_id, game_path);
assert!(settings.additional_plugins_directories().is_empty());
}
}
#[test]
fn additional_plugins_directories_should_be_empty_if_fallout4_is_not_from_the_microsoft_store()
{
let settings = game_with_generic_paths(GameId::Fallout4);
assert!(settings.additional_plugins_directories().is_empty());
}
#[test]
fn additional_plugins_directories_should_not_be_empty_if_game_is_fallout4_from_the_microsoft_store(
) {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
File::create(game_path.join("appxmanifest.xml")).unwrap();
let settings = game_with_game_path(GameId::Fallout4, game_path);
assert_eq!(
vec![
game_path.join(MS_FO4_AUTOMATRON_PATH),
game_path.join(MS_FO4_NUKA_WORLD_PATH),
game_path.join(MS_FO4_WASTELAND_PATH),
game_path.join(MS_FO4_TEXTURE_PACK_PATH),
game_path.join(MS_FO4_VAULT_TEC_PATH),
game_path.join(MS_FO4_FAR_HARBOR_PATH),
game_path.join(MS_FO4_CONTRAPTIONS_PATH),
],
settings.additional_plugins_directories()
);
}
#[test]
fn additional_plugins_directories_should_not_be_empty_if_game_is_starfield() {
let settings = game_with_generic_paths(GameId::Starfield);
assert_eq!(
vec![Path::new("my games").join("Data")],
settings.additional_plugins_directories()
);
}
#[test]
fn additional_plugins_directories_should_include_dlc_paths_if_game_is_ms_store_starfield() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
File::create(game_path.join("appxmanifest.xml")).unwrap();
let settings = game_with_game_path(GameId::Starfield, game_path);
assert_eq!(
vec![
Path::new("Data"),
&game_path.join("../../Old Mars/Content/Data"),
&game_path.join("../../Shattered Space/Content/Data"),
],
settings.additional_plugins_directories()
);
}
#[test]
fn additional_plugins_directories_should_read_from_openmw_cfgs() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path().join("game");
let my_games_path = tmp_dir.path().join("my games");
let global_cfg_path = game_path.join("openmw.cfg");
let cfg_path = my_games_path.join("openmw.cfg");
create_dir_all(global_cfg_path.parent().unwrap()).unwrap();
std::fs::write(&global_cfg_path, "config=\"../my games\"\ndata=\"foo/bar\"").unwrap();
create_dir_all(cfg_path.parent().unwrap()).unwrap();
std::fs::write(
&cfg_path,
"data=\"Path\\&&&\"&a&&&&\\Data Files\"\ndata=games/path",
)
.unwrap();
let settings =
GameSettings::with_local_path(GameId::OpenMW, &game_path, &my_games_path).unwrap();
let expected: Vec<PathBuf> = vec![
game_path.join("foo/bar"),
my_games_path.join("Path\\&\"a&&\\Data Files"),
my_games_path.join("games/path"),
];
assert_eq!(expected, settings.additional_plugins_directories());
}
#[test]
fn plugin_path_should_append_plugin_name_to_additional_plugin_directory_if_that_path_exists() {
let tmp_dir = tempdir().unwrap();
let other_dir = tmp_dir.path().join("other");
let plugin_name = "external.esp";
let expected_plugin_path = other_dir.join(plugin_name);
let mut settings = game_with_generic_paths(GameId::Fallout4);
settings.additional_plugins_directories = vec![other_dir.clone()];
copy_to_dir("Blank.esp", &other_dir, plugin_name, GameId::Fallout4);
let plugin_path = settings.plugin_path(plugin_name);
assert_eq!(expected_plugin_path, plugin_path);
}
#[test]
fn plugin_path_should_append_plugin_name_to_additional_plugin_directory_if_the_ghosted_path_exists(
) {
let tmp_dir = tempdir().unwrap();
let other_dir = tmp_dir.path().join("other");
let plugin_name = "external.esp";
let ghosted_plugin_name = "external.esp.ghost";
let expected_plugin_path = other_dir.join(ghosted_plugin_name);
let mut settings = game_with_generic_paths(GameId::Fallout4);
settings.additional_plugins_directories = vec![other_dir.clone()];
copy_to_dir(
"Blank.esp",
&other_dir,
ghosted_plugin_name,
GameId::Fallout4,
);
let plugin_path = settings.plugin_path(plugin_name);
assert_eq!(expected_plugin_path, plugin_path);
}
#[test]
fn plugin_path_should_not_resolve_ghosted_paths_for_openmw() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path().join("game");
let other_dir = tmp_dir.path().join("other");
let plugin_name = "external.esp";
let mut settings = game_with_game_path(GameId::OpenMW, &game_path);
settings.additional_plugins_directories = vec![other_dir.clone()];
copy_to_dir(
"Blank.esp",
&other_dir,
"external.esp.ghost",
GameId::OpenMW,
);
let plugin_path = settings.plugin_path(plugin_name);
assert_eq!(
game_path.join("resources/vfs").join(plugin_name),
plugin_path
);
}
#[test]
fn plugin_path_should_return_the_last_directory_that_contains_a_file_for_openmw() {
let tmp_dir = tempdir().unwrap();
let other_dir_1 = tmp_dir.path().join("other1");
let other_dir_2 = tmp_dir.path().join("other2");
let plugin_name = "Blank.esp";
let mut settings = game_with_game_path(GameId::OpenMW, tmp_dir.path());
settings.additional_plugins_directories = vec![other_dir_1.clone(), other_dir_2.clone()];
copy_to_dir("Blank.esp", &other_dir_1, plugin_name, GameId::OpenMW);
copy_to_dir("Blank.esp", &other_dir_2, plugin_name, GameId::OpenMW);
let plugin_path = settings.plugin_path(plugin_name);
assert_eq!(other_dir_2.join(plugin_name), plugin_path);
}
#[test]
fn plugin_path_should_return_plugins_dir_subpath_if_name_does_not_match_any_external_plugin() {
let settings = game_with_generic_paths(GameId::Fallout4);
let plugin_name = "DLCCoast.esm";
assert_eq!(
settings.plugins_directory().join(plugin_name),
settings.plugin_path(plugin_name)
);
}
#[test]
fn plugin_path_should_only_resolve_additional_starfield_plugin_paths_if_they_exist_or_are_ghosted_in_the_plugins_directory(
) {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path().join("game");
let data_path = game_path.join("Data");
let other_dir = tmp_dir.path().join("other");
let plugin_name_1 = "external1.esp";
let plugin_name_2 = "external2.esp";
let plugin_name_3 = "external3.esp";
let ghosted_plugin_name_3 = "external3.esp.ghost";
let mut settings = game_with_game_path(GameId::Starfield, &game_path);
settings.additional_plugins_directories = vec![other_dir.clone()];
copy_to_dir("Blank.esp", &other_dir, plugin_name_1, GameId::Starfield);
copy_to_dir("Blank.esp", &other_dir, plugin_name_2, GameId::Starfield);
copy_to_dir("Blank.esp", &data_path, plugin_name_2, GameId::Starfield);
copy_to_dir("Blank.esp", &other_dir, plugin_name_3, GameId::Starfield);
copy_to_dir(
"Blank.esp",
&data_path,
ghosted_plugin_name_3,
GameId::Starfield,
);
let plugin_1_path = settings.plugin_path(plugin_name_1);
let plugin_2_path = settings.plugin_path(plugin_name_2);
let plugin_3_path = settings.plugin_path(plugin_name_3);
assert_eq!(data_path.join(plugin_name_1), plugin_1_path);
assert_eq!(other_dir.join(plugin_name_2), plugin_2_path);
assert_eq!(other_dir.join(plugin_name_3), plugin_3_path);
}
#[test]
fn refresh_implicitly_active_plugins_should_update_early_loading_and_implicitly_active_plugins()
{
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let mut settings = GameSettings::with_local_and_my_games_paths(
GameId::SkyrimSE,
game_path,
&PathBuf::default(),
game_path.to_path_buf(),
)
.unwrap();
let hardcoded_plugins = vec![
"Skyrim.esm",
"Update.esm",
"Dawnguard.esm",
"HearthFires.esm",
"Dragonborn.esm",
];
assert_eq!(hardcoded_plugins, settings.early_loading_plugins());
assert_eq!(hardcoded_plugins, settings.implicitly_active_plugins());
std::fs::write(game_path.join("Skyrim.ccc"), "ccBGSSSE002-ExoticArrows.esl").unwrap();
std::fs::write(
game_path.join("Skyrim.ini"),
"[General]\nsTestFile1=plugin.esp\n",
)
.unwrap();
settings.refresh_implicitly_active_plugins().unwrap();
let mut expected_plugins = hardcoded_plugins;
expected_plugins.push("ccBGSSSE002-ExoticArrows.esl");
assert_eq!(expected_plugins, settings.early_loading_plugins());
expected_plugins.push("plugin.esp");
assert_eq!(expected_plugins, settings.implicitly_active_plugins());
}
#[test]
fn get_target_modified_timestamp_should_return_the_modified_timestamp_of_a_symlinks_target_file(
) {
let tmp_dir = tempdir().unwrap();
let file_path = tmp_dir.path().join("file");
let symlink_path = tmp_dir.path().join("symlink");
std::fs::File::create(&file_path).unwrap();
symlink_file(&file_path, &symlink_path);
let symlink_timestamp = symlink_path.symlink_metadata().unwrap().modified().unwrap();
let file_timestamp = symlink_timestamp - std::time::Duration::from_secs(1);
assert_ne!(symlink_timestamp, file_timestamp);
let file = File::options().append(true).open(file_path).unwrap();
file.set_modified(file_timestamp).unwrap();
let mut dir_entries = tmp_dir
.path()
.read_dir()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
dir_entries.sort_by_key(DirEntry::file_name);
assert!(dir_entries[0].file_type().unwrap().is_file());
assert_eq!(
file_timestamp,
get_target_modified_timestamp(&dir_entries[0]).unwrap()
);
assert!(dir_entries[1].file_type().unwrap().is_symlink());
assert_eq!(
file_timestamp,
get_target_modified_timestamp(&dir_entries[1]).unwrap()
);
}
#[test]
fn find_plugins_in_directories_should_sort_files_by_modification_timestamp() {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let plugin_names = [
"Blank.esp",
"Blank - Different.esp",
"Blank - Master Dependent.esp",
NON_ASCII,
];
copy_to_dir("Blank.esp", game_path, NON_ASCII, GameId::Oblivion);
for (i, plugin_name) in plugin_names.iter().enumerate() {
let path = game_path.join(plugin_name);
if !path.exists() {
copy_to_dir(plugin_name, game_path, plugin_name, GameId::Oblivion);
}
set_file_timestamps(&path, i.try_into().unwrap());
}
let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Oblivion);
let expected: Vec<_> = plugin_names.iter().map(|n| game_path.join(n)).collect();
assert_eq!(expected, result);
}
#[test]
fn find_plugins_in_directories_should_sort_files_by_descending_uppercased_filename_if_timestamps_are_equal(
) {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let non_ascii = NON_ASCII;
let plugin_names = [
"Blank.esm",
"Blank.esp",
"Blank - Different.esp",
"Blank - Master Dependent.esp",
non_ascii,
];
copy_to_dir("Blank.esp", game_path, non_ascii, GameId::Oblivion);
for (i, plugin_name) in plugin_names.iter().enumerate() {
let path = game_path.join(plugin_name);
if !path.exists() {
copy_to_dir(plugin_name, game_path, plugin_name, GameId::Oblivion);
}
set_file_timestamps(&path, i.try_into().unwrap());
}
let timestamp = 3;
set_file_timestamps(&game_path.join("Blank - Different.esp"), timestamp);
set_file_timestamps(&game_path.join("Blank - Master Dependent.esp"), timestamp);
copy_to_dir("Blank.esp", game_path, "a.esp", GameId::Oblivion);
set_file_timestamps(&game_path.join("a.esp"), timestamp);
let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Oblivion);
let plugin_paths = vec![
game_path.join("Blank.esm"),
game_path.join("Blank.esp"),
game_path.join("Blank - Master Dependent.esp"),
game_path.join("Blank - Different.esp"),
game_path.join("a.esp"),
game_path.join(non_ascii),
];
assert_eq!(plugin_paths, result);
}
#[test]
fn find_plugins_in_directories_should_sort_files_by_descending_uppercased_filename_if_timestamps_are_equal_and_game_is_starfield(
) {
let tmp_dir = tempdir().unwrap();
let game_path = tmp_dir.path();
let plugin_names = [
"Blank.full.esm",
"Blank.small.esm",
"Blank.medium.esm",
"Blank.esp",
"Blank - Override.esp",
"a.esp",
];
let timestamp = 1_321_009_991;
for plugin_name in &plugin_names[..plugin_names.len() - 1] {
let path = game_path.join(plugin_name);
if !path.exists() {
copy_to_dir(plugin_name, game_path, plugin_name, GameId::Starfield);
}
set_file_timestamps(&path, timestamp);
}
copy_to_dir(
"Blank.esp",
game_path,
plugin_names.last().unwrap(),
GameId::Starfield,
);
let result = find_plugins_in_directories(once(&game_path.to_path_buf()), GameId::Starfield);
let plugin_paths = vec![
game_path.join("Blank.small.esm"),
game_path.join("Blank.medium.esm"),
game_path.join("Blank.full.esm"),
game_path.join("Blank.esp"),
game_path.join("Blank - Override.esp"),
game_path.join("a.esp"),
];
assert_eq!(plugin_paths, result);
}
#[test]
fn find_plugins_in_directories_should_find_symlinks_to_plugins() {
const BLANK_ESM: &str = "Blank.esm";
const BLANK_ESP: &str = "Blank.esp";
let tmp_dir = tempdir().unwrap();
let data_path = tmp_dir.path().join("game");
let other_path = tmp_dir.path().join("other");
copy_to_dir(BLANK_ESM, &data_path, BLANK_ESM, GameId::OpenMW);
copy_to_dir(BLANK_ESP, &other_path, BLANK_ESP, GameId::OpenMW);
symlink_file(&other_path.join(BLANK_ESP), &data_path.join(BLANK_ESP));
let result = find_plugins_in_directories(once(&data_path), GameId::OpenMW);
let plugin_paths = vec![data_path.join(BLANK_ESM), data_path.join(BLANK_ESP)];
assert_eq!(plugin_paths, result);
}
}