From 08639a1e8f8ee68a675654fb0bfa523da07b1200 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Wed, 29 Jan 2025 18:37:55 +0000 Subject: [PATCH] Fix path handling for OpenMW - Disable support for plugin ghosting for OpenMW, it's not clear if it's useful and it increases complexity. - Search additional data paths in reverse order. --- src/function/path.rs | 177 ++++++++++++++++++++++++++++++++++++++----- src/lib.rs | 4 + 2 files changed, 160 insertions(+), 21 deletions(-) diff --git a/src/function/path.rs b/src/function/path.rs index c56b15d..d411985 100644 --- a/src/function/path.rs +++ b/src/function/path.rs @@ -26,10 +26,13 @@ fn has_unghosted_plugin_file_extension(game_type: GameType, path: &Path) -> bool pub fn has_plugin_file_extension(game_type: GameType, path: &Path) -> bool { match path.extension() { - Some(ext) if ext.eq_ignore_ascii_case(GHOST_EXTENSION) => path - .file_stem() - .map(|s| has_unghosted_plugin_file_extension(game_type, Path::new(s))) - .unwrap_or(false), + Some(ext) + if game_type.allows_ghosted_plugins() && ext.eq_ignore_ascii_case(GHOST_EXTENSION) => + { + path.file_stem() + .map(|s| has_unghosted_plugin_file_extension(game_type, Path::new(s))) + .unwrap_or(false) + } Some(ext) => is_unghosted_plugin_file_extension(game_type, ext), _ => false, } @@ -47,6 +50,10 @@ fn add_ghost_extension(path: PathBuf) -> PathBuf { } pub fn normalise_file_name(game_type: GameType, name: &OsStr) -> &OsStr { + if !game_type.allows_ghosted_plugins() { + return name; + } + let path = Path::new(name); if path .extension() @@ -64,31 +71,62 @@ pub fn normalise_file_name(game_type: GameType, name: &OsStr) -> &OsStr { name } -pub fn resolve_path(state: &State, path: &Path) -> PathBuf { - // First check external data paths, as files there may override files in the main data path. - for data_path in &state.additional_data_paths { - let mut path = data_path.join(path); +pub fn resolve_path_in_parent_paths<'a>( + path: &Path, + parent_paths: impl Iterator, + try_with_ghost_extension: bool, +) -> Option { + for parent_path in parent_paths { + let joined_path = parent_path.join(path); - if path.exists() { - return path; + if joined_path.exists() { + return Some(joined_path); } - if has_unghosted_plugin_file_extension(state.game_type, &path) { - path = add_ghost_extension(path); - } + if try_with_ghost_extension { + let ghosted_path = add_ghost_extension(joined_path); - if path.exists() { - return path; + if ghosted_path.exists() { + return Some(ghosted_path); + } } } - // Now check the main data path. - let path = state.data_path.join(path); + None +} - if !path.exists() && has_unghosted_plugin_file_extension(state.game_type, &path) { - add_ghost_extension(path) +pub fn resolve_path(state: &State, path: &Path) -> PathBuf { + let try_with_ghost_extension = state.game_type.allows_ghosted_plugins() + && has_unghosted_plugin_file_extension(state.game_type, &path); + + // OpenMW uses the last data directory that contains a matching path, with + // the main data path being listed first, while for other games the first + // additional data path that contains a matching path is used, and then the + // main data path is checked. + let result = match state.game_type { + GameType::OpenMW => resolve_path_in_parent_paths( + path, + state.additional_data_paths.iter().rev(), + try_with_ghost_extension, + ), + _ => resolve_path_in_parent_paths( + path, + state.additional_data_paths.iter(), + try_with_ghost_extension, + ), + }; + + if let Some(path) = result { + return path; + } + + // Now check the main data path. + let joined_path = state.data_path.join(path); + + if !joined_path.exists() && try_with_ghost_extension { + add_ghost_extension(joined_path) } else { - path + joined_path } } @@ -478,6 +516,15 @@ mod tests { )); } + #[test] + fn has_plugin_file_extension_should_return_false_if_the_path_has_a_ghosted_plugin_extension_for_openmw( + ) { + assert!(!has_plugin_file_extension( + GameType::OpenMW, + Path::new("plugin.esp.Ghost") + )); + } + #[test] fn has_plugin_file_extension_should_return_false_if_the_path_has_a_non_plugin_extension() { assert!(!has_plugin_file_extension( @@ -523,6 +570,38 @@ mod tests { assert_eq!(PathBuf::from("plugin.ghost"), path); } + #[test] + fn normalise_file_name_should_remove_ghost_extension_from_a_plugin_filename() { + assert_eq!( + "plugin.esp", + normalise_file_name(GameType::Oblivion, OsStr::new("plugin.esp.ghost")) + ); + } + + #[test] + fn normalise_file_name_should_not_remove_ghost_extension_from_a_non_plugin_filename() { + assert_eq!( + "plugin.ghost", + normalise_file_name(GameType::Oblivion, OsStr::new("plugin.ghost")) + ); + } + + #[test] + fn normalise_file_name_should_return_a_non_ghost_extension_filename_unchanged() { + assert_eq!( + "plugin.esp", + normalise_file_name(GameType::Oblivion, OsStr::new("plugin.esp")) + ); + } + + #[test] + fn normalise_file_name_should_return_the_path_unchanged_for_openmw() { + assert_eq!( + "plugin.esp.ghost", + normalise_file_name(GameType::OpenMW, OsStr::new("plugin.esp.ghost")) + ); + } + #[test] fn resolve_path_should_return_the_data_path_prefixed_path_if_it_exists() { let data_path = PathBuf::from("."); @@ -563,6 +642,16 @@ mod tests { ); } + #[test] + fn resolve_path_should_not_add_ghost_extension_for_openmw() { + let data_path = PathBuf::from("."); + let state = State::new(GameType::OpenMW, data_path.clone()); + let input_path = Path::new("plugin.esp"); + let resolved_path = resolve_path(&state, input_path); + + assert_eq!(data_path.join(input_path), resolved_path); + } + #[test] fn resolve_path_should_check_external_data_paths_in_order_before_data_path() { use std::fs::copy; @@ -576,6 +665,11 @@ mod tests { create_dir(&external_data_path_1).unwrap(); create_dir(&external_data_path_2).unwrap(); create_dir(&data_path).unwrap(); + copy( + Path::new("Cargo.toml"), + external_data_path_1.join("Cargo.toml"), + ) + .unwrap(); copy( Path::new("Cargo.toml"), external_data_path_2.join("Cargo.toml"), @@ -584,7 +678,48 @@ mod tests { copy(Path::new("Cargo.toml"), data_path.join("Cargo.toml")).unwrap(); let mut state = State::new(GameType::Skyrim, data_path); - state.set_additional_data_paths(vec![external_data_path_1, external_data_path_2.clone()]); + state.set_additional_data_paths(vec![ + external_data_path_1.clone(), + external_data_path_2.clone(), + ]); + + let input_path = Path::new("Cargo.toml"); + let resolved_path = resolve_path(&state, input_path); + + assert_eq!(external_data_path_1.join(input_path), resolved_path); + } + + #[test] + fn resolve_path_should_check_external_data_paths_in_reverse_order_before_data_path_for_openmw() + { + use std::fs::copy; + use std::fs::create_dir; + + let tmp_dir = tempfile::tempdir().unwrap(); + let external_data_path_1 = tmp_dir.path().join("Data1"); + let external_data_path_2 = tmp_dir.path().join("Data2"); + let data_path = tmp_dir.path().join("Data3"); + + create_dir(&external_data_path_1).unwrap(); + create_dir(&external_data_path_2).unwrap(); + create_dir(&data_path).unwrap(); + copy( + Path::new("Cargo.toml"), + external_data_path_1.join("Cargo.toml"), + ) + .unwrap(); + copy( + Path::new("Cargo.toml"), + external_data_path_2.join("Cargo.toml"), + ) + .unwrap(); + copy(Path::new("Cargo.toml"), data_path.join("Cargo.toml")).unwrap(); + + let mut state = State::new(GameType::OpenMW, data_path); + state.set_additional_data_paths(vec![ + external_data_path_1.clone(), + external_data_path_2.clone(), + ]); let input_path = Path::new("Cargo.toml"); let resolved_path = resolve_path(&state, input_path); diff --git a/src/lib.rs b/src/lib.rs index 3f81cb4..6fb1edc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,10 @@ impl GameType { | GameType::Starfield ) } + + fn allows_ghosted_plugins(self) -> bool { + self != GameType::OpenMW + } } #[derive(Debug)]