minecraft_java_rs_core/loader/
fabric.rs1use tokio::sync::mpsc::Sender;
2
3use crate::error::LoaderError;
4use crate::launcher::events::LaunchEvent;
5use crate::launcher::options::LaunchOptions;
6use crate::models::loader::{FabricJson, FabricMeta, LoaderType};
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
12const FABRIC_META: &str = "https://meta.fabricmc.net/v2/versions";
15const FABRIC_PROFILE: &str =
16 "https://meta.fabricmc.net/v2/versions/loader/${version}/${build}/profile/json";
17
18const LEGACY_META: &str = "https://meta.legacyfabric.net/v2/versions";
19const LEGACY_PROFILE: &str =
20 "https://meta.legacyfabric.net/v2/versions/loader/${version}/${build}/profile/json";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum FabricVariant {
26 Modern,
27 Legacy,
28}
29
30pub struct FabricMC {
31 variant: FabricVariant,
32}
33
34impl FabricMC {
37 pub fn new(variant: FabricVariant) -> Self {
38 Self { variant }
39 }
40
41 pub fn loader_type(&self) -> LoaderType {
42 match self.variant {
43 FabricVariant::Modern => LoaderType::Fabric,
44 FabricVariant::Legacy => LoaderType::LegacyFabric,
45 }
46 }
47
48 pub async fn download_json(
53 &self,
54 mc_version: &str,
55 build: &str,
56 client: &reqwest::Client,
57 ) -> Result<FabricJson, LoaderError> {
58 let (meta_url, profile_template) = match self.variant {
59 FabricVariant::Modern => (FABRIC_META, FABRIC_PROFILE),
60 FabricVariant::Legacy => (LEGACY_META, LEGACY_PROFILE),
61 };
62
63 let meta: FabricMeta = fetch_json(client, meta_url)
64 .await
65 .map_err(LoaderError::ApiError)?;
66
67 let version_name = match self.variant {
69 FabricVariant::Modern => "FabricMC",
70 FabricVariant::Legacy => "LegacyFabric",
71 };
72 if !meta.game.iter().any(|g| g.version == mc_version) {
73 return Err(LoaderError::VersionNotFound(format!(
74 "{version_name} doesn't support Minecraft {mc_version}"
75 )));
76 }
77
78 let build_ver = if matches!(build, "latest" | "recommended") {
80 meta.loader
81 .first()
82 .map(|b| b.version.clone())
83 .ok_or_else(|| {
84 LoaderError::VersionNotFound(format!("No {version_name} builds available"))
85 })?
86 } else {
87 meta.loader
88 .iter()
89 .find(|b| b.version == build)
90 .map(|b| b.version.clone())
91 .ok_or_else(|| {
92 let available: Vec<_> =
93 meta.loader.iter().map(|b| b.version.as_str()).collect();
94 LoaderError::VersionNotFound(format!(
95 "{version_name} build {build} not found. Available: {}",
96 available.join(", ")
97 ))
98 })?
99 };
100
101 let profile_url = profile_template
102 .replace("${version}", mc_version)
103 .replace("${build}", &build_ver);
104
105 let json: FabricJson = fetch_json(client, &profile_url)
106 .await
107 .map_err(LoaderError::ApiError)?;
108
109 Ok(json)
110 }
111
112 pub async fn download_libraries(
117 &self,
118 options: &LaunchOptions,
119 fabric_json: &FabricJson,
120 _client: &reqwest::Client,
121 event_tx: &Sender<LaunchEvent>,
122 ) -> Result<Vec<AssetItem>, LoaderError> {
123 let libs = &fabric_json.libraries;
124 let total = libs.len();
125 let mut items: Vec<AssetItem> = Vec::with_capacity(total);
126 let mut pending: Vec<DownloadItem> = Vec::new();
127
128 for (idx, lib) in libs.iter().enumerate() {
129 let _ = event_tx
130 .send(LaunchEvent::Check {
131 current: idx + 1,
132 total,
133 kind: "libraries".into(),
134 })
135 .await;
136
137 if lib.rules.is_some() {
139 continue;
140 }
141
142 let lib_info = match get_path_libraries(&lib.name, None, None) {
143 Ok(i) => i,
144 Err(_) => continue,
145 };
146
147 let loader_name = match self.variant {
148 FabricVariant::Modern => "fabric",
149 FabricVariant::Legacy => "legacyfabric",
150 };
151 let folder = options
152 .loader_dir(loader_name)
153 .join("libraries")
154 .join(&lib_info.path);
155 let dest = folder.join(&lib_info.name);
156
157 let url = resolve_lib_url(lib, &lib_info.path, &lib_info.name);
158
159 items.push(AssetItem::Asset {
160 path: dest.to_string_lossy().into_owned(),
161 sha1: lib
162 .downloads
163 .as_ref()
164 .and_then(|d| d.artifact.as_ref())
165 .and_then(|a| a.sha1.clone())
166 .unwrap_or_default(),
167 size: lib
168 .downloads
169 .as_ref()
170 .and_then(|d| d.artifact.as_ref())
171 .and_then(|a| a.size)
172 .unwrap_or(0),
173 url: url.clone(),
174 });
175
176 if !dest.exists() {
177 pending.push(DownloadItem {
178 url,
179 path: dest,
180 folder,
181 name: lib_info.name,
182 size: 0,
183 r#type: Some("libraries".into()),
184 sha1: None,
185 });
186 }
187 }
188
189 if !pending.is_empty() {
190 let downloader = Downloader::new(
191 options.timeout_secs,
192 options.clamped_concurrency(),
193 options.force_ipv4,
194 );
195 downloader
196 .download_multiple(pending, event_tx.clone())
197 .await
198 .map_err(|e| {
199 LoaderError::Io(std::io::Error::new(
200 std::io::ErrorKind::Other,
201 e.to_string(),
202 ))
203 })?;
204 }
205
206 Ok(items)
207 }
208}
209
210pub(crate) fn resolve_lib_url(
213 lib: &crate::models::loader::LoaderLibrary,
214 rel_path: &str,
215 name: &str,
216) -> String {
217 if let Some(url) = lib
219 .downloads
220 .as_ref()
221 .and_then(|d| d.artifact.as_ref())
222 .map(|a| a.url.as_str())
223 {
224 return url.to_owned();
225 }
226
227 let base = lib
229 .url
230 .as_deref()
231 .unwrap_or("https://repo1.maven.org/maven2/");
232 let base = base.trim_end_matches('/');
233 format!("{base}/{rel_path}/{name}")
234}
235
236#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn fabric_variant_modern_and_legacy_differ() {
244 assert_ne!(FabricVariant::Modern, FabricVariant::Legacy);
245 }
246
247 #[test]
248 fn resolve_lib_url_uses_downloads_url_when_present() {
249 use crate::models::loader::{LoaderArtifact, LoaderLibraryDownloads};
250 let lib = crate::models::loader::LoaderLibrary {
251 name: "a:b:1.0".into(),
252 url: Some("https://repo.example.com/".into()),
253 downloads: Some(LoaderLibraryDownloads {
254 artifact: Some(LoaderArtifact {
255 sha1: None,
256 size: None,
257 path: None,
258 url: "https://direct.example.com/b-1.0.jar".into(),
259 }),
260 }),
261 rules: None,
262 clientreq: None,
263 };
264 let url = resolve_lib_url(&lib, "a/b/1.0", "b-1.0.jar");
265 assert_eq!(url, "https://direct.example.com/b-1.0.jar");
266 }
267
268 #[test]
269 fn resolve_lib_url_constructs_from_base_url() {
270 let lib = crate::models::loader::LoaderLibrary {
271 name: "a:b:1.0".into(),
272 url: Some("https://maven.fabricmc.net/".into()),
273 downloads: None,
274 rules: None,
275 clientreq: None,
276 };
277 let url = resolve_lib_url(&lib, "a/b/1.0", "b-1.0.jar");
278 assert_eq!(url, "https://maven.fabricmc.net/a/b/1.0/b-1.0.jar");
279 }
280
281 #[test]
282 fn resolve_lib_url_falls_back_to_maven_central() {
283 let lib = crate::models::loader::LoaderLibrary {
284 name: "a:b:1.0".into(),
285 url: None,
286 downloads: None,
287 rules: None,
288 clientreq: None,
289 };
290 let url = resolve_lib_url(&lib, "a/b/1.0", "b-1.0.jar");
291 assert!(url.contains("repo1.maven.org"));
292 assert!(url.contains("b-1.0.jar"));
293 }
294}