Skip to main content

loadsmith_loader/loaders/
shimloader.rs

1use std::{path::PathBuf, sync::LazyLock, vec};
2
3use loadsmith_install::{InstallRule, InstallRuleset, OwnedInstallRuleset, RouteRule};
4
5use crate::{LaunchArgs, LaunchContext, Loader, Result, glob_rule};
6
7/// Loader implementation for [Unreal Shimloader](https://thunderstore.io/c/holocure/p/Thunderstore/unreal_shimloader/),
8/// a UE4SS-based mod loader for games like HoloCure.
9///
10/// Routes packages into `shimloader/mod`, `shimloader/pak` (`.pak` files
11/// only), `shimloader/cfg` (mutable, flat), and `shimloader/overlay`. The
12/// loader uses `dwmapi.dll` as its proxy DLL.
13///
14/// # Examples
15///
16/// ```rust
17/// use loadsmith_loader::{Shimloader, Loader};
18///
19/// let loader = Shimloader::with_default_rules();
20/// assert_eq!(loader.id(), "Shimloader");
21/// ```
22#[derive(Debug, Clone)]
23pub struct Shimloader {
24    package_install_ruleset: OwnedInstallRuleset,
25}
26
27impl Shimloader {
28    /// Creates a `Shimloader` with a custom install ruleset.
29    pub fn with_rules(package_install_ruleset: OwnedInstallRuleset) -> Self {
30        Self {
31            package_install_ruleset,
32        }
33    }
34
35    /// Creates a `Shimloader` with the default rules.
36    ///
37    /// Default routes:
38    ///
39    /// * `shimloader/mod` — default rule
40    /// * `shimloader/pak` (files with `.pak` extension)
41    /// * `shimloader/cfg` (mutable, flat)
42    /// * `shimloader/overlay`
43    pub fn with_default_rules() -> Self {
44        OwnedInstallRuleset::from_rule_iter(
45            [
46                RouteRule::new_static("shimloader/mod"),
47                RouteRule::new_static("shimloader/pak").with_file_extension("pak"),
48                RouteRule::new_static("shimloader/cfg")
49                    .with_subdir(false)
50                    .with_mutable(true),
51                RouteRule::new_static("shimloader/overlay"),
52            ],
53            Some(0),
54        )
55        .map(Self::with_rules)
56        .expect("rules are not empty so there should always be a valid default rule index")
57    }
58}
59
60impl Default for Shimloader {
61    fn default() -> Self {
62        Self::with_default_rules()
63    }
64}
65
66impl Loader for Shimloader {
67    fn id(&self) -> &'static str {
68        "Shimloader"
69    }
70
71    fn loader_install_rules(&self) -> InstallRuleset<'_> {
72        static RULES: LazyLock<Vec<InstallRule>> = LazyLock::new(|| {
73            vec![
74                glob_rule!("UE4SS/Mods/*" => "shimloader/mod")
75                    .use_links(true)
76                    .strip_levels(2)
77                    .into(),
78                glob_rule!("UE4SS/UE4SS.dll" => ".")
79                    .use_links(true)
80                    .strip_levels(1)
81                    .into(),
82                glob_rule!("UE4SS/UE4SS-settings.ini" => ".")
83                    .use_links(true)
84                    .strip_levels(1)
85                    .into(),
86                glob_rule!("dwmapi.dll" => ".").use_links(true).into(),
87            ]
88        });
89
90        InstallRuleset::new(&RULES)
91    }
92
93    fn package_install_rules(&self) -> InstallRuleset<'_> {
94        self.package_install_ruleset.as_ref()
95    }
96
97    fn generate_launch_args(&self, ctx: &LaunchContext) -> Result<LaunchArgs> {
98        let path = ctx.profile_path().join("shimloader");
99
100        let mod_path = path.join("mod");
101        let pak_path = path.join("pak");
102        let cfg_path = path.join("cfg");
103
104        let mod_path = ctx.format_proton_path(&mod_path);
105        let pak_path = ctx.format_proton_path(&pak_path);
106        let cfg_path = ctx.format_proton_path(&cfg_path);
107
108        let args = LaunchArgs::new()
109            .arg("--mod-dir")
110            .arg(&*mod_path)
111            .arg("--pak-dir")
112            .arg(&*pak_path)
113            .arg("--cfg-dir")
114            .arg(&*cfg_path);
115
116        Ok(args)
117    }
118
119    fn package_config_dirs(&self) -> Vec<PathBuf> {
120        vec!["shimloader/cfg".into()]
121    }
122
123    fn log_file(&self) -> Option<PathBuf> {
124        None
125    }
126
127    fn proxy_dll(&self) -> Option<PathBuf> {
128        Some("dwmapi.dll".into())
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use loadsmith_core::{PackageRef, Version};
135
136    use crate::{assert_map, assert_maps, test_util::MapFileTester};
137
138    use super::*;
139
140    #[test]
141    fn map_loader_files() {
142        assert_maps!(MapFileTester::new(
143            Shimloader::with_default_rules(),
144            PackageRef::new("Thunderstore-unreal_shimloader".to_string(), Version::new(1, 1, 7)),
145            true,
146        ), [
147            "README.md" => None,
148            "dwmapi.dll" => "./dwmapi.dll",
149            "UE4SS/dwmapi.dll" => None,
150            "UE4SS/UE4SS.dll" => "./UE4SS.dll",
151            "UE4SS/UE4SS-settings.ini" => "./UE4SS-settings.ini",
152            "UE4SS/Mods/mods.json" => "shimloader/mod/mods.json",
153            "UE4SS/Mods/mods.txt" => "shimloader/mod/mods.txt",
154            "UE4SS/Mods/shared/scripts/script.lua" => "shimloader/mod/shared/scripts/script.lua",
155        ]);
156    }
157
158    #[test]
159    fn map_package_files() {
160        assert_maps!(
161            MapFileTester::new(
162                Shimloader::with_default_rules(),
163                PackageRef::new("Author-Name".to_string(), Version::new(1, 0, 0)),
164                false,
165            ),
166            [
167                "README.md" => "shimloader/mod/Author-Name/README.md",
168                "pak/file" => "shimloader/pak/Author-Name/file",
169                "cfg/settings.json" => "shimloader/cfg/settings.json",
170                "nested/file.txt" => "shimloader/mod/Author-Name/file.txt",
171                "mypak.pak" => "shimloader/pak/Author-Name/mypak.pak",
172            ]
173        );
174    }
175
176    #[test]
177    fn package_dir_works() {
178        let loader = Shimloader::with_default_rules();
179        let package = PackageRef::new("Author-Name".to_string(), Version::new(1, 0, 0));
180
181        let package_dir = loader.package_dir(&package);
182
183        assert_eq!(
184            package_dir,
185            Some(PathBuf::from("shimloader/mod/Author-Name"))
186        );
187    }
188}