Skip to main content

hexomc_lib/install/
fabric.rs

1use crate::{
2    download::{download_batch, DownloadTask},
3    error::Result,
4    install::vanilla::InstanceConfig,
5};
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9const FABRIC_META_BASE: &str = "https://meta.fabricmc.net/v2";
10
11#[derive(Debug, Deserialize, Serialize, Clone)]
12pub struct FabricLoaderVersion {
13    pub version: String,
14    pub stable: bool,
15}
16
17#[derive(Debug, Deserialize, Serialize, Clone)]
18#[serde(rename_all = "camelCase")]
19pub struct FabricProfileJson {
20    pub main_class: String,
21    pub arguments: FabricArguments,
22    pub libraries: Vec<FabricLibrary>,
23}
24
25#[derive(Debug, Deserialize, Serialize, Clone)]
26pub struct FabricArguments {
27    pub jvm: Option<Vec<String>>,
28    pub game: Option<Vec<String>>,
29}
30
31#[derive(Debug, Deserialize, Serialize, Clone)]
32pub struct FabricLibrary {
33    pub name: String,
34    pub url: String,
35}
36
37pub async fn get_fabric_loader_versions(mc_version: &str) -> Result<Vec<FabricLoaderVersion>> {
38    let url = format!("{}/versions/loader/{}", FABRIC_META_BASE, mc_version);
39    let client = reqwest::Client::new();
40
41    #[derive(Deserialize)]
42    struct Entry {
43        loader: FabricLoaderVersion,
44    }
45
46    let entries: Vec<Entry> = client.get(&url).send().await?.json().await?;
47    Ok(entries.into_iter().map(|e| e.loader).collect())
48}
49
50async fn fetch_fabric_profile(mc_version: &str, loader_version: &str) -> Result<FabricProfileJson> {
51    let url = format!(
52        "{}/versions/loader/{}/{}/profile/json",
53        FABRIC_META_BASE, mc_version, loader_version
54    );
55    let client = reqwest::Client::new();
56    let profile = client
57        .get(&url)
58        .send()
59        .await?
60        .json::<FabricProfileJson>()
61        .await?;
62    Ok(profile)
63}
64
65pub async fn install_fabric(
66    mc_version: &str,
67    loader_version: &str,
68    instance_name: &str,
69    base_dir: &Path,
70) -> Result<()> {
71    let instance_dir = base_dir.join("instance").join(instance_name);
72    let lib_dir = base_dir.join("libraries");
73
74    let mut config = InstanceConfig::load(&instance_dir).await?;
75    let profile = fetch_fabric_profile(mc_version, loader_version).await?;
76
77    let mut tasks: Vec<DownloadTask> = Vec::new();
78    let mut new_libs = Vec::new();
79
80    for fab_lib in &profile.libraries {
81        let parsed = parse_fabric_lib(fab_lib)?;
82
83        let artifact_path = lib_dir.join(&parsed.local_path);
84        let sha1 = fetch_text(&parsed.sha1_url).await.unwrap_or_default();
85
86        tasks.push(
87            DownloadTask::new(&parsed.jar_url, &artifact_path).with_sha1(sha1.trim().to_string()),
88        );
89
90        new_libs.push(crate::install::vanilla::LibEntry {
91            name: fab_lib.name.clone(),
92            path: artifact_path,
93            sha1: sha1.trim().to_string(),
94        });
95    }
96
97    download_batch(tasks, 8, |_, _| {}).await?;
98
99    // Prepend Fabric's JVM args.
100    if let Some(jvm_args) = &profile.arguments.jvm {
101        let trimmed: Vec<String> = jvm_args.iter().map(|s| s.trim().to_string()).collect();
102        let mut new_args = trimmed;
103        new_args.extend(config.start_args.drain(..));
104        config.start_args = new_args;
105    }
106
107    if let Some(game_args) = &profile.arguments.game {
108        config.start_args.extend(game_args.iter().cloned());
109    }
110
111    config.main_class = profile.main_class.clone();
112    config.loader_type = crate::install::vanilla::LoaderType::Fabric;
113
114    // Prepend the new libs so Fabric takes precedence.
115    new_libs.extend(config.lib_list.drain(..));
116    config.lib_list = new_libs;
117
118    config.save(&instance_dir).await?;
119    Ok(())
120}
121
122struct FabricLibParsed {
123    jar_url: String,
124    sha1_url: String,
125    local_path: std::path::PathBuf,
126}
127
128fn parse_fabric_lib(lib: &FabricLibrary) -> Result<FabricLibParsed> {
129    let parts: Vec<&str> = lib.name.split(':').collect();
130    let group = parts[0].replace('.', "/");
131    let artifact = parts[1];
132    let version = parts[2];
133
134    let base = format!("{}{}/{}/{}", lib.url, group, artifact, version);
135    let jar_name = format!("{}-{}.jar", artifact, version);
136    let jar_url = format!("{}/{}", base, jar_name);
137    let sha1_url = format!("{}/{}.sha1", base, jar_name);
138
139    let local_path = std::path::PathBuf::from(&group)
140        .join(artifact)
141        .join(version)
142        .join(&jar_name);
143
144    Ok(FabricLibParsed {
145        jar_url,
146        sha1_url,
147        local_path,
148    })
149}
150
151async fn fetch_text(url: &str) -> Result<String> {
152    let client = reqwest::Client::new();
153    let text = client.get(url).send().await?.text().await?;
154    Ok(text)
155}