Skip to main content

modde_core/installer/
probe.rs

1//! Game-specific install detection hooks.
2//!
3//! `modde-core` cannot reference `modde-games` (would create a dependency
4//! cycle), so the analyzer takes an [`InstallProbe`] that the caller
5//! constructs from whatever game plugin context it has. `modde-games`
6//! provides a `game_probe(plugin)` helper that wraps a
7//! `&'static dyn GamePlugin` into a probe.
8//!
9//! The probe owns its closures (as `Box<dyn Fn>`), so analysis does not
10//! leak memory and the probe can be passed across tasks.
11
12use std::path::Path;
13
14use super::types::InstallMethod;
15
16/// Callbacks the analyzer uses to delegate to a game plugin.
17///
18/// Both hooks have sensible defaults ([`InstallProbe::noop`]), so a game
19/// plugin with no special layouts can just leave them unimplemented.
20pub struct InstallProbe {
21    /// Return a game-specific [`InstallMethod`] if the plugin recognizes
22    /// the extracted archive authoritatively (e.g. Cyberpunk identifying
23    /// a REDmod by `info.json` + `archives/` presence). Runs **before**
24    /// the generic probes so it can claim layouts that also happen to
25    /// trigger generic heuristics.
26    pub analyze: Box<dyn Fn(&Path) -> Option<InstallMethod> + Send + Sync>,
27
28    /// Return `true` if the extracted archive looks like a bare-extract
29    /// for this game (e.g. top-level `Data/` for Bethesda). Runs as the
30    /// last fallback before [`InstallMethod::Unknown`].
31    pub recognizes_bare: Box<dyn Fn(&Path) -> bool + Send + Sync>,
32}
33
34impl InstallProbe {
35    /// Construct a probe from two closures.
36    pub fn new<A, B>(analyze: A, recognizes_bare: B) -> Self
37    where
38        A: Fn(&Path) -> Option<InstallMethod> + Send + Sync + 'static,
39        B: Fn(&Path) -> bool + Send + Sync + 'static,
40    {
41        Self {
42            analyze: Box::new(analyze),
43            recognizes_bare: Box::new(recognizes_bare),
44        }
45    }
46
47    /// A probe that never claims anything game-specific. Used by tests of
48    /// the generic detection pipeline and as a "no plugin available"
49    /// fallback.
50    pub fn noop() -> Self {
51        Self {
52            analyze: Box::new(|_| None),
53            recognizes_bare: Box::new(|_| false),
54        }
55    }
56}