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.
This commit is contained in:
Oliver Hamlet
2025-01-29 18:38:32 +00:00
parent a8f9a5734a
commit 08639a1e8f
2 changed files with 160 additions and 21 deletions
+156 -21
View File
@@ -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()
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),
.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_in_parent_paths<'a>(
path: &Path,
parent_paths: impl Iterator<Item = &'a PathBuf>,
try_with_ghost_extension: bool,
) -> Option<PathBuf> {
for parent_path in parent_paths {
let joined_path = parent_path.join(path);
if joined_path.exists() {
return Some(joined_path);
}
if try_with_ghost_extension {
let ghosted_path = add_ghost_extension(joined_path);
if ghosted_path.exists() {
return Some(ghosted_path);
}
}
}
None
}
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);
let try_with_ghost_extension = state.game_type.allows_ghosted_plugins()
&& has_unghosted_plugin_file_extension(state.game_type, &path);
if path.exists() {
// 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;
}
if has_unghosted_plugin_file_extension(state.game_type, &path) {
path = add_ghost_extension(path);
}
if path.exists() {
return path;
}
}
// Now check the main data path.
let path = state.data_path.join(path);
let joined_path = state.data_path.join(path);
if !path.exists() && has_unghosted_plugin_file_extension(state.game_type, &path) {
add_ghost_extension(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);
+4
View File
@@ -49,6 +49,10 @@ impl GameType {
| GameType::Starfield
)
}
fn allows_ghosted_plugins(self) -> bool {
self != GameType::OpenMW
}
}
#[derive(Debug)]