Skip to main content

loadsmith_loader/
lib.rs

1//! Mod loader definitions and launch-argument generation for the loadsmith
2//! mod-manager library.
3//!
4//! This is an internal crate of the [`loadsmith`] workspace. Most consumers
5//! should depend on the `loadsmith` facade crate instead of using this
6//! crate directly.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use loadsmith_loader::{BepInEx, Loader};
12//!
13//! let loader = BepInEx::with_default_rules();
14//! assert_eq!(loader.id(), "BepInEx");
15//! ```
16
17use std::{fmt::Debug, path::PathBuf, sync::LazyLock};
18
19use camino::Utf8PathBuf;
20use globset::{Glob, GlobBuilder, GlobSet};
21use loadsmith_core::PackageRef;
22use loadsmith_install::InstallRuleset;
23
24mod args;
25mod context;
26pub mod doorstop;
27mod error;
28mod loaders;
29
30pub use args::LaunchArgs;
31pub use context::LaunchContext;
32pub use error::{Error, Result};
33pub use loaders::*;
34
35/// A mod loader definition that knows how to install packages and generate
36/// launch arguments for a target game.
37///
38/// Each loader implementation describes:
39///
40/// * How its own loader-pack files are placed (`loader_install_rules`).
41/// * How mod packages are placed (`package_install_rules`).
42/// * What CLI arguments / environment variables are needed at launch
43///   (`generate_launch_args`).
44///
45/// By default [`prepare_launch`](Loader::prepare_launch) copies all top-level
46/// `*.dll` files from the profile into the game directory.
47///
48/// # Examples
49///
50/// ```rust
51/// use loadsmith_loader::{BepInEx, Loader};
52///
53/// let loader = BepInEx::with_default_rules();
54/// assert_eq!(loader.id(), "BepInEx");
55/// ```
56pub trait Loader: Debug + Send + Sync {
57    /// A unique, human-readable identifier for this loader (e.g. `"BepInEx"`).
58    fn id(&self) -> &'static str;
59
60    /// Returns the install rules for the loader's own files (the "loader pack").
61    fn package_install_rules(&self) -> InstallRuleset<'_>;
62    /// Returns the install rules for end-user mod packages.
63    fn loader_install_rules(&self) -> InstallRuleset<'_>;
64
65    /// Prepares the game directory by copying files from the profile.
66    ///
67    /// The default implementation copies every top-level `*.dll` file.
68    fn prepare_launch(&self, ctx: &LaunchContext) -> Result<()> {
69        static GLOB_SET: LazyLock<GlobSet> = LazyLock::new(|| {
70            GlobSet::builder()
71                .add(top_level_dll_glob())
72                .build()
73                .expect("constant globs should be valid")
74        });
75
76        ctx.copy_glob_to_game(&GLOB_SET)
77    }
78
79    /// Builds the command-line arguments and environment variables needed to
80    /// launch the game with this loader.
81    fn generate_launch_args(&self, ctx: &LaunchContext) -> Result<LaunchArgs>;
82
83    /// Returns the directory where a package's files are placed, if any.
84    fn package_dir(&self, package: &PackageRef) -> Option<PathBuf> {
85        self.package_install_rules()
86            .default_rule()
87            .and_then(|default_rule| default_rule.map_file("", package))
88            .map(Utf8PathBuf::into_std_path_buf)
89    }
90
91    /// Directories that contain mutable per-package configuration.
92    fn package_config_dirs(&self) -> Vec<PathBuf> {
93        Vec::new()
94    }
95    /// Path to the loader's log file, relative to the game directory.
96    fn log_file(&self) -> Option<PathBuf> {
97        None
98    }
99    /// Filename of the proxy DLL used by this loader, if any.
100    fn proxy_dll(&self) -> Option<PathBuf> {
101        None
102    }
103}
104
105fn top_level_dll_glob() -> Glob {
106    GlobBuilder::new("*.dll")
107        .literal_separator(true)
108        .build()
109        .expect("constant glob should be valid")
110}
111
112#[cfg(test)]
113mod test_util {
114    use camino::{Utf8Path, Utf8PathBuf};
115    use loadsmith_core::PackageRef;
116    use loadsmith_install::InstallRuleset;
117
118    use crate::Loader;
119
120    #[macro_export]
121    macro_rules! assert_map {
122        ($tester:expr, $file:literal, $expected:literal) => {
123            assert_eq!(
124                $tester.map($file),
125                Some(camino::Utf8PathBuf::from($expected))
126            );
127        };
128        ($tester:expr, $file:literal, None) => {
129            assert_eq!($tester.map($file), None);
130        };
131    }
132
133    #[macro_export]
134    macro_rules! assert_maps {
135        ($tester:expr, [$($file:literal => $expected:tt),* $(,)?]) => {
136            {
137                let _tester = $tester;
138                $(
139                    assert_map!(_tester, $file, $expected);
140                )*
141            }
142        };
143    }
144
145    pub struct MapFileTester<T> {
146        loader: T,
147        package: PackageRef,
148        loader_package: bool,
149    }
150
151    impl<T: Loader> MapFileTester<T> {
152        pub fn new(loader: T, package: PackageRef, loader_package: bool) -> Self {
153            Self {
154                package,
155                loader_package,
156                loader,
157            }
158        }
159
160        pub fn ruleset(&self) -> InstallRuleset<'_> {
161            if self.loader_package {
162                self.loader.loader_install_rules()
163            } else {
164                self.loader.package_install_rules()
165            }
166        }
167
168        pub fn map(&self, file: impl AsRef<Utf8Path>) -> Option<Utf8PathBuf> {
169            self.ruleset().map_file(file, &self.package)
170        }
171    }
172}