From 9f45282bee375e5ff5f8a4a0f39fa4c94fc8b11e Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Sun, 3 Aug 2025 12:55:09 +0100 Subject: [PATCH] Support plugin and archive symlinks On Linux, and on Windows when the game is OpenMW or Oblivion Remastered. --- Cargo.lock | 16 ++---- Cargo.toml | 2 +- cpp/src/tests/common_game_test_fixture.h | 7 ++- src/game.rs | 73 +++++++++++++++++++++++- src/lib.rs | 1 - 5 files changed, 79 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc168182..66114722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,16 +535,15 @@ dependencies = [ [[package]] name = "libloadorder" -version = "18.4.0" +version = "18.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4de694d488b4ce2a775d59825ec30d206546ec87fe0866717e6db25c46a164" +checksum = "e3ffd695d1e67468555244807358d53524a372c4f5d2becd0890de48a106e797" dependencies = [ "dirs", "encoding_rs", "esplugin", "keyvalues-parser", "rayon", - "regex", "rust-ini", "unicase", "windows", @@ -1034,13 +1033,12 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +checksum = "e7295b7ce3bf4806b419dc3420745998b447178b7005e2011947b38fc5aa6791" dependencies = [ "cfg-if", "ordered-multimap", - "trim-in-place", "unicase", ] @@ -1235,12 +1233,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "trim-in-place" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" - [[package]] name = "typenum" version = "1.18.0" diff --git a/Cargo.toml b/Cargo.toml index 67d9b758..6bec55e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true [dependencies] crc32fast = "1.5.0" esplugin = "6.1.3" -libloadorder = "18.4.0" +libloadorder = "18.5.0" log = { version = "0.4.26", features = ["std"] } loot-condition-interpreter = "5.3.2" petgraph = "0.8.1" diff --git a/cpp/src/tests/common_game_test_fixture.h b/cpp/src/tests/common_game_test_fixture.h index dc47b44f..b6517793 100644 --- a/cpp/src/tests/common_game_test_fixture.h +++ b/cpp/src/tests/common_game_test_fixture.h @@ -482,9 +482,10 @@ private: const std::vector>& loadOrder) const { if (gameType_ == GameType::tes3) { std::ofstream out(gamePath / "Morrowind.ini"); - for (const auto& plugin : loadOrder) { - if (plugin.second) { - out << "GameFile0=" << plugin.first << std::endl; + out << "[Game Files]" << std::endl; + for (auto i = 0; i < loadOrder.size(); i += 1) { + if (loadOrder[i].second) { + out << "GameFile" << i << "=" << loadOrder[i].first << std::endl; } } } else if (gameType_ == GameType::openmw) { diff --git a/src/game.rs b/src/game.rs index 4b2ad4b1..dd2f03a8 100644 --- a/src/game.rs +++ b/src/game.rs @@ -625,14 +625,15 @@ fn find_archives( data_path: &Path, ) -> std::io::Result> { let extension = archive_file_extension(game_type); + let allow_symlinks = allow_archive_symlinks(game_type); let mut archive_paths = Vec::new(); for path in additional_data_paths { - let paths = find_archives_in_path(path, extension)?; + let paths = find_archives_in_path(path, extension, allow_symlinks)?; archive_paths.extend(paths); } - let paths = find_archives_in_path(data_path, extension)?; + let paths = find_archives_in_path(data_path, extension, allow_symlinks)?; archive_paths.extend(paths); Ok(archive_paths) @@ -645,18 +646,29 @@ fn archive_file_extension(game_type: GameType) -> &'static str { } } +fn allow_archive_symlinks(game_type: GameType) -> bool { + // Assuming support for archive symlinks matches support for plugin symlinks. + !cfg!(windows) || matches!(game_type, GameType::OblivionRemastered | GameType::OpenMW) +} + fn find_archives_in_path( parent_path: &Path, archive_file_extension: &str, + allow_symlinks: bool, ) -> std::io::Result> { if !parent_path.exists() { return Ok(Vec::new()); } + #[expect( + clippy::filetype_is_file, + reason = "Only files are supported except in specific cases" + )] let paths = std::fs::read_dir(parent_path)? .filter_map(Result::ok) .filter(|e| { - e.file_type().map(|f| f.is_file()).unwrap_or(false) + e.file_type() + .is_ok_and(|f| f.is_file() || (allow_symlinks && f.is_symlink())) && iends_with_ascii(&e.file_name().to_string_lossy(), archive_file_extension) }) .map(|e| e.path()) @@ -2044,6 +2056,61 @@ mod tests { } } + mod find_archives { + use tempfile::tempdir; + + use super::*; + + #[cfg(windows)] + pub(crate) fn symlink_file(original: &Path, link: &Path) { + std::os::windows::fs::symlink_file(original, link).unwrap(); + } + + #[cfg(unix)] + pub(crate) fn symlink_file(original: &Path, link: &Path) { + std::os::unix::fs::symlink(original, link).unwrap(); + } + + #[cfg(unix)] + #[parameterized_test(ALL_GAME_TYPES)] + fn should_find_symlinked_archives_on_linux(game_type: GameType) { + test_find_symlinked_archives(game_type, true); + } + + #[cfg(windows)] + #[parameterized_test(ALL_GAME_TYPES)] + fn should_find_symlinked_archives_for_only_openmw_and_oblivion_remastered_on_windows( + game_type: GameType, + ) { + test_find_symlinked_archives( + game_type, + matches!(game_type, GameType::OpenMW | GameType::OblivionRemastered), + ); + } + + fn test_find_symlinked_archives(game_type: GameType, should_include_symlinks: bool) { + let tmp_dir = tempdir().unwrap(); + + let extension = archive_file_extension(game_type); + let archive_path = tmp_dir.path().join(format!("archive{extension}")); + let symlink_path = tmp_dir.path().join(format!("archive.symlink{extension}")); + + std::fs::File::create(&archive_path).unwrap(); + + symlink_file(&archive_path, &symlink_path); + + let archives = find_archives(game_type, &[], tmp_dir.path()).unwrap(); + + assert!(archives.contains(&archive_path)); + + if should_include_symlinks { + assert!(archives.contains(&symlink_path)); + } else { + assert!(!archives.contains(&symlink_path)); + } + } + } + #[test] fn to_plugin_sorting_data_should_filter_out_files_with_false_constraints() { let game_type = GameType::Oblivion; diff --git a/src/lib.rs b/src/lib.rs index 3488b05b..c720c35b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ // Allow some lints that are denied at the workspace level. #![allow( - clippy::filetype_is_file, clippy::must_use_candidate, clippy::missing_errors_doc, clippy::wildcard_enum_match_arm