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}