diff --git a/Cargo.lock b/Cargo.lock index d97693fc..71d47924 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1167,6 +1167,8 @@ dependencies = [ "modio", "obake", "opener", + "path-slash", + "rayon", "regex", "repak", "reqwest", @@ -2880,6 +2882,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + [[package]] name = "patternsleuth_scanner" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c058813f..bfc872dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ lazy_static = "1.4.0" modio = { git = "https://github.com/trumank/modio-rs.git", branch = "dev", features = ["rustls-tls"] } obake = { version = "1.0.5", features = ["serde"] } opener = "0.6.1" +path-slash = "0.2.1" +rayon = "1.7.0" regex = "1.9.3" repak = { git = "https://github.com/trumank/repak.git", version = "0.1.3" } reqwest = { version = "0.11.18", features = ["blocking"] } diff --git a/src/gui/message.rs b/src/gui/message.rs index df46613c..a8a491d1 100644 --- a/src/gui/message.rs +++ b/src/gui/message.rs @@ -416,6 +416,7 @@ impl LintMods { store: Arc, mods: Vec, enabled_lints: BTreeSet, + game_pak_path: Option, tx: Sender, ctx: egui::Context, ) -> MessageHandle<()> { @@ -429,7 +430,11 @@ impl LintMods { let report_res = match mod_path_pairs_res { Ok(pairs) => tokio::task::spawn_blocking(move || { - crate::mod_lints::run_lints(&enabled_lints, BTreeSet::from_iter(pairs)) + crate::mod_lints::run_lints( + &enabled_lints, + BTreeSet::from_iter(pairs), + game_pak_path, + ) }) .await .unwrap(), diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 59a36c44..283d30e9 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -97,6 +97,7 @@ struct LintOptions { shader_files: bool, non_asset_files: bool, split_asset_pairs: bool, + unmodified_game_assets: bool, } enum LastActionStatus { @@ -900,6 +901,16 @@ impl App { ui.label("Mods containing split {uexp, uasset} pairs"); ui.add(toggle_switch(&mut self.lint_options.split_asset_pairs)); ui.end_row(); + + ui.label("Mods containing unmodified game assets"); + ui.add_enabled( + self.state.config.drg_pak_path.is_some(), + toggle_switch(&mut self.lint_options.unmodified_game_assets), + ) + .on_disabled_hover_text( + "This lint requires DRG pak path to be specified", + ); + ui.end_row(); }); }); @@ -942,6 +953,10 @@ impl App { LintId::SPLIT_ASSET_PAIRS, self.lint_options.split_asset_pairs, ), + ( + LintId::UNMODIFIED_GAME_ASSETS, + self.lint_options.unmodified_game_assets, + ), ]); trace!(?lint_options); @@ -956,6 +971,7 @@ impl App { .into_iter() .filter_map(|(lint, enabled)| enabled.then_some(lint)), ), + self.state.config.drg_pak_path.clone(), self.tx.clone(), ctx.clone(), )); @@ -1228,6 +1244,34 @@ impl App { }); } } + + if let Some(unmodified_game_assets_mods) = &report.unmodified_game_assets_mods { + if !unmodified_game_assets_mods.is_empty() { + CollapsingHeader::new( + RichText::new( + "⚠ Mod(s) with unmodified game assets detected", + ) + .color(AMBER), + ) + .default_open(true) + .show(ui, |ui| { + unmodified_game_assets_mods.iter().for_each(|(r#mod, files)| { + CollapsingHeader::new( + RichText::new(format!( + "⚠ {} includes unmodified game assets", + r#mod.url + )) + .color(AMBER), + ) + .show(ui, |ui| { + files.iter().for_each(|file| { + ui.label(file); + }); + }); + }); + }); + } + } }); } else { ui.spinner(); diff --git a/src/gui/toggle_switch.rs b/src/gui/toggle_switch.rs index cba76c31..c155df69 100644 --- a/src/gui/toggle_switch.rs +++ b/src/gui/toggle_switch.rs @@ -3,7 +3,6 @@ use eframe::egui; -/// Here is the same code again, but a bit more compact: fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0); let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); @@ -29,13 +28,6 @@ fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { response } -// A wrapper that allows the more idiomatic usage pattern: `ui.add(toggle(&mut my_bool))` -/// iOS-style toggle switch. -/// -/// ## Example: -/// ``` ignore -/// ui.add(toggle(&mut my_bool)); -/// ``` #[allow(clippy::needless_pass_by_ref_mut)] pub fn toggle_switch(on: &mut bool) -> impl egui::Widget + '_ { move |ui: &mut egui::Ui| toggle_ui(ui, on) diff --git a/src/main.rs b/src/main.rs index 642f4752..7bf11585 100644 --- a/src/main.rs +++ b/src/main.rs @@ -228,7 +228,7 @@ fn init_provider(state: &mut State, url: String, factory: &ProviderFactory) -> R } async fn action_integrate(action: ActionIntegrate) -> Result<()> { - let path_game_pak = action + let game_pak_path = action .fsd_pak .or_else(|| { DRGInstallation::find() @@ -236,7 +236,7 @@ async fn action_integrate(action: ActionIntegrate) -> Result<()> { .map(DRGInstallation::main_pak) }) .context("Could not find DRG pak file, please specify manually with the --fsd_pak flag")?; - debug!(?path_game_pak); + debug!(?game_pak_path); let mut state = State::init()?; @@ -247,7 +247,7 @@ async fn action_integrate(action: ActionIntegrate) -> Result<()> { .collect::>(); resolve_unordered_and_integrate_with_provider_init( - path_game_pak, + game_pak_path, &mut state, &mod_specs, action.update, @@ -257,7 +257,7 @@ async fn action_integrate(action: ActionIntegrate) -> Result<()> { } async fn action_integrate_profile(action: ActionIntegrateProfile) -> Result<()> { - let path_game_pak = action + let game_pak_path = action .fsd_pak .or_else(|| { DRGInstallation::find() @@ -265,7 +265,7 @@ async fn action_integrate_profile(action: ActionIntegrateProfile) -> Result<()> .map(DRGInstallation::main_pak) }) .context("Could not find DRG pak file, please specify manually with the --fsd_pak flag")?; - debug!(?path_game_pak); + debug!(?game_pak_path); let mut state = State::init()?; @@ -275,7 +275,7 @@ async fn action_integrate_profile(action: ActionIntegrateProfile) -> Result<()> }); resolve_unordered_and_integrate_with_provider_init( - path_game_pak, + game_pak_path, &mut state, &mods, action.update, @@ -285,6 +285,16 @@ async fn action_integrate_profile(action: ActionIntegrateProfile) -> Result<()> } async fn action_lint(action: ActionLint) -> Result<()> { + let game_pak_path = action + .fsd_pak + .or_else(|| { + DRGInstallation::find() + .as_ref() + .map(DRGInstallation::main_pak) + }) + .context("Could not find DRG pak file, please specify manually with the --fsd_pak flag")?; + debug!(?game_pak_path); + let mut state = State::init()?; let mut mods = Vec::new(); @@ -303,8 +313,12 @@ async fn action_lint(action: ActionLint) -> Result<()> { LintId::EMPTY_ARCHIVE, LintId::OUTDATED_PAK_VERSION, LintId::SHADER_FILES, + LintId::ARCHIVE_WITH_MULTIPLE_PAKS, + LintId::NON_ASSET_FILES, + LintId::SPLIT_ASSET_PAIRS, ]), BTreeSet::from_iter(mods.into_iter().zip(mod_paths)), + Some(game_pak_path), ) }) .await??; diff --git a/src/mod_lints/archive_multiple_paks.rs b/src/mod_lints/archive_multiple_paks.rs index fe01742a..50c29137 100644 --- a/src/mod_lints/archive_multiple_paks.rs +++ b/src/mod_lints/archive_multiple_paks.rs @@ -15,7 +15,7 @@ impl Lint for ArchiveMultiplePaksLint { fn check_mods(&mut self, lcx: &LintCtxt) -> Result { let mut archive_multiple_paks_mods = BTreeSet::new(); lcx.for_each_mod( - |_, _| Ok(()), + |_, _, _| Ok(()), None::, None::, Some(|mod_spec| { diff --git a/src/mod_lints/archive_only_non_pak_files.rs b/src/mod_lints/archive_only_non_pak_files.rs index ee5e2a38..b70fd4ef 100644 --- a/src/mod_lints/archive_only_non_pak_files.rs +++ b/src/mod_lints/archive_only_non_pak_files.rs @@ -13,7 +13,7 @@ impl Lint for ArchiveOnlyNonPakFilesLint { fn check_mods(&mut self, lcx: &LintCtxt) -> anyhow::Result { let mut archive_only_non_pak_files_mods = BTreeSet::new(); lcx.for_each_mod( - |_, _| Ok(()), + |_, _, _| Ok(()), None::, Some(|mod_spec| { archive_only_non_pak_files_mods.insert(mod_spec); diff --git a/src/mod_lints/asset_register_bin.rs b/src/mod_lints/asset_register_bin.rs index db7e0a05..9a5a99a0 100644 --- a/src/mod_lints/asset_register_bin.rs +++ b/src/mod_lints/asset_register_bin.rs @@ -14,7 +14,7 @@ impl Lint for AssetRegisterBinLint { fn check_mods(&mut self, lcx: &LintCtxt) -> Result { let mut asset_register_bin_mods = BTreeMap::new(); - lcx.for_each_mod_file(|mod_spec, _, raw_path, normalized_path| { + lcx.for_each_mod_file(|mod_spec, _, _, raw_path, normalized_path| { if let Some(filename) = raw_path.file_name() { if filename == "AssetRegistry.bin" { asset_register_bin_mods diff --git a/src/mod_lints/conflicting_mods.rs b/src/mod_lints/conflicting_mods.rs index 29f30a4a..d3c6f0ad 100644 --- a/src/mod_lints/conflicting_mods.rs +++ b/src/mod_lints/conflicting_mods.rs @@ -18,7 +18,7 @@ impl Lint for ConflictingModsLint { fn check_mods(&mut self, lcx: &LintCtxt) -> Result { let mut per_path_modifiers = BTreeMap::new(); - lcx.for_each_mod_file(|mod_spec, _, _, normalized_path| { + lcx.for_each_mod_file(|mod_spec, _, _, _, normalized_path| { per_path_modifiers .entry(normalized_path) .and_modify(|modifiers: &mut IndexSet| { diff --git a/src/mod_lints/empty_archive.rs b/src/mod_lints/empty_archive.rs index 246a5b15..99609afc 100644 --- a/src/mod_lints/empty_archive.rs +++ b/src/mod_lints/empty_archive.rs @@ -16,7 +16,7 @@ impl Lint for EmptyArchiveLint { let mut empty_archive_mods = BTreeSet::new(); lcx.for_each_mod( - |_, _| Ok(()), + |_, _, _| Ok(()), Some(|mod_spec| { empty_archive_mods.insert(mod_spec); }), diff --git a/src/mod_lints/mod.rs b/src/mod_lints/mod.rs index c38e4c29..e5a334e6 100644 --- a/src/mod_lints/mod.rs +++ b/src/mod_lints/mod.rs @@ -7,6 +7,7 @@ mod non_asset_files; mod outdated_pak_version; mod shader_files; mod split_asset_pairs; +mod unmodified_game_assets; use std::collections::{BTreeMap, BTreeSet}; use std::io::BufReader; @@ -18,7 +19,7 @@ use repak::PakReader; use tracing::trace; use crate::mod_lints::conflicting_mods::ConflictingModsLint; -use crate::providers::ModSpecification; +use crate::providers::{ModSpecification, ReadSeek}; use crate::{lint_get_all_files_from_data, open_file, GetAllFilesFromDataError, PakOrNotPak}; use self::archive_multiple_paks::ArchiveMultiplePaksLint; @@ -30,15 +31,20 @@ use self::outdated_pak_version::OutdatedPakVersionLint; use self::shader_files::ShaderFilesLint; pub use self::split_asset_pairs::SplitAssetPair; use self::split_asset_pairs::SplitAssetPairsLint; +use self::unmodified_game_assets::UnmodifiedGameAssetsLint; pub struct LintCtxt { - mods: BTreeSet<(ModSpecification, PathBuf)>, + pub(crate) mods: BTreeSet<(ModSpecification, PathBuf)>, + pub(crate) fsd_pak_path: Option, } impl LintCtxt { - pub fn init(mods: BTreeSet<(ModSpecification, PathBuf)>) -> Result { + pub fn init( + mods: BTreeSet<(ModSpecification, PathBuf)>, + fsd_pak_path: Option, + ) -> Result { trace!("LintCtxt::init"); - Ok(Self { mods }) + Ok(Self { mods, fsd_pak_path }) } pub fn for_each_mod( @@ -49,7 +55,7 @@ impl LintCtxt { mut multiple_pak_files_handler: Option, ) -> Result<()> where - F: FnMut(ModSpecification, &PakReader) -> Result<()>, + F: FnMut(ModSpecification, &mut Box, &PakReader) -> Result<()>, EmptyArchiveHandler: FnMut(ModSpecification), OnlyNonPakFilesHandler: FnMut(ModSpecification), MultiplePakFilesHandler: FnMut(ModSpecification), @@ -89,9 +95,9 @@ impl LintCtxt { } } - let mut first_pak_reader = individual_pak_readers.remove(0); - let pak_reader = repak::PakReader::new_any(&mut first_pak_reader, None)?; - f(mod_spec.clone(), &pak_reader)? + let mut first_pak_read_seek = individual_pak_readers.remove(0); + let pak_reader = repak::PakReader::new_any(&mut first_pak_read_seek, None)?; + f(mod_spec.clone(), &mut first_pak_read_seek, &pak_reader)? } Ok(()) @@ -99,10 +105,16 @@ impl LintCtxt { pub fn for_each_mod_file(&self, mut f: F) -> Result<()> where - F: FnMut(ModSpecification, &PakReader, PathBuf, String) -> Result<()>, + F: FnMut( + ModSpecification, + &mut Box, + &PakReader, + PathBuf, + String, + ) -> Result<()>, { self.for_each_mod( - |mod_spec, pak_reader| { + |mod_spec, pak_read_seek, pak_reader| { let mount = PathBuf::from(pak_reader.mount_point()); for p in pak_reader.files() { let path = mount.join(&p); @@ -113,6 +125,7 @@ impl LintCtxt { let normalized_path = normalized_path.to_ascii_lowercase(); f( mod_spec.clone(), + pak_read_seek, pak_reader, path_buf.to_path_buf(), normalized_path, @@ -171,6 +184,9 @@ impl LintId { pub const SPLIT_ASSET_PAIRS: Self = LintId { name: "split_asset_pairs", }; + pub const UNMODIFIED_GAME_ASSETS: Self = LintId { + name: "unmodified_game_assets", + }; } #[derive(Default, Debug)] @@ -185,13 +201,15 @@ pub struct LintReport { pub non_asset_file_mods: Option>>, pub split_asset_pairs_mods: Option>>, + pub unmodified_game_assets_mods: Option>>, } pub fn run_lints( enabled_lints: &BTreeSet, mods: BTreeSet<(ModSpecification, PathBuf)>, + fsd_pak_path: Option, ) -> Result { - let lint_ctxt = LintCtxt::init(mods)?; + let lint_ctxt = LintCtxt::init(mods, fsd_pak_path)?; let mut lint_report = LintReport::default(); for lint_id in enabled_lints { @@ -232,6 +250,10 @@ pub fn run_lints( let res = SplitAssetPairsLint.check_mods(&lint_ctxt)?; lint_report.split_asset_pairs_mods = Some(res); } + LintId::UNMODIFIED_GAME_ASSETS => { + let res = UnmodifiedGameAssetsLint.check_mods(&lint_ctxt)?; + lint_report.unmodified_game_assets_mods = Some(res); + } _ => unimplemented!(), } } diff --git a/src/mod_lints/non_asset_files.rs b/src/mod_lints/non_asset_files.rs index 9120860f..8f2e3c8e 100644 --- a/src/mod_lints/non_asset_files.rs +++ b/src/mod_lints/non_asset_files.rs @@ -25,7 +25,7 @@ impl Lint for NonAssetFilesLint { fn check_mods(&mut self, lcx: &LintCtxt) -> Result { let mut non_asset_files = BTreeMap::new(); - lcx.for_each_mod_file(|mod_spec, _, _, normalized_path| { + lcx.for_each_mod_file(|mod_spec, _, _, _, normalized_path| { let is_unreal_asset = ENDS_WITH_WHITE_LIST .iter() .any(|end| normalized_path.ends_with(end)); diff --git a/src/mod_lints/outdated_pak_version.rs b/src/mod_lints/outdated_pak_version.rs index 2bc1ccf8..9f42fc21 100644 --- a/src/mod_lints/outdated_pak_version.rs +++ b/src/mod_lints/outdated_pak_version.rs @@ -16,7 +16,7 @@ impl Lint for OutdatedPakVersionLint { let mut outdated_pak_version_mods = BTreeMap::new(); lcx.for_each_mod( - |mod_spec, pak_reader| { + |mod_spec, _, pak_reader| { if pak_reader.version() < repak::Version::V11 { outdated_pak_version_mods.insert(mod_spec.clone(), pak_reader.version()); } diff --git a/src/mod_lints/shader_files.rs b/src/mod_lints/shader_files.rs index 9d205cb1..efaa82a4 100644 --- a/src/mod_lints/shader_files.rs +++ b/src/mod_lints/shader_files.rs @@ -15,7 +15,7 @@ impl Lint for ShaderFilesLint { fn check_mods(&mut self, lcx: &LintCtxt) -> Result { let mut shader_file_mods = BTreeMap::new(); - lcx.for_each_mod_file(|mod_spec, _, raw_path, normalized_path| { + lcx.for_each_mod_file(|mod_spec, _, _, raw_path, normalized_path| { if raw_path.extension().and_then(std::ffi::OsStr::to_str) == Some("ushaderbytecode") { shader_file_mods .entry(mod_spec) diff --git a/src/mod_lints/split_asset_pairs.rs b/src/mod_lints/split_asset_pairs.rs index 7de80126..8c9e8667 100644 --- a/src/mod_lints/split_asset_pairs.rs +++ b/src/mod_lints/split_asset_pairs.rs @@ -21,7 +21,7 @@ impl Lint for SplitAssetPairsLint { fn check_mods(&mut self, lcx: &LintCtxt) -> anyhow::Result { let mut per_mod_path_without_final_ext_to_exts_map = BTreeMap::new(); - lcx.for_each_mod_file(|mod_spec, _, _, normalized_path| { + lcx.for_each_mod_file(|mod_spec, _, _, _, normalized_path| { let mut iter = normalized_path.rsplit('.').take(2); let Some(final_ext) = iter.next() else { return Ok(()); diff --git a/src/mod_lints/unmodified_game_assets.rs b/src/mod_lints/unmodified_game_assets.rs new file mode 100644 index 00000000..bcfc9bc7 --- /dev/null +++ b/src/mod_lints/unmodified_game_assets.rs @@ -0,0 +1,99 @@ +use std::borrow::Cow; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; + +use anyhow::{bail, Result}; +use path_slash::PathExt; +use rayon::prelude::*; +use sha2::Digest; +use tracing::trace; + +use crate::open_file; +use crate::providers::ModSpecification; + +use super::{Lint, LintCtxt}; + +#[derive(Default)] +pub struct UnmodifiedGameAssetsLint; + +impl Lint for UnmodifiedGameAssetsLint { + type Output = BTreeMap>; + + fn check_mods(&mut self, lcx: &LintCtxt) -> Result { + let Some(game_pak_path) = &lcx.fsd_pak_path else { + bail!("UnmodifiedGameAssetsLint requires specifying a valid game pak path"); + }; + + // Adapted from + // . + let mut reader = BufReader::new(open_file(game_pak_path)?); + let pak = repak::PakReader::new_any(&mut reader, None)?; + + let mount_point = PathBuf::from(pak.mount_point()); + + let full_paths = pak + .files() + .into_iter() + .map(|f| (mount_point.join(&f), f)) + .collect::>(); + let stripped = full_paths + .iter() + .map(|(full_path, _path)| full_path.strip_prefix("../../../")) + .collect::, _>>()?; + + let game_file_hashes: std::sync::Arc< + std::sync::Mutex, Vec>>, + > = Default::default(); + + full_paths.par_iter().zip(stripped).try_for_each_init( + || (game_file_hashes.clone(), File::open(game_pak_path)), + |(hashes, file), ((_full_path, path), stripped)| -> Result<(), repak::Error> { + let mut hasher = sha2::Sha256::new(); + pak.read_file( + path, + &mut BufReader::new(file.as_ref().unwrap()), + &mut hasher, + )?; + let hash = hasher.finalize(); + hashes + .lock() + .unwrap() + .insert(stripped.to_slash_lossy(), hash.to_vec()); + Ok(()) + }, + )?; + + let mut unmodified_game_assets = BTreeMap::new(); + + lcx.for_each_mod_file( + |mod_spec, mut pak_read_seek, pak_reader, _, normalized_path| { + if let Some(reference_hash) = game_file_hashes + .lock() + .unwrap() + .get(&Cow::Owned(normalized_path.clone())) + { + let mut hasher = sha2::Sha256::new(); + pak_reader.read_file(&normalized_path, &mut pak_read_seek, &mut hasher)?; + let mod_file_hash = hasher.finalize().to_vec(); + + if &mod_file_hash == reference_hash { + unmodified_game_assets + .entry(mod_spec) + .and_modify(|paths: &mut BTreeSet| { + paths.insert(normalized_path.clone()); + }) + .or_insert_with(|| [normalized_path].into()); + } + } + + Ok(()) + }, + )?; + + trace!("unmodified_game_assets:\n{:#?}", unmodified_game_assets); + + Ok(unmodified_game_assets) + } +} diff --git a/test_assets/lints/reference.pak b/test_assets/lints/reference.pak new file mode 100644 index 00000000..438d4bdb Binary files /dev/null and b/test_assets/lints/reference.pak differ diff --git a/test_assets/lints/reference/a.uasset b/test_assets/lints/reference/a.uasset new file mode 100644 index 00000000..78981922 --- /dev/null +++ b/test_assets/lints/reference/a.uasset @@ -0,0 +1 @@ +a diff --git a/test_assets/lints/reference/a.uexp b/test_assets/lints/reference/a.uexp new file mode 100644 index 00000000..78981922 --- /dev/null +++ b/test_assets/lints/reference/a.uexp @@ -0,0 +1 @@ +a diff --git a/test_assets/lints/unmodified_game_assets.pak b/test_assets/lints/unmodified_game_assets.pak new file mode 100644 index 00000000..438d4bdb Binary files /dev/null and b/test_assets/lints/unmodified_game_assets.pak differ diff --git a/test_assets/lints/unmodified_game_assets/a.uasset b/test_assets/lints/unmodified_game_assets/a.uasset new file mode 100644 index 00000000..78981922 --- /dev/null +++ b/test_assets/lints/unmodified_game_assets/a.uasset @@ -0,0 +1 @@ +a diff --git a/test_assets/lints/unmodified_game_assets/a.uexp b/test_assets/lints/unmodified_game_assets/a.uexp new file mode 100644 index 00000000..78981922 --- /dev/null +++ b/test_assets/lints/unmodified_game_assets/a.uexp @@ -0,0 +1 @@ +a diff --git a/tests/lint/mod.rs b/tests/lint/mod.rs index b26777dc..f40b2300 100644 --- a/tests/lint/mod.rs +++ b/tests/lint/mod.rs @@ -23,7 +23,7 @@ pub fn test_lint_conflicting_files() { let LintReport { conflicting_mods, .. - } = drg_mod_integration::mod_lints::run_lints(&[LintId::CONFLICTING].into(), mods.into()) + } = drg_mod_integration::mod_lints::run_lints(&[LintId::CONFLICTING].into(), mods.into(), None) .unwrap(); println!("{:#?}", conflicting_mods); @@ -52,8 +52,12 @@ pub fn test_lint_shader() { let LintReport { shader_file_mods, .. - } = drg_mod_integration::mod_lints::run_lints(&[LintId::SHADER_FILES].into(), mods.into()) - .unwrap(); + } = drg_mod_integration::mod_lints::run_lints( + &[LintId::SHADER_FILES].into(), + mods.into(), + None, + ) + .unwrap(); println!("{:#?}", shader_file_mods); @@ -85,6 +89,7 @@ pub fn test_lint_asset_registry_bin() { } = drg_mod_integration::mod_lints::run_lints( &[LintId::ASSET_REGISTRY_BIN].into(), mods.into(), + None, ) .unwrap(); @@ -113,6 +118,7 @@ pub fn test_lint_outdated_pak_version() { } = drg_mod_integration::mod_lints::run_lints( &[LintId::OUTDATED_PAK_VERSION].into(), mods.into(), + None, ) .unwrap(); @@ -137,8 +143,12 @@ pub fn test_lint_empty_archive() { let LintReport { empty_archive_mods, .. - } = drg_mod_integration::mod_lints::run_lints(&[LintId::EMPTY_ARCHIVE].into(), mods.into()) - .unwrap(); + } = drg_mod_integration::mod_lints::run_lints( + &[LintId::EMPTY_ARCHIVE].into(), + mods.into(), + None, + ) + .unwrap(); println!("{:#?}", empty_archive_mods); @@ -170,6 +180,7 @@ pub fn test_lint_only_non_pak_files() { } = drg_mod_integration::mod_lints::run_lints( &[LintId::ARCHIVE_WITH_ONLY_NON_PAK_FILES].into(), mods.into(), + None, ) .unwrap(); @@ -197,6 +208,7 @@ pub fn test_lint_multi_pak_archive() { } = drg_mod_integration::mod_lints::run_lints( &[LintId::ARCHIVE_WITH_MULTIPLE_PAKS].into(), mods.into(), + None, ) .unwrap(); @@ -221,8 +233,12 @@ pub fn test_lint_non_asset_files() { let LintReport { non_asset_file_mods, .. - } = drg_mod_integration::mod_lints::run_lints(&[LintId::NON_ASSET_FILES].into(), mods.into()) - .unwrap(); + } = drg_mod_integration::mod_lints::run_lints( + &[LintId::NON_ASSET_FILES].into(), + mods.into(), + None, + ) + .unwrap(); println!("{:#?}", non_asset_file_mods); @@ -239,15 +255,19 @@ pub fn test_lint_split_asset_pairs() { let split_asset_pairs_path = base_path.clone().join("split_asset_pairs.pak"); assert!(split_asset_pairs_path.exists()); let split_asset_pairs_spec = ModSpecification { - url: "mismatched_asset_pairs".to_string(), + url: "split_asset_pairs".to_string(), }; let mods = [(split_asset_pairs_spec.clone(), split_asset_pairs_path)]; let LintReport { split_asset_pairs_mods, .. - } = drg_mod_integration::mod_lints::run_lints(&[LintId::SPLIT_ASSET_PAIRS].into(), mods.into()) - .unwrap(); + } = drg_mod_integration::mod_lints::run_lints( + &[LintId::SPLIT_ASSET_PAIRS].into(), + mods.into(), + None, + ) + .unwrap(); println!("{:#?}", split_asset_pairs_mods); @@ -268,3 +288,39 @@ pub fn test_lint_split_asset_pairs() { ) ); } + +#[test] +pub fn test_lint_unmodified_game_assets() { + let base_path = PathBuf::from_str("test_assets/lints/").unwrap(); + assert!(base_path.exists()); + let reference_pak_path = base_path.clone().join("reference.pak"); + assert!(reference_pak_path.exists()); + let unmodified_game_assets_path = base_path.clone().join("unmodified_game_assets.pak"); + assert!(unmodified_game_assets_path.exists()); + let unmodified_game_assets_spec = ModSpecification { + url: "unmodified_game_assets".to_string(), + }; + let mods = [( + unmodified_game_assets_spec.clone(), + unmodified_game_assets_path, + )]; + + let LintReport { + unmodified_game_assets_mods, + .. + } = drg_mod_integration::mod_lints::run_lints( + &[LintId::UNMODIFIED_GAME_ASSETS].into(), + mods.into(), + Some(reference_pak_path), + ) + .unwrap(); + + println!("{:#?}", unmodified_game_assets_mods); + + assert_eq!( + unmodified_game_assets_mods + .unwrap() + .get(&unmodified_game_assets_spec), + Some(&["a.uexp".to_string(), "a.uasset".to_string()].into()) + ); +}