Skip to main content

modde_games/ue4/
scanner.rs

1//! Mod scanner for Unreal Engine 4 pak-based games.
2
3use anyhow::Result;
4
5use crate::scanner_patterns::FileGroupRule;
6use crate::traits::{DiscoveredMod, ModScanner, ScanContext};
7
8/// Data-driven scanner for UE4 pak-based mods.
9///
10/// Walks `<ProjectName>/Content/Paks/~mods` and `.../LogicMods`, grouping
11/// `.pak` / `.ucas` / `.utoc` triples by file stem. Each stem becomes one
12/// [`DiscoveredMod`] with `mod_id = "pak/<stem>"`.
13pub struct Ue4Scanner {
14    pub game_id: &'static str,
15    pub project_name: &'static str,
16}
17
18pub static STELLAR_BLADE_SCANNER: Ue4Scanner = Ue4Scanner {
19    game_id: "stellar-blade",
20    project_name: "SB",
21};
22
23pub static SUBNAUTICA2_SCANNER: Ue4Scanner = Ue4Scanner {
24    game_id: "subnautica2",
25    project_name: "Subnautica2",
26};
27
28const UE4_GROUP_EXTENSIONS: &[&str] = &["pak", "ucas", "utoc"];
29
30impl Ue4Scanner {
31    fn scan_subdir(
32        &self,
33        install: &std::path::Path,
34        subdir: &str,
35        location: &'static str,
36        out: &mut Vec<DiscoveredMod>,
37    ) {
38        let dir = install
39            .join(self.project_name)
40            .join("Content")
41            .join("Paks")
42            .join(subdir);
43        FileGroupRule {
44            rel_dir: "",
45            extensions: UE4_GROUP_EXTENSIONS,
46            mod_id_prefix: "pak",
47            source_location: location,
48            confidence: 0.9,
49        }
50        .scan_dir(install, &dir, out);
51    }
52}
53
54impl ModScanner for Ue4Scanner {
55    fn scan_directories(&self) -> &[&str] {
56        // Used by cache invalidation. Exact subpaths are composed at scan
57        // time because they depend on `project_name`.
58        &["Content/Paks/~mods", "Content/Paks/LogicMods"]
59    }
60
61    fn scan_filesystem(&self, ctx: &ScanContext<'_>) -> Result<Vec<DiscoveredMod>> {
62        let mut out = Vec::new();
63        self.scan_subdir(ctx.install_dir, "~mods", "paks-mods", &mut out);
64        self.scan_subdir(ctx.install_dir, "LogicMods", "logic-mods", &mut out);
65        Ok(out)
66    }
67
68    fn mod_id_footprint(&self, mod_id: &str) -> Option<modde_core::scanner::ModFootprint> {
69        // Represent the mod by its `.pak` file; ucas/utoc siblings collapse
70        // into the same row (same convention as Cyberpunk's `archive/` branch).
71        let stem = mod_id.strip_prefix("pak/")?.to_lowercase();
72        Some(modde_core::scanner::ModFootprint::File(format!(
73            "{}/content/paks/~mods/{stem}.pak",
74            self.project_name.to_lowercase()
75        )))
76    }
77}