loadsmith_loader/loaders/
bep_in_ex.rs1use std::{path::PathBuf, sync::LazyLock};
2
3use camino::Utf8PathBuf;
4use globset::{GlobBuilder, GlobSet};
5use loadsmith_install::{InstallRule, InstallRuleset, OwnedInstallRuleset, RouteRule};
6
7use crate::{Error, LaunchArgs, LaunchContext, Loader, Result, doorstop, glob, glob_rule};
8
9#[derive(Debug, Clone)]
24pub struct BepInEx {
25 package_install_ruleset: OwnedInstallRuleset,
26}
27
28impl BepInEx {
29 pub fn with_rules(package_install_ruleset: OwnedInstallRuleset) -> Self {
31 Self {
32 package_install_ruleset,
33 }
34 }
35
36 pub fn with_default_rules() -> Self {
46 OwnedInstallRuleset::from_rule_iter(
47 vec![
48 RouteRule::new_static("BepInEx/config")
49 .with_subdir(false)
50 .with_mutable(true),
51 RouteRule::new_static("BepInEx/patchers"),
52 RouteRule::new_static("BepInEx/core"),
53 RouteRule::new_static("BepInEx/monomod").with_file_extension("mm.dll"),
54 RouteRule::new_static("BepInEx/plugins").with_file_extension("dll"),
55 ],
56 Some(4),
57 )
58 .map(Self::with_rules)
59 .expect("rules are not empty so there should always be a valid default rule index")
60 }
61
62 pub fn add_install_rule(&mut self, rule: impl Into<InstallRule>) {
64 self.package_install_ruleset.add_rule(rule.into());
65 }
66
67 pub fn insert_install_rule(&mut self, index: usize, rule: impl Into<InstallRule>) {
69 self.package_install_ruleset.insert_rule(index, rule.into());
70 }
71}
72
73impl Default for BepInEx {
74 fn default() -> Self {
75 Self::with_default_rules()
76 }
77}
78
79impl Loader for BepInEx {
80 fn id(&self) -> &'static str {
81 "BepInEx"
82 }
83
84 fn loader_install_rules(&self) -> InstallRuleset<'_> {
85 static RULES: LazyLock<Vec<InstallRule>> = LazyLock::new(|| {
86 vec![
87 glob_rule!("*/*.{ini,cfg}" => ".")
88 .strip_top_level(true)
89 .into(),
90 glob_rule!("*/*" => ".")
91 .strip_top_level(true)
92 .use_links(true)
93 .into(),
94 ]
95 });
96
97 InstallRuleset::new(&RULES)
98 }
99
100 fn package_install_rules(&self) -> InstallRuleset<'_> {
101 self.package_install_ruleset.as_ref()
102 }
103
104 fn prepare_launch(&self, ctx: &LaunchContext) -> Result<()> {
105 static INCLUDE_SET: LazyLock<GlobSet> = LazyLock::new(|| {
106 GlobSet::builder()
107 .add(
108 GlobBuilder::new("*")
110 .literal_separator(true)
111 .build()
112 .expect("constant glob should be valid"),
113 )
114 .add(glob!("doorstop_libs/*"))
115 .add(glob!("dotnet/*"))
116 .add(glob!("corlibs/*"))
117 .build()
118 .expect("constant globs should be valid")
119 });
120
121 ctx.copy_glob_to_game(&INCLUDE_SET)
122 }
123
124 fn generate_launch_args(&self, ctx: &LaunchContext) -> Result<LaunchArgs> {
125 let (enable_prefix, target_prefix) = doorstop::args(None, ctx)?;
126 let preloader_path = bepinex_preloader_path(None, ctx)?;
127
128 let args = LaunchArgs::new()
129 .arg(enable_prefix)
130 .arg("true")
131 .arg(target_prefix)
132 .arg(&*ctx.format_proton_path(&preloader_path));
133
134 Ok(args)
135 }
136
137 fn package_config_dirs(&self) -> Vec<PathBuf> {
138 vec!["BepInEx/config".into()]
139 }
140
141 fn log_file(&self) -> Option<PathBuf> {
142 Some("BepInEx/BepInEx.log".into())
143 }
144
145 fn proxy_dll(&self) -> Option<PathBuf> {
146 Some("winhttp.dll".into())
147 }
148}
149
150pub(crate) fn bepinex_preloader_path(
151 prefix: Option<&str>,
152 ctx: &LaunchContext,
153) -> Result<Utf8PathBuf> {
154 let mut core_directory = ctx.profile_path().to_path_buf();
155
156 if let Some(prefix) = prefix {
157 core_directory.push(prefix);
158 }
159
160 core_directory.push("BepInEx");
161 core_directory.push("core");
162
163 const PRELOADER_NAMES: &[&str] = &[
164 "BepInEx.Unity.Mono.Preloader.dll",
165 "BepInEx.Unity.IL2CPP.dll",
166 "BepInEx.Preloader.dll",
167 "BepInEx.IL2CPP.dll",
168 ];
169
170 let entry = core_directory
171 .read_dir()
172 .map_err(|err| Error::BepInExCoreDirectoryMissing { source: err })?
173 .filter_map(|entry| entry.ok())
174 .find(|entry| {
175 let file_name = entry.file_name();
176 PRELOADER_NAMES.iter().any(|name| file_name == **name)
177 })
178 .ok_or(Error::BepInExPreloaderNotFound { core_directory })?;
179
180 let path = Utf8PathBuf::from_path_buf(entry.path())
181 .expect("BepInEx core directory should be valid UTF-8");
182
183 Ok(path)
184}
185
186#[cfg(test)]
187mod tests {
188 use loadsmith_core::{PackageRef, Version};
189
190 use crate::{assert_map, assert_maps, test_util::MapFileTester};
191
192 use super::*;
193
194 #[test]
195 fn map_loader_files() {
196 assert_maps!(MapFileTester::new(
197 BepInEx::with_default_rules(),
198 PackageRef::new("BepInEx-BepInExPack".to_string(), Version::new(5, 4, 2100)),
199 true,
200 ), [
201 "README.md" => None,
202 "BepInExPack/doorstop_config.ini" => "./doorstop_config.ini",
203 "BepInExPack/BepInEx/core/BepInEx.Preloader.dll" => "./BepInEx/core/BepInEx.Preloader.dll"
204 ])
205 }
206
207 #[test]
208 fn map_package_files() {
209 assert_maps!(MapFileTester::new(
210 BepInEx::with_default_rules(),
211 PackageRef::new("Author-Name".to_string(), Version::new(1, 0, 0)),
212 false,
213 ), [
214 "README.md" => "BepInEx/plugins/Author-Name/README.md",
215 "nested/file.txt" => "BepInEx/plugins/Author-Name/file.txt",
216 "plugins/nested/file.txt" => "BepInEx/plugins/Author-Name/nested/file.txt",
217 "BepInEx/plugins/file.txt" => "BepInEx/plugins/Author-Name/file.txt",
218 "config/settings.json" => "BepInEx/config/settings.json",
219 "BepInEx/config/myconfig.cfg" => "BepInEx/config/myconfig.cfg",
220 "patchers/patcher.dll" => "BepInEx/patchers/Author-Name/patcher.dll",
221 "core/core.dll" => "BepInEx/core/Author-Name/core.dll",
222 "patch.mm.dll" => "BepInEx/monomod/Author-Name/patch.mm.dll",
223 "nested/patch.mm.dll" => "BepInEx/monomod/Author-Name/patch.mm.dll",
224 "monomod/patch.dll" => "BepInEx/monomod/Author-Name/patch.dll",
225 "monomod/nested/patch.dll" => "BepInEx/monomod/Author-Name/nested/patch.dll"
226 ]);
227 }
228
229 #[test]
230 fn config_files_should_not_link() {
231 let loader = BepInEx::with_default_rules();
232 let rules = loader.package_install_rules();
233
234 assert!(
235 !rules
236 .find_rule_for_path("config/settings.json")
237 .unwrap()
238 .use_links()
239 );
240 }
241
242 #[test]
243 fn other_files_should_link() {
244 let loader = BepInEx::with_default_rules();
245 let rules = loader.package_install_rules();
246
247 assert!(
248 rules
249 .find_rule_for_path("plugins/file.txt")
250 .unwrap()
251 .use_links()
252 );
253
254 assert!(
255 rules
256 .find_rule_for_path("patchers/patch.dll")
257 .unwrap()
258 .use_links()
259 );
260
261 assert!(
262 rules
263 .find_rule_for_path("core/core.dll")
264 .unwrap()
265 .use_links()
266 );
267
268 assert!(
269 rules
270 .find_rule_for_path("patch.mm.dll")
271 .unwrap()
272 .use_links()
273 );
274 }
275
276 #[test]
277 fn package_dir_works() {
278 let loader = BepInEx::with_default_rules();
279 let package = PackageRef::new("Author-Name".to_string(), Version::new(1, 0, 0));
280
281 let package_dir = loader.package_dir(&package).unwrap();
282
283 assert_eq!(package_dir, PathBuf::from("BepInEx/plugins/Author-Name"));
284 }
285}