Skip to main content

minecraft_java_rs_core/loader/
quilt.rs

1use tokio::sync::mpsc::Sender;
2
3use crate::error::LoaderError;
4use crate::launcher::events::LaunchEvent;
5use crate::launcher::options::LaunchOptions;
6use crate::models::loader::{FabricJson, QuiltMeta};
7use crate::models::minecraft::AssetItem;
8use crate::net::downloader::{DownloadItem, Downloader};
9use crate::net::http::fetch_json;
10use crate::utils::paths::get_path_libraries;
11
12use super::fabric::resolve_lib_url;
13
14const QUILT_META: &str = "https://meta.quiltmc.org/v3/versions";
15const QUILT_PROFILE: &str =
16    "https://meta.quiltmc.org/v3/versions/loader/${version}/${build}/profile/json";
17
18// ── Public API ────────────────────────────────────────────────────────────────
19
20pub struct QuiltMC;
21
22impl QuiltMC {
23    pub fn new() -> Self {
24        Self
25    }
26
27    /// Fetch the Quilt loader profile JSON for the given Minecraft version.
28    ///
29    /// `build` options:
30    /// - `"latest"` → first build in the list (highest version).
31    /// - `"recommended"` → first build whose version does not contain `"beta"`.
32    /// - Any other string → exact version match.
33    pub async fn download_json(
34        &self,
35        mc_version: &str,
36        build: &str,
37        client: &reqwest::Client,
38    ) -> Result<FabricJson, LoaderError> {
39        let meta: QuiltMeta = fetch_json(client, QUILT_META)
40            .await
41            .map_err(LoaderError::ApiError)?;
42
43        if !meta.game.iter().any(|g| g.version == mc_version) {
44            return Err(LoaderError::VersionNotFound(format!(
45                "QuiltMC doesn't support Minecraft {mc_version}"
46            )));
47        }
48
49        let build_ver = match build {
50            "latest" => meta
51                .loader
52                .first()
53                .map(|b| b.version.clone())
54                .ok_or_else(|| LoaderError::VersionNotFound("No Quilt builds available".into()))?,
55            "recommended" => meta
56                .loader
57                .iter()
58                .find(|b| !b.version.contains("beta"))
59                .map(|b| b.version.clone())
60                .ok_or_else(|| {
61                    LoaderError::VersionNotFound("No stable Quilt build found".into())
62                })?,
63            ver => meta
64                .loader
65                .iter()
66                .find(|b| b.version == ver)
67                .map(|b| b.version.clone())
68                .ok_or_else(|| {
69                    let available: Vec<_> =
70                        meta.loader.iter().map(|b| b.version.as_str()).collect();
71                    LoaderError::VersionNotFound(format!(
72                        "Quilt build {ver} not found. Available: {}",
73                        available.join(", ")
74                    ))
75                })?,
76        };
77
78        let profile_url = QUILT_PROFILE
79            .replace("${version}", mc_version)
80            .replace("${build}", &build_ver);
81
82        let json: FabricJson = fetch_json(client, &profile_url)
83            .await
84            .map_err(LoaderError::ApiError)?;
85
86        Ok(json)
87    }
88
89    /// Download any Quilt libraries not yet on disk.
90    ///
91    /// Behaviour is identical to `FabricMC::download_libraries`.
92    pub async fn download_libraries(
93        &self,
94        options: &LaunchOptions,
95        quilt_json: &FabricJson,
96        _client: &reqwest::Client,
97        event_tx: &Sender<LaunchEvent>,
98    ) -> Result<Vec<AssetItem>, LoaderError> {
99        let libs = &quilt_json.libraries;
100        let total = libs.len();
101        let mut items: Vec<AssetItem> = Vec::with_capacity(total);
102        let mut pending: Vec<DownloadItem> = Vec::new();
103
104        for (idx, lib) in libs.iter().enumerate() {
105            let _ = event_tx
106                .send(LaunchEvent::Check {
107                    current: idx + 1,
108                    total,
109                    kind: "libraries".into(),
110                })
111                .await;
112
113            if lib.rules.is_some() {
114                continue;
115            }
116
117            let lib_info = match get_path_libraries(&lib.name, None, None) {
118                Ok(i) => i,
119                Err(_) => continue,
120            };
121
122            let folder = options
123                .loader_dir("quilt")
124                .join("libraries")
125                .join(&lib_info.path);
126            let dest = folder.join(&lib_info.name);
127            let url = resolve_lib_url(lib, &lib_info.path, &lib_info.name);
128
129            items.push(AssetItem::Asset {
130                path: dest.to_string_lossy().into_owned(),
131                sha1: lib
132                    .downloads
133                    .as_ref()
134                    .and_then(|d| d.artifact.as_ref())
135                    .and_then(|a| a.sha1.clone())
136                    .unwrap_or_default(),
137                size: lib
138                    .downloads
139                    .as_ref()
140                    .and_then(|d| d.artifact.as_ref())
141                    .and_then(|a| a.size)
142                    .unwrap_or(0),
143                url: url.clone(),
144            });
145
146            if !dest.exists() {
147                pending.push(DownloadItem {
148                    url,
149                    path: dest,
150                    folder,
151                    name: lib_info.name,
152                    size: 0,
153                    r#type: Some("libraries".into()),
154                    sha1: None,
155                });
156            }
157        }
158
159        if !pending.is_empty() {
160            let downloader = Downloader::new(
161                options.timeout_secs,
162                options.clamped_concurrency(),
163                options.force_ipv4,
164            );
165            downloader
166                .download_multiple(pending, event_tx.clone())
167                .await
168                .map_err(|e| {
169                    LoaderError::Io(std::io::Error::new(
170                        std::io::ErrorKind::Other,
171                        e.to_string(),
172                    ))
173                })?;
174        }
175
176        Ok(items)
177    }
178}
179
180impl Default for QuiltMC {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186// ── Tests ─────────────────────────────────────────────────────────────────────
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn quilt_mc_constructs_without_options() {
194        let _q = QuiltMC::new();
195    }
196
197    #[test]
198    fn quilt_mc_default_same_as_new() {
199        let _q = QuiltMC::default();
200    }
201}