Skip to main content

modde_games/gamebryo/
scanner.rs

1//! Mod scanner for Gamebryo-engine games.
2
3use std::path::Path;
4
5use anyhow::{Context, Result};
6
7use crate::traits::{DiscoveredFile, DiscoveredMod, ModScanner, ModSource, ScanContext};
8
9/// [`ModScanner`] for a specific Gamebryo-engine game.
10pub struct GamebryoScanner {
11    pub game_id: &'static str,
12}
13
14pub static FALLOUT_NEW_VEGAS_SCANNER: GamebryoScanner = GamebryoScanner {
15    game_id: "fallout-new-vegas",
16};
17
18pub static OBLIVION_SCANNER: GamebryoScanner = GamebryoScanner {
19    game_id: "oblivion",
20};
21
22const PLUGIN_EXTENSIONS: &[&str] = &["esp", "esm"];
23const ARCHIVE_EXTENSIONS: &[&str] = &["bsa"];
24
25impl ModScanner for GamebryoScanner {
26    fn scan_directories(&self) -> &[&str] {
27        &["Data"]
28    }
29
30    fn scan_filesystem(&self, ctx: &ScanContext<'_>) -> Result<Vec<DiscoveredMod>> {
31        let data_dir = ctx.install_dir.join("Data");
32        if !data_dir.is_dir() {
33            return Ok(Vec::new());
34        }
35
36        let mut mods = Vec::new();
37        for entry in std::fs::read_dir(&data_dir)
38            .with_context(|| format!("failed to read directory: {}", data_dir.display()))?
39            .flatten()
40        {
41            let path = entry.path();
42            if path.is_dir() {
43                continue;
44            }
45            let ext = path
46                .extension()
47                .and_then(|ext| ext.to_str())
48                .unwrap_or("")
49                .to_lowercase();
50            if !PLUGIN_EXTENSIONS.contains(&ext.as_str()) {
51                continue;
52            }
53
54            let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
55            let mut files = vec![make_data_file(ctx.install_dir, &path)];
56            for archive_ext in ARCHIVE_EXTENSIONS {
57                let archive_path = data_dir.join(format!("{stem}.{archive_ext}"));
58                if archive_path.exists() {
59                    files.push(make_data_file(ctx.install_dir, &archive_path));
60                }
61            }
62
63            let filename = path
64                .file_name()
65                .and_then(|name| name.to_str())
66                .unwrap_or(stem)
67                .to_string();
68            mods.push(DiscoveredMod {
69                mod_id: format!("plugin/{filename}"),
70                display_name: filename,
71                version: None,
72                files,
73                source: ModSource::Filesystem {
74                    location: "Data".into(),
75                },
76                confidence: 0.85,
77            });
78        }
79
80        Ok(mods)
81    }
82
83    fn mod_id_footprint(&self, mod_id: &str) -> Option<modde_core::scanner::ModFootprint> {
84        let filename = mod_id.strip_prefix("plugin/")?.to_lowercase();
85        Some(modde_core::scanner::ModFootprint::File(filename))
86    }
87}
88
89fn make_data_file(install_root: &Path, file_path: &Path) -> DiscoveredFile {
90    let rel = file_path
91        .strip_prefix(install_root)
92        .unwrap_or(file_path)
93        .to_string_lossy()
94        .replace('\\', "/");
95    let size = file_path.metadata().map_or(0, |m| m.len());
96    DiscoveredFile {
97        rel_path: rel,
98        size,
99    }
100}