diff --git a/crates/benilla-formats/src/chain.rs b/crates/benilla-formats/src/chain.rs index 4fd5d238f..8974ae524 100644 --- a/crates/benilla-formats/src/chain.rs +++ b/crates/benilla-formats/src/chain.rs @@ -59,6 +59,19 @@ fn is_patch_glob_match(name: &str) -> bool { /// [`VANILLA_BASE_ORDER`] archives, `patch.MPQ`, every `patch-?.MPQ` by case-folded name (the /// reference sorts descending with `strnicmp` and walks backwards), then `speech2.MPQ`. Names /// match case-insensitively and come back as found on disk. +/// `$BENILLA_STOCK_CHAIN` set and not empty: a `Data` directory mounts only the stock 1.12.1 +/// archives, its base set, `patch.MPQ`, `patch-2.MPQ` and `speech2.MPQ`, and leaves out every +/// other `patch-?.MPQ` (texture packs, interface mods), so the tests that read the install see the +/// data they assert. The language pack's folder is not filtered. `scripts/unit-tests.sh` sets it. +fn stock_chain_only() -> bool { + std::env::var_os("BENILLA_STOCK_CHAIN").is_some_and(|v| !v.is_empty()) +} + +/// Whether `name` is a stock 1.12.1 patch archive. +fn is_stock_patch(name: &str) -> bool { + name.eq_ignore_ascii_case("patch-2.MPQ") +} + fn mount_order(dir_names: &[String]) -> Vec { let find = |want: &str| { dir_names @@ -86,11 +99,17 @@ impl Chain { /// `"Failed to open archive"` and goes on, because a skipped corrupt archive surfaces only as /// missing files far downstream. pub fn open(path: &Path) -> Result { - Self::open_without(path, &[]) + Self::open_dir(path, stock_chain_only(), &[]) } /// [`Chain::open`], `Data`'s archives named in `skip` left out. fn open_without(path: &Path, skip: &[String]) -> Result { + Self::open_dir(path, stock_chain_only(), skip) + } + + /// [`Chain::open`]; `stock` keeps the `Data` directory to its stock archives + /// ([`stock_chain_only`]). + fn open_dir(path: &Path, stock: bool, skip: &[String]) -> Result { let mut archives = Vec::new(); if path.is_dir() { let mut names: Vec = std::fs::read_dir(path) @@ -105,7 +124,9 @@ impl Chain { // read_dir order is arbitrary; sort so case-variant ties resolve deterministically. names.sort(); for name in mount_order(&names) { - if skip.iter().any(|s| s.eq_ignore_ascii_case(&name)) { + if skip.iter().any(|s| s.eq_ignore_ascii_case(&name)) + || (stock && is_patch_glob_match(&name) && !is_stock_patch(&name)) + { continue; } let mpq = path.join(&name); @@ -161,7 +182,9 @@ impl Chain { let folder = pack .pack_folder(path) .context("ruRU language pack is not installed")?; - chain.archives.extend(Self::open(&folder)?.archives); + chain + .archives + .extend(Self::open_dir(&folder, false, &[])?.archives); } chain.locale = Locale::for_language_pack(pack); Ok(chain) @@ -473,6 +496,54 @@ mod tests { ); } + /// The stock chain keeps the base set, `patch.MPQ` and `patch-2.MPQ`, and drops the other + /// `patch-?.MPQ` in any case; without it every patch mounts. + #[test] + fn the_stock_chain_mounts_only_the_stock_patches() { + let dir = std::env::temp_dir().join(format!( + "benilla-chain-stock-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + for archive in [ + "interface.MPQ", + "patch.MPQ", + "patch-2.MPQ", + "patch-A.mpq", + "patch-F.MPQ", + ] { + let line = format!("X\\{archive}\r\n"); + std::fs::write( + dir.join(archive), + fixture_mpq(&[("(listfile)", line.as_bytes())]), + ) + .unwrap(); + } + let stock = Chain::open_dir(&dir, true, &[]) + .expect("open") + .enumerate_archive_files("x\\"); + let every = Chain::open_dir(&dir, false, &[]) + .expect("open") + .enumerate_archive_files("x\\"); + let _ = std::fs::remove_dir_all(&dir); + assert_eq!( + stock, + ["X\\patch.MPQ", "X\\patch-2.MPQ", "X\\interface.MPQ"] + ); + assert_eq!( + every, + [ + "X\\patch.MPQ", + "X\\patch-2.MPQ", + "X\\patch-A.mpq", + "X\\patch-F.MPQ", + "X\\interface.MPQ" + ] + ); + } + /// An archive left out by name, in any case, does not mount; the others keep their order. #[test] fn a_left_out_archive_does_not_mount() { @@ -491,7 +562,7 @@ mod tests { ) .unwrap(); } - let chain = Chain::open_without(&dir, &["PATCH-3.mpq".to_owned()]).expect("open"); + let chain = Chain::open_dir(&dir, false, &["PATCH-3.mpq".to_owned()]).expect("open"); let walked = chain.enumerate_archive_files("x\\"); let _ = std::fs::remove_dir_all(&dir); assert_eq!( diff --git a/scripts/unit-tests.sh b/scripts/unit-tests.sh index bf822323c..f31d204aa 100755 --- a/scripts/unit-tests.sh +++ b/scripts/unit-tests.sh @@ -7,6 +7,9 @@ # scripts/unit-tests.sh --workspace # scripts/unit-tests.sh -p benilla-app --no-default-features --lib set -euo pipefail +# The tests that read the install assert the stock 1.12.1 data: the chain leaves out any other +# patch archive a player added (texture packs, interface mods) (`Chain::open`). +export BENILLA_STOCK_CHAIN=1 if cargo nextest --version >/dev/null 2>&1; then exec cargo nextest run --no-fail-fast --no-tests=pass "$@" fi