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(|| LoaderError::VersionNotFound(format!("No {version_name} builds available")))?
84 } else {
85 meta.loader
86 .iter()
87 .find(|b| b.version == build)
88 .map(|b| b.version.clone())
89 .ok_or_else(|| {
90 let available: Vec<_> = meta.loader.iter().map(|b| b.version.as_str()).collect();
91 LoaderError::VersionNotFound(format!(
92 "{version_name} build {build} not found. Available: {}",
93 available.join(", ")
94 ))
95 })?
96 };
97
98 let profile_url = profile_template
99 .replace("${version}", mc_version)
100 .replace("${build}", &build_ver);
101
102 let json: FabricJson = fetch_json(client, &profile_url)
103 .await
104 .map_err(LoaderError::ApiError)?;
105
106 Ok(json)
107 }
108
109 pub async fn download_libraries(
114 &self,
115 options: &LaunchOptions,
116 fabric_json: &FabricJson,
117 _client: &reqwest::Client,
118 event_tx: &Sender<LaunchEvent>,
119 ) -> Result<Vec<AssetItem>, LoaderError> {
120 let libs = &fabric_json.libraries;
121 let total = libs.len();
122 let mut items: Vec<AssetItem> = Vec::with_capacity(total);
123 let mut pending: Vec<DownloadItem> = Vec::new();
124
125 for (idx, lib) in libs.iter().enumerate() {
126 let _ = event_tx
127 .send(LaunchEvent::Check {
128 current: idx + 1,
129 total,
130 kind: "libraries".into(),
131 })
132 .await;
133
134 if lib.rules.is_some() {
136 continue;
137 }
138
139 let lib_info = match get_path_libraries(&lib.name, None, None) {
140 Ok(i) => i,
141 Err(_) => continue,
142 };
143
144 let loader_name = match self.variant {
145 FabricVariant::Modern => "fabric",
146 FabricVariant::Legacy => "legacyfabric",
147 };
148 let folder = options
149 .loader_dir(loader_name)
150 .join("libraries")
151 .join(&lib_info.path);
152 let dest = folder.join(&lib_info.name);
153
154 let url = resolve_lib_url(lib, &lib_info.path, &lib_info.name);
155
156 items.push(AssetItem::Asset {
157 path: dest.to_string_lossy().into_owned(),
158 sha1: lib
159 .downloads
160 .as_ref()
161 .and_then(|d| d.artifact.as_ref())
162 .and_then(|a| a.sha1.clone())
163 .unwrap_or_default(),
164 size: lib
165 .downloads
166 .as_ref()
167 .and_then(|d| d.artifact.as_ref())
168 .and_then(|a| a.size)
169 .unwrap_or(0),
170 url: url.clone(),
171 });
172
173 if !dest.exists() {
174 pending.push(DownloadItem {
175 url,
176 path: dest,
177 folder,
178 name: lib_info.name,
179 size: 0,
180 r#type: Some("libraries".into()),
181 sha1: None,
182 });
183 }
184 }
185
186 if !pending.is_empty() {
187 let downloader = Downloader::new(options.timeout_secs, options.download_concurrency);
188 downloader
189 .download_multiple(pending, event_tx.clone())
190 .await
191 .map_err(|e| LoaderError::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))?;
192 }
193
194 Ok(items)
195 }
196}
197
198pub(crate) fn resolve_lib_url(
201 lib: &crate::models::loader::LoaderLibrary,
202 rel_path: &str,
203 name: &str,
204) -> String {
205 if let Some(url) = lib
207 .downloads
208 .as_ref()
209 .and_then(|d| d.artifact.as_ref())
210 .map(|a| a.url.as_str())
211 {
212 return url.to_owned();
213 }
214
215 let base = lib
217 .url
218 .as_deref()
219 .unwrap_or("https://repo1.maven.org/maven2/");
220 let base = base.trim_end_matches('/');
221 format!("{base}/{rel_path}/{name}")
222}
223
224#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn fabric_variant_modern_and_legacy_differ() {
232 assert_ne!(FabricVariant::Modern, FabricVariant::Legacy);
233 }
234
235 #[test]
236 fn resolve_lib_url_uses_downloads_url_when_present() {
237 use crate::models::loader::{LoaderArtifact, LoaderLibraryDownloads};
238 let lib = crate::models::loader::LoaderLibrary {
239 name: "a:b:1.0".into(),
240 url: Some("https://repo.example.com/".into()),
241 downloads: Some(LoaderLibraryDownloads {
242 artifact: Some(LoaderArtifact {
243 sha1: None,
244 size: None,
245 path: None,
246 url: "https://direct.example.com/b-1.0.jar".into(),
247 }),
248 }),
249 rules: None,
250 clientreq: None,
251 };
252 let url = resolve_lib_url(&lib, "a/b/1.0", "b-1.0.jar");
253 assert_eq!(url, "https://direct.example.com/b-1.0.jar");
254 }
255
256 #[test]
257 fn resolve_lib_url_constructs_from_base_url() {
258 let lib = crate::models::loader::LoaderLibrary {
259 name: "a:b:1.0".into(),
260 url: Some("https://maven.fabricmc.net/".into()),
261 downloads: None,
262 rules: None,
263 clientreq: None,
264 };
265 let url = resolve_lib_url(&lib, "a/b/1.0", "b-1.0.jar");
266 assert_eq!(url, "https://maven.fabricmc.net/a/b/1.0/b-1.0.jar");
267 }
268
269 #[test]
270 fn resolve_lib_url_falls_back_to_maven_central() {
271 let lib = crate::models::loader::LoaderLibrary {
272 name: "a:b:1.0".into(),
273 url: None,
274 downloads: None,
275 rules: None,
276 clientreq: None,
277 };
278 let url = resolve_lib_url(&lib, "a/b/1.0", "b-1.0.jar");
279 assert!(url.contains("repo1.maven.org"));
280 assert!(url.contains("b-1.0.jar"));
281 }
282}