use std::collections::BTreeMap;
use std::path::Path;
use anyhow::Result;
use crate::traits::{
walk_files_relative, DiscoveredFile, DiscoveredMod, ModScanner, ModSource, ScanContext,
};
pub struct Ue4Scanner {
pub game_id: &'static str,
pub project_name: &'static str,
}
pub static STELLAR_BLADE_SCANNER: Ue4Scanner = Ue4Scanner {
game_id: "stellar-blade",
project_name: "SB",
};
fn stem_for(rel: &str) -> Option<String> {
let lower = rel.to_lowercase();
if !(lower.ends_with(".pak") || lower.ends_with(".ucas") || lower.ends_with(".utoc")) {
return None;
}
let path = std::path::Path::new(rel);
path.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
}
impl Ue4Scanner {
fn scan_subdir(
&self,
install: &Path,
subdir: &str,
location: &str,
out: &mut Vec<DiscoveredMod>,
) {
let dir = install
.join(self.project_name)
.join("Content")
.join("Paks")
.join(subdir);
if !dir.is_dir() {
return;
}
let mut by_stem: BTreeMap<String, Vec<DiscoveredFile>> = BTreeMap::new();
let Ok(entries) = std::fs::read_dir(&dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
for f in walk_files_relative(install, &path) {
if let Some(stem) = stem_for(&f.rel_path) {
by_stem.entry(stem).or_default().push(f);
}
}
continue;
}
let Ok(meta) = path.metadata() else {
continue;
};
let Ok(rel) = path.strip_prefix(install) else {
continue;
};
let rel_str = rel.to_string_lossy().to_string();
let Some(stem) = stem_for(&rel_str) else {
continue;
};
by_stem.entry(stem).or_default().push(DiscoveredFile {
rel_path: rel_str,
size: meta.len(),
});
}
for (stem, files) in by_stem {
out.push(DiscoveredMod {
mod_id: format!("pak/{stem}"),
display_name: stem,
version: None,
files,
source: ModSource::Filesystem {
location: location.into(),
},
confidence: 0.9,
});
}
}
}
impl ModScanner for Ue4Scanner {
fn scan_directories(&self) -> &[&str] {
&["Content/Paks/~mods", "Content/Paks/LogicMods"]
}
fn scan_filesystem(&self, ctx: &ScanContext<'_>) -> Result<Vec<DiscoveredMod>> {
let mut out = Vec::new();
self.scan_subdir(ctx.install_dir, "~mods", "paks-mods", &mut out);
self.scan_subdir(ctx.install_dir, "LogicMods", "logic-mods", &mut out);
Ok(out)
}
fn mod_id_footprint(&self, mod_id: &str) -> Option<modde_core::scanner::ModFootprint> {
let stem = mod_id.strip_prefix("pak/")?.to_lowercase();
Some(modde_core::scanner::ModFootprint::File(format!(
"{}/content/paks/~mods/{stem}.pak",
self.project_name.to_lowercase()
)))
}
}