use std::path::Path;
use anyhow::{Context, Result};
use super::plugins_txt;
use crate::scanner_patterns::SingleFileModRule;
use crate::traits::{DiscoveredFile, DiscoveredMod, ModScanner, ModSource, ScanContext};
pub struct BethesdaScanner {
pub game_id: &'static str,
pub steam_app_id: u32,
pub game_folder_name: &'static str,
}
pub static SKYRIM_SCANNER: BethesdaScanner = BethesdaScanner {
game_id: "skyrim-se",
steam_app_id: plugins_txt::SKYRIM_SE_APP_ID,
game_folder_name: "Skyrim Special Edition",
};
pub static FALLOUT4_SCANNER: BethesdaScanner = BethesdaScanner {
game_id: "fallout4",
steam_app_id: plugins_txt::FALLOUT4_APP_ID,
game_folder_name: "Fallout4",
};
pub static FALLOUT76_SCANNER: BethesdaArchiveScanner = BethesdaArchiveScanner {
game_id: "fallout76",
};
pub static STARFIELD_SCANNER: BethesdaScanner = BethesdaScanner {
game_id: "starfield",
steam_app_id: plugins_txt::STARFIELD_APP_ID,
game_folder_name: "Starfield",
};
const BETHESDA_SCAN_DIRS: &[&str] = &["Data"];
const PLUGIN_EXTENSIONS: &[&str] = &["esp", "esm", "esl"];
const ARCHIVE_EXTENSIONS: &[&str] = &["bsa", "ba2"];
pub struct BethesdaArchiveScanner {
pub game_id: &'static str,
}
impl ModScanner for BethesdaArchiveScanner {
fn scan_directories(&self) -> &[&str] {
BETHESDA_SCAN_DIRS
}
fn scan_filesystem(&self, ctx: &ScanContext<'_>) -> Result<Vec<DiscoveredMod>> {
let mut mods = Vec::new();
SingleFileModRule {
rel_dir: "Data",
extension: "ba2",
ignored_prefixes: &["SeventySix"],
mod_id_prefix: "archive",
source_location: "Data",
confidence: 0.8,
}
.scan(ctx.install_dir, &mut mods)?;
Ok(mods)
}
fn mod_id_footprint(&self, mod_id: &str) -> Option<modde_core::scanner::ModFootprint> {
let stem = mod_id.strip_prefix("archive/")?.to_lowercase();
Some(modde_core::scanner::ModFootprint::File(format!(
"data/{stem}.ba2"
)))
}
}
impl ModScanner for BethesdaScanner {
fn scan_directories(&self) -> &[&str] {
BETHESDA_SCAN_DIRS
}
fn scan_filesystem(&self, ctx: &ScanContext<'_>) -> Result<Vec<DiscoveredMod>> {
let data_dir = ctx.install_dir.join("Data");
if !data_dir.is_dir() {
return Ok(Vec::new());
}
let mut mods = Vec::new();
let known_plugins = plugins_txt::read_plugins_txt(self.steam_app_id, self.game_folder_name)
.unwrap_or_default();
let mut seen_stems: std::collections::HashSet<String> = std::collections::HashSet::new();
for plugin_entry in &known_plugins {
let plugin_path = data_dir.join(&plugin_entry.name);
if !plugin_path.exists() {
continue;
}
let stem = std::path::Path::new(&plugin_entry.name)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(&plugin_entry.name)
.to_string();
seen_stems.insert(stem.to_lowercase());
let mut files = vec![make_data_file(ctx.install_dir, &plugin_path)];
for ext in ARCHIVE_EXTENSIONS {
let archive_path = data_dir.join(format!("{stem}.{ext}"));
if archive_path.exists() {
files.push(make_data_file(ctx.install_dir, &archive_path));
}
let tex_path = data_dir.join(format!("{stem} - Textures.{ext}"));
if tex_path.exists() {
files.push(make_data_file(ctx.install_dir, &tex_path));
}
}
mods.push(DiscoveredMod {
mod_id: format!("plugin/{}", plugin_entry.name),
display_name: plugin_entry.name.clone(),
version: None,
files,
source: ModSource::Filesystem {
location: "Data".into(),
},
confidence: 0.95,
});
}
for entry in std::fs::read_dir(&data_dir)
.with_context(|| format!("failed to read directory: {}", data_dir.display()))?
.flatten()
{
let path = entry.path();
if path.is_dir() {
continue;
}
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if !PLUGIN_EXTENSIONS.contains(&ext.as_str()) {
continue;
}
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if seen_stems.contains(&stem.to_lowercase()) {
continue;
}
let mut files = vec![make_data_file(ctx.install_dir, &path)];
for archive_ext in ARCHIVE_EXTENSIONS {
let archive_path = data_dir.join(format!("{stem}.{archive_ext}"));
if archive_path.exists() {
files.push(make_data_file(ctx.install_dir, &archive_path));
}
}
mods.push(DiscoveredMod {
mod_id: format!("plugin/{stem}.{ext}"),
display_name: format!("{stem}.{ext}"),
version: None,
files,
source: ModSource::Filesystem {
location: "Data".into(),
},
confidence: 0.8, });
}
Ok(mods)
}
fn mod_id_footprint(&self, mod_id: &str) -> Option<modde_core::scanner::ModFootprint> {
let filename = mod_id.strip_prefix("plugin/")?.to_lowercase();
Some(modde_core::scanner::ModFootprint::File(filename))
}
}
fn make_data_file(install_root: &Path, file_path: &Path) -> DiscoveredFile {
let rel = file_path
.strip_prefix(install_root)
.unwrap_or(file_path)
.to_string_lossy()
.replace('\\', "/");
let size = file_path.metadata().map_or(0, |m| m.len());
DiscoveredFile {
rel_path: rel,
size,
}
}