modde_games/bannerlord/
scanner.rs1use anyhow::{Context, Result};
4
5use crate::traits::{DiscoveredMod, ModScanner, ModSource, ScanContext, walk_files_relative};
6
7pub struct BannerlordScanner;
9
10pub static BANNERLORD_SCANNER: BannerlordScanner = BannerlordScanner;
11
12impl ModScanner for BannerlordScanner {
13 fn scan_directories(&self) -> &[&str] {
14 &["Modules"]
15 }
16
17 fn scan_filesystem(&self, ctx: &ScanContext<'_>) -> Result<Vec<DiscoveredMod>> {
18 let modules_dir = ctx.install_dir.join("Modules");
19 if !modules_dir.is_dir() {
20 return Ok(Vec::new());
21 }
22
23 let mut out = Vec::new();
24 for entry in std::fs::read_dir(&modules_dir)
25 .with_context(|| format!("failed to read directory: {}", modules_dir.display()))?
26 .flatten()
27 {
28 if !entry.file_type().is_ok_and(|ty| ty.is_dir()) {
29 continue;
30 }
31 let module_dir = entry.path();
32 let submodule = module_dir.join("SubModule.xml");
33 if !submodule.is_file() {
34 continue;
35 }
36 let info = super::parse_submodule_xml(&submodule).ok();
37 let fallback = entry.file_name().to_string_lossy().to_string();
38 let id = info
39 .as_ref()
40 .map_or(fallback.clone(), |info| info.id.clone());
41 let display_name = info.map_or(fallback, |info| info.name);
42 let files = walk_files_relative(ctx.install_dir, &module_dir);
43 if files.is_empty() {
44 continue;
45 }
46 out.push(DiscoveredMod {
47 mod_id: format!("module/{id}"),
48 display_name,
49 version: None,
50 files,
51 source: ModSource::Filesystem {
52 location: "Modules".into(),
53 },
54 confidence: 0.95,
55 });
56 }
57
58 Ok(out)
59 }
60
61 fn mod_id_footprint(&self, mod_id: &str) -> Option<modde_core::scanner::ModFootprint> {
62 let name = mod_id.strip_prefix("module/")?.to_lowercase();
63 Some(modde_core::scanner::ModFootprint::Directory(format!(
64 "modules/{name}/"
65 )))
66 }
67}