Skip to main content

loadsmith_loader/loaders/
melon_loader.rs

1use std::{path::PathBuf, sync::LazyLock};
2
3use camino::Utf8Path;
4use globset::GlobSet;
5use loadsmith_install::{InstallRule, InstallRuleset, OwnedInstallRuleset, RouteRule};
6
7use crate::{LaunchArgs, LaunchContext, Loader, Result, glob_rule};
8
9/// Loader implementation for [MelonLoader](https://melonloader.net/), a
10/// mod loader for Unity games.
11///
12/// MelonLoader supports two installation schemes:
13///
14/// * **Legacy** — routes files to `UserLibs`, `Plugins`, `MelonLoader/Managed`,
15///   `MelonLoader/Libs`, `MelonLoader`, `Mods`, and `UserData/ModManager`.
16/// * **Recursive** — routes all files into `Mods` and keeps `UserData` for
17///   mutable config.
18///
19/// # Examples
20///
21/// ```rust
22/// use loadsmith_loader::{MelonLoader, Loader};
23///
24/// let loader = MelonLoader::with_default_legacy_rules();
25/// assert_eq!(loader.id(), "MelonLoader");
26/// ```
27#[derive(Debug, Clone)]
28pub struct MelonLoader {
29    package_install_ruleset: OwnedInstallRuleset,
30}
31
32impl MelonLoader {
33    /// Creates a `MelonLoader` with a custom install ruleset.
34    pub fn with_rules(package_install_ruleset: OwnedInstallRuleset) -> Self {
35        Self {
36            package_install_ruleset,
37        }
38    }
39
40    /// Creates a `MelonLoader` with the legacy install rules (original
41    /// per-directory routing).
42    pub fn with_default_legacy_rules() -> Self {
43        OwnedInstallRuleset::from_rule_iter(
44            [
45                RouteRule::new_static("UserLibs")
46                    .with_file_extension("lib.dll")
47                    .with_subdir(false),
48                RouteRule::new_static("Plugins")
49                    .with_file_extension("plugin.dll")
50                    .with_subdir(false),
51                RouteRule::new_static("MelonLoader/Managed")
52                    .with_file_extension("managed.dll")
53                    .with_subdir(false),
54                RouteRule::new_static("MelonLoader/Libs").with_subdir(false),
55                RouteRule::new_static("MelonLoader").with_subdir(false),
56                RouteRule::new_static("Mods")
57                    .with_file_extension("dll")
58                    .with_subdir(false),
59                RouteRule::new_static("UserData/ModManager")
60                    .with_flatten(false)
61                    .with_mutable(true),
62            ],
63            Some(6),
64        )
65        .map(Self::with_rules)
66        .expect("rules are not empty so there should always be a valid default rule index")
67    }
68
69    /// Creates a `MelonLoader` with the recursive install rules (all files go
70    /// into `Mods`).
71    pub fn with_default_recursive_rules() -> Self {
72        OwnedInstallRuleset::from_rule_iter(
73            [
74                RouteRule::new_with_target("", Utf8Path::new("Mods")).with_flatten(false),
75                RouteRule::new_static("UserData").with_mutable(true),
76            ],
77            Some(0),
78        )
79        .map(Self::with_rules)
80        .expect("rules are not empty so there should always be a valid default rule index")
81    }
82
83    /// Sets a glob set of files to exclude from installation.
84    pub fn set_exclude(&mut self, exclude: GlobSet) {
85        self.package_install_ruleset.set_exclude(exclude);
86    }
87
88    /// Adds an install rule to the end of the ruleset.
89    pub fn add_install_rule(&mut self, rule: InstallRule) {
90        self.package_install_ruleset.add_rule(rule);
91    }
92}
93
94impl Default for MelonLoader {
95    fn default() -> Self {
96        Self::with_default_legacy_rules()
97    }
98}
99
100impl Loader for MelonLoader {
101    fn id(&self) -> &'static str {
102        "MelonLoader"
103    }
104
105    fn loader_install_rules(&self) -> InstallRuleset<'_> {
106        static RULES: LazyLock<Vec<InstallRule>> = LazyLock::new(|| {
107            vec![
108                glob_rule!("MelonLoader/{Dependencies,Documentation,net*}/*" => ".")
109                    .use_links(true)
110                    .into(),
111                glob_rule!("{version,dobby}.dll" => ".").into(),
112            ]
113        });
114
115        InstallRuleset::new(&RULES)
116    }
117
118    fn package_install_rules(&self) -> InstallRuleset<'_> {
119        self.package_install_ruleset.as_ref()
120    }
121
122    fn generate_launch_args(&self, ctx: &LaunchContext) -> Result<LaunchArgs> {
123        let mut args = LaunchArgs::new()
124            .arg("--melonloader.basedir")
125            .arg(ctx.profile_path());
126
127        let mono_assembly_exists = ctx
128            .profile_path()
129            .join("MelonLoader/Managed/Assembly-CSharp.dll")
130            .exists();
131        let il2cpp_assembly_exists = ctx
132            .profile_path()
133            .join("MelonLoader/Il2CppAssemblies/Assembly-CSharp.dll")
134            .exists();
135
136        if !mono_assembly_exists && !il2cpp_assembly_exists {
137            args = args.arg("--melonloader.agfregenerate");
138        }
139
140        Ok(args)
141    }
142
143    fn package_config_dirs(&self) -> Vec<PathBuf> {
144        Vec::new()
145    }
146
147    fn log_file(&self) -> Option<PathBuf> {
148        Some("MelonLoader/Latest.log".into())
149    }
150
151    fn proxy_dll(&self) -> Option<PathBuf> {
152        None
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use loadsmith_core::{PackageRef, Version};
159
160    use crate::{assert_map, assert_maps, test_util::MapFileTester};
161
162    use super::*;
163
164    #[test]
165    fn map_loader_files() {
166        assert_maps!(MapFileTester::new(
167            MelonLoader::with_default_legacy_rules(),
168            PackageRef::new("LavaGang-MelonLoader".to_string(), Version::new(1, 0, 0)),
169            true,
170        ), [
171            "README.md" => None,
172            "version.dll" => "./version.dll",
173            "MelonLoader/Dependencies/Name/file.dll" => "./MelonLoader/Dependencies/Name/file.dll",
174            "MelonLoader/Documentation/file.txt" => "./MelonLoader/Documentation/file.txt",
175            "MelonLoader/net6/file.dll" => "./MelonLoader/net6/file.dll",
176            "MelonLoader/net35/file.dll" => "./MelonLoader/net35/file.dll",
177            "MelonLoader/net472/file.dll" => "./MelonLoader/net472/file.dll",
178        ])
179    }
180
181    #[test]
182    fn map_package_files() {
183        assert_maps!(MapFileTester::new(
184            MelonLoader::with_default_legacy_rules(),
185            PackageRef::new("Author-Name".to_string(), Version::new(1, 0, 0)),
186            false,
187        ), [
188            "UserLibs/file" => "UserLibs/file",
189            "UserLibs/nested/file" => "UserLibs/nested/file",
190            "userlibrary.lib.dll" => "UserLibs/userlibrary.lib.dll",
191            "Managed/file" => "MelonLoader/Managed/file",
192            "Managed/nested/file" => "MelonLoader/Managed/nested/file",
193            "managed.managed.dll" => "MelonLoader/Managed/managed.managed.dll",
194            "Mods/file" => "Mods/file",
195            "Mods/nested/file" => "Mods/nested/file",
196            "mod.dll" => "Mods/mod.dll",
197            "manifest.json" => "UserData/ModManager/Author-Name/manifest.json",
198            "ModManager/file" => "UserData/ModManager/Author-Name/file",
199            "ModManager/nested/file" => "UserData/ModManager/Author-Name/nested/file",
200            "MelonLoader/file" => "MelonLoader/file",
201            "MelonLoader/nested/file" => "MelonLoader/nested/file",
202            "MelonLoader/Libs/file" => "MelonLoader/Libs/file",
203            "MelonLoader/Libs/nested/file" => "MelonLoader/Libs/nested/file",
204            "MelonLoader/Libs/lib.dll" => "MelonLoader/Libs/lib.dll",
205        ]);
206    }
207
208    #[test]
209    fn package_dir_works() {
210        let loader = MelonLoader::with_default_legacy_rules();
211        let package = PackageRef::new("Author-Name".to_string(), Version::new(1, 0, 0));
212
213        let package_dir = loader.package_dir(&package);
214
215        assert_eq!(
216            package_dir,
217            Some(PathBuf::from("UserData/ModManager/Author-Name"))
218        );
219    }
220
221    #[test]
222    fn loader_files_should_link() {
223        let loader = MelonLoader::with_default_legacy_rules();
224        let rules = loader.loader_install_rules();
225
226        assert!(
227            rules
228                .find_rule_for_mapped_path("./MelonLoader/Dependencies/Name/file.dll")
229                .unwrap()
230                .use_links()
231        );
232    }
233}