Support plugin and archive symlinks

On Linux, and on Windows when the game is OpenMW or Oblivion Remastered.
This commit is contained in:
Oliver Hamlet
2025-08-03 14:26:17 +01:00
parent d6b0244203
commit 9f45282bee
5 changed files with 79 additions and 20 deletions
Generated
+4 -12
View File
@@ -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"
+1 -1
View File
@@ -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"
+4 -3
View File
@@ -482,9 +482,10 @@ private:
const std::vector<std::pair<std::string, bool>>& 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) {
+70 -3
View File
@@ -625,14 +625,15 @@ fn find_archives(
data_path: &Path,
) -> std::io::Result<Vec<PathBuf>> {
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<Vec<PathBuf>> {
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;
-1
View File
@@ -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