diff --git a/helixlauncher-meta/src/util.rs b/helixlauncher-meta/src/util.rs index db29335..c463b34 100644 --- a/helixlauncher-meta/src/util.rs +++ b/helixlauncher-meta/src/util.rs @@ -79,6 +79,24 @@ impl Display for GradleSpecifier { } } +impl GradleSpecifier { + pub fn to_url(&self, base_repo: &str) -> String { + format!( + "{}{}/{}/{}/{}-{}{}.{}", + base_repo, + self.group.replace(".", "/"), + self.artifact, + self.version, + self.artifact, + self.version, + self.classifier + .as_ref() + .map_or("".to_string(), |it| "-".to_string() + &it), + self.extension + ) + } +} + cfg_if::cfg_if! { if #[cfg(windows)] { pub const CURRENT_OS: component::OsName = component::OsName::Windows; diff --git a/src/intermediary.rs b/src/intermediary.rs new file mode 100644 index 0000000..19ca3e8 --- /dev/null +++ b/src/intermediary.rs @@ -0,0 +1,151 @@ +use std::{collections::BTreeSet, fs, path::Path, str::FromStr}; + +use anyhow::Result; +use chrono::{DateTime, Utc}; +use futures::{stream, StreamExt, TryStreamExt}; +use helixlauncher_meta::{ + component::{Component, ComponentDependency, ConditionalClasspathEntry, Download}, + index::Index, + util::GradleSpecifier, +}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +use crate::{get_hash, get_size}; + +const CONCURRENT_FETCH_LIMIT: usize = 5; +pub async fn fetch(client: &Client) -> Result<()> { + let upstream_base = Path::new("upstream/intermediary"); + + fs::create_dir_all(&upstream_base).unwrap(); + + stream::iter(get_versions(client).await?) + .map(|version| async { fetch_version(version, client, &upstream_base).await }) + .buffer_unordered(CONCURRENT_FETCH_LIMIT) + .try_collect::<()>() + .await?; + Ok(()) +} + +async fn fetch_version(version: String, client: &Client, upstream_base: &Path) -> Result<()> { + let version_path = upstream_base.join(format!("{}.json", version)); + if version_path.try_exists()? { + return Ok(()); + } + + let library = crate::Library { + name: GradleSpecifier::from_str(&format!("net.fabricmc:intermediary:{version}")).unwrap(), + url: "https://maven.fabricmc.net/".into(), + }; + let download = Download { + name: library.name.clone(), + url: library.name.to_url(&library.url), + hash: get_hash(client, &library).await?, + size: get_size(client, &library).await?.try_into().unwrap(), + }; + + let release_time = DateTime::parse_from_rfc2822( + // TODO: This does one more request than necessary, should get_size or get_hash be merged into this? + client + .head(library.name.to_url(&library.url)) + .header("User-Agent", "helixlauncher-meta") + .send() + .await? + .headers() + .get("last-modified") + .expect("Cannot handle servers returning no last-modified") + .to_str()?, + ) + .expect(&format!( + "Error parsing last-modified header of {}", + library.name.to_url(&library.url) + )) + .into(); + + let download = DownloadWithReleaseTime { + download, + release_time, + }; + + fs::write(version_path, serde_json::to_string_pretty(&download)?)?; + + Ok(()) +} + +pub fn process() -> Result<()> { + let out_base = Path::new("out/net.fabricmc.intermediary"); + let upstream_base = Path::new("upstream/intermediary"); + fs::create_dir_all(out_base)?; + + let mut index: Index = vec![]; + + for version_meta in fs::read_dir(upstream_base)? { + let version_meta: DownloadWithReleaseTime = + serde_json::from_str(&fs::read_to_string(version_meta?.path())?)?; + + let classpath = vec![ConditionalClasspathEntry::All( + version_meta.download.name.clone(), + )]; + + let component = Component { + format_version: 1, + assets: None, + conflicts: vec![], + id: "net.fabricmc.intermediary".into(), + jarmods: vec![], + natives: vec![], + release_time: version_meta.release_time, + version: version_meta.download.name.version.clone(), + traits: BTreeSet::new(), + requires: vec![ComponentDependency { + id: "net.minecraft".into(), + version: Some(version_meta.download.name.version.clone()), + }], + game_jar: None, + main_class: None, + game_arguments: vec![], + classpath, + downloads: vec![version_meta.download], + }; + + fs::write( + out_base.join(format!("{}.json", component.version)), + serde_json::to_string_pretty(&component)?, + )?; + + index.push(component.into()); + } + + index.sort_by(|x, y| y.release_time.cmp(&x.release_time)); + + fs::write( + out_base.join("index.json"), + serde_json::to_string_pretty(&index)?, + )?; + + Ok(()) +} + +async fn get_versions(client: &Client) -> Result> { + let response: Vec = client + .get("https://meta.fabricmc.net/v2/versions/intermediary") + .header("User-Agent", "helixlauncher-meta") + .send() + .await? + .json() + .await?; + Ok(response.into_iter().map(|v| v.version).collect()) +} + +#[derive(Deserialize)] +struct IntermediaryVersionData { + // maven: GradleSpecifier, + version: String, + // stable: bool, +} + +#[derive(Serialize, Deserialize)] +struct DownloadWithReleaseTime { + download: Download, + release_time: DateTime, +} diff --git a/src/main.rs b/src/main.rs index 087c58d..9c5d34d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,17 +6,64 @@ #![deny(rust_2018_idioms)] use anyhow::Result; +use futures::try_join; +use helixlauncher_meta::{component::Hash, util::GradleSpecifier}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; mod forge; +mod intermediary; mod mojang; +mod quilt; #[tokio::main] async fn main() -> Result<()> { let client = reqwest::Client::new(); - mojang::fetch(&client).await?; + try_join!( + mojang::fetch(&client), + quilt::fetch(&client), + intermediary::fetch(&client), + )?; mojang::process()?; + // forge::process()?; + + quilt::process()?; + + intermediary::process()?; + Ok(()) } + +pub(crate) async fn get_hash(client: &Client, coord: &Library) -> Result { + Ok(Hash::SHA256( + client + .get(coord.name.to_url(&coord.url) + ".sha256") + .header("User-Agent", "helixlauncher-meta") + .send() + .await? + .text() + .await?, + )) +} + +pub(crate) async fn get_size(client: &Client, coord: &Library) -> Result { + Ok(client + .head(coord.name.to_url(&coord.url)) + .header("User-Agent", "helixlauncher-meta") + .send() + .await? + .headers() + .get("content-length") + .expect("Cannot handle servers returning no content length") + .to_str()? + .parse()?) +} + +#[derive(Serialize, Deserialize, Debug)] +struct Library { + name: GradleSpecifier, + url: String, +} diff --git a/src/quilt.rs b/src/quilt.rs new file mode 100644 index 0000000..86aa7fd --- /dev/null +++ b/src/quilt.rs @@ -0,0 +1,247 @@ +use std::{ + collections::BTreeSet, + fs, iter, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::{Context, Result}; +use chrono::{DateTime, TimeZone, Utc}; +use futures::{stream, StreamExt, TryStreamExt}; +use helixlauncher_meta::{ + component::{Component, ComponentDependency, ConditionalClasspathEntry, Download}, + index::Index, + util::GradleSpecifier, +}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +use crate::Library; + +const CONCURRENT_FETCH_LIMIT: usize = 5; + +pub async fn fetch(client: &Client) -> Result<()> { + let upstream_base = Path::new("upstream/quilt"); + let versions_base = upstream_base.join("versions"); + let downloads_base = upstream_base.join("downloads"); + + fs::create_dir_all(&versions_base).unwrap(); + fs::create_dir_all(&downloads_base).unwrap(); + + stream::iter(get_loader_versions(client).await?) + .map(|loader_version| async { + let version_meta = fetch_version(&loader_version, client, &versions_base).await?; + if let Some(version_meta) = version_meta { + fetch_downloads(loader_version, version_meta, client, &downloads_base).await + } else { + Ok(()) + } + }) + .buffer_unordered(CONCURRENT_FETCH_LIMIT) + .try_collect::<()>() + .await?; + Ok(()) +} + +async fn fetch_version( + loader_version: &String, + client: &Client, + versions_base: &PathBuf, +) -> Result> { + if loader_version == "0.17.5-beta.4" { + // This version's meta is very broken and I hate it + return Ok(None); + } + + let version_path = versions_base.join(format!("{}.json", loader_version)); + if version_path.try_exists()? { + return Ok(Some(serde_json::from_str(&fs::read_to_string( + version_path, + )?)?)); + } + + let response = client.get(format!("https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-loader/{loader_version}/quilt-loader-{loader_version}.json")) + .header("User-Agent", "helixlauncher-meta") + .send().await?; + + let release_time = Utc + .timestamp_millis_opt( + response + .headers() + .get("quilt-last-modified-timestamp") + .context("Error quilt did not provide release date in metadata")? + .to_str()? + .parse()?, + ) + .single() + .context("unable to parse release timestamp")?; + + let response: LoaderMeta = response.json().await?; + let response = LoaderMetaWithReleaseTime { + meta: response, + release_time, + }; + + serde_json::to_writer_pretty(fs::File::create(version_path)?, &response)?; + Ok(Some(response)) +} + +async fn fetch_downloads( + loader_version: String, + loader_meta: LoaderMetaWithReleaseTime, + client: &Client, + downloads_base: &PathBuf, +) -> Result<()> { + let downloads_path = downloads_base.join(format!("{}.json", loader_version)); + if downloads_path.try_exists()? { + return Ok(()); + } + + let libraries = loader_meta + .meta + .libraries + .common + .into_iter() + .chain(iter::once(crate::Library { + name: GradleSpecifier::from_str(&format!("org.quiltmc:quilt-loader:{loader_version}")) + .unwrap(), + url: "https://maven.quiltmc.org/repository/release/".into(), + })); + + let downloads = stream::iter(libraries) + .map(|library| library_to_download(client, library)) + .buffer_unordered(CONCURRENT_FETCH_LIMIT) + .try_collect::>() + .await?; + + serde_json::to_writer_pretty(fs::File::create(downloads_path)?, &downloads)?; + + Ok(()) +} + +async fn library_to_download(client: &Client, library: Library) -> Result { + Ok(Download { + name: library.name.clone(), + url: library.name.to_url(&library.url), + hash: crate::get_hash(client, &library).await?, + size: crate::get_size(client, &library).await?.try_into().unwrap(), + }) +} + +pub fn process() -> Result<()> { + let upstream_base = Path::new("upstream/quilt"); + let versions_base = upstream_base.join("versions"); + let downloads_base = upstream_base.join("downloads"); + let out_base = Path::new("out/org.quiltmc.quilt-loader"); + fs::create_dir_all(out_base)?; + + let mut index: Index = vec![]; + + for loader_meta in fs::read_dir(versions_base)? { + let loader_meta = loader_meta?; + let loader_version = loader_meta.file_name().clone().to_string_lossy() + [..loader_meta.file_name().len() - 5] + .to_string(); + let downloads: Vec = serde_json::from_str(&fs::read_to_string( + &downloads_base.join(loader_meta.file_name()), + )?)?; + let loader_meta: LoaderMetaWithReleaseTime = + serde_json::from_str(&fs::read_to_string(loader_meta.path())?)?; + + let classpath = downloads + .iter() + .map(|download| ConditionalClasspathEntry::All(download.name.clone())) + .collect(); + + let component = Component { + format_version: 1, + assets: None, + conflicts: vec![], + id: "org.quiltmc.quilt-loader".into(), + jarmods: vec![], + natives: vec![], + release_time: loader_meta.release_time, + version: loader_version, + traits: BTreeSet::new(), + requires: vec![ + ComponentDependency { + id: "net.minecraft".into(), + version: None, + }, + ComponentDependency { + id: "net.fabricmc.intermediary".into(), + version: None, + }, + ], + game_jar: None, + main_class: Some(loader_meta.meta.main_class.client), + game_arguments: vec![], + classpath, + downloads, + }; + + fs::write( + out_base.join(format!("{}.json", component.version)), + serde_json::to_string_pretty(&component)?, + )?; + + index.push(component.into()); + } + + index.sort_by(|x, y| y.release_time.cmp(&x.release_time)); + + fs::write( + out_base.join("index.json"), + serde_json::to_string_pretty(&index)?, + )?; + + Ok(()) +} + +async fn get_loader_versions(client: &Client) -> Result> { + let response: Vec = client + .get("https://meta.quiltmc.org/v3/versions/loader") + .header("User-Agent", "helixlauncher-meta") + .send() + .await? + .json() + .await?; + Ok(response.into_iter().map(|x| x.version).collect()) +} + +#[derive(Deserialize, Debug)] +struct LoaderVersionData { + // separator: String, + // build: i32, + // maven: GradleSpecifier, + version: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Libraries { + client: Vec, + common: Vec, + server: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +struct MainClass { + client: String, + server: String, + #[serde(rename = "serverLauncher")] + server_launcher: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +struct LoaderMeta { + version: i32, + libraries: Libraries, + #[serde(rename = "mainClass")] + main_class: MainClass, +} + +#[derive(Serialize, Deserialize, Debug)] +struct LoaderMetaWithReleaseTime { + meta: LoaderMeta, + release_time: DateTime, +}