diff --git a/helixlauncher-meta/src/component.rs b/helixlauncher-meta/src/component.rs index 364ab26..7a20cba 100644 --- a/helixlauncher-meta/src/component.rs +++ b/helixlauncher-meta/src/component.rs @@ -19,7 +19,7 @@ pub enum OsName { Windows, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)] pub struct ComponentDependency { pub id: String, #[serde(skip_serializing_if = "Option::is_none", default)] @@ -131,6 +131,15 @@ pub enum MinecraftArgument { }, } +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Dependencies { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub requires: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub conflicts: Vec, + pub optional: Vec, +} + #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug)] #[serde(deny_unknown_fields)] @@ -138,13 +147,12 @@ pub struct Component { pub format_version: u32, pub id: String, pub version: String, + pub dependencies: Dependencies, #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub requires: Vec, + pub provides: Vec, #[serde(skip_serializing_if = "BTreeSet::is_empty", default)] pub traits: BTreeSet, pub assets: Option, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub conflicts: Vec, pub downloads: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub jarmods: Vec, 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/forge.rs b/src/forge.rs index 0e73f20..654d67b 100644 --- a/src/forge.rs +++ b/src/forge.rs @@ -99,13 +99,17 @@ fn process_version(file: &fs::DirEntry, out_base: &Path) -> Result Result<()> { + let out_base = Path::new("out/net.fabricmc.intermediary"); + fs::create_dir_all(out_base)?; + + let mut index: Index = vec![]; + + for version in get_versions(client).await? { + let library = crate::Library { + name: GradleSpecifier::from_str(&format!("net.fabricmc:intermediary:{version}")) + .unwrap(), + url: "https://maven.fabricmc.net/".into(), + }; + let downloads = vec![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 classpath = vec![ConditionalClasspathEntry::All(library.name)]; + + let component = Component { + format_version: 1, + assets: None, + dependencies: Dependencies { + requires: vec![ComponentDependency { + id: "net.minecraft".into(), + version: Some(version.clone()), + }], + conflicts: vec![], + optional: vec![], + }, + provides: vec![], + id: "net.fabricmc.intermediary".into(), + jarmods: vec![], + natives: vec![], + release_time, + version, + traits: BTreeSet::new(), + game_jar: None, + main_class: None, + 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_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, +} diff --git a/src/main.rs b/src/main.rs index 087c58d..99750c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,9 +6,14 @@ #![deny(rust_2018_idioms)] use anyhow::Result; +use helixlauncher_meta::{component::Hash, util::GradleSpecifier}; +use reqwest::Client; +use serde::Deserialize; mod forge; +mod intermediary; mod mojang; +mod quilt; #[tokio::main] async fn main() -> Result<()> { @@ -18,5 +23,42 @@ async fn main() -> Result<()> { mojang::process()?; + // forge::process()?; + + quilt::process(&client).await?; + + intermediary::process(&client).await?; + 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(Deserialize, Debug)] +struct Library { + name: GradleSpecifier, + url: String, +} diff --git a/src/mojang.rs b/src/mojang.rs index 381e7bd..92d534e 100644 --- a/src/mojang.rs +++ b/src/mojang.rs @@ -12,7 +12,7 @@ use anyhow::{bail, ensure, Context, Result}; use chrono::{DateTime, Utc}; use data_encoding::HEXLOWER; use futures::{StreamExt, TryStreamExt}; -use helix::component::{ConditionFeature, MinecraftArgument}; +use helix::component::{ConditionFeature, MinecraftArgument, Dependencies}; use indexmap::{IndexMap, IndexSet}; use lazy_static::lazy_static; use maven_version::Maven3ArtifactVersion; @@ -657,8 +657,8 @@ pub fn process_version( traits, assets: version.asset_index.map(|a| a.into()), version: version.id.to_owned(), - requires: vec![], // TODO: lwjgl 2 (deal with that later) - conflicts: vec![], + dependencies: Dependencies::default(), // TODO: lwjgl 2 (deal with that later) + provides: vec![], downloads: downloads.into_values().collect(), classpath: classpath.into_iter().collect(), natives: natives.into_iter().collect(), diff --git a/src/quilt.rs b/src/quilt.rs new file mode 100644 index 0000000..178487d --- /dev/null +++ b/src/quilt.rs @@ -0,0 +1,152 @@ +use std::{collections::BTreeSet, fs, path::Path, str::FromStr}; + +use anyhow::{Context, Result}; +use chrono::{TimeZone, Utc}; +use helixlauncher_meta::{ + component::{Component, ComponentDependency, ConditionalClasspathEntry, Download, Dependencies}, + index::Index, + util::GradleSpecifier, +}; +use reqwest::Client; +use serde::Deserialize; + +use crate::Library; +pub async fn process(client: &Client) -> Result<()> { + let out_base = Path::new("out/org.quiltmc.quilt-loader"); + fs::create_dir_all(out_base)?; + + let mut index: Index = vec![]; + + for loader_version in get_loader_versions(client).await? { + if loader_version == "0.17.5-beta.4" { + // This version's meta is very broken and I hate it + continue; + } + + 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 library = crate::Library { + name: GradleSpecifier::from_str(&format!("org.quiltmc:quilt-loader:{loader_version}")) + .unwrap(), + url: "https://maven.quiltmc.org/repository/release/".into(), + }; + let mut downloads = vec![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(), + }]; + let mut classpath = vec![ConditionalClasspathEntry::All(library.name)]; + for library in response.libraries.common { + downloads.push(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(), + }); + classpath.push(ConditionalClasspathEntry::All(library.name)) + } + + let component = Component { + format_version: 1, + assets: None, + provides: vec![], + id: "org.quiltmc.quilt-loader".into(), + jarmods: vec![], + natives: vec![], + release_time, + version: loader_version, + traits: BTreeSet::new(), + dependencies: Dependencies { + conflicts: vec![], + optional: vec![], + requires: vec![ + ComponentDependency { + id: "net.minecraft".into(), + version: None, + }, + ComponentDependency { + id: "net.fabricmc.intermediary".into(), + version: None, + }, + ], + }, + game_jar: None, + main_class: Some(response.mainClass.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(Deserialize, Debug)] +struct Libraries { + client: Vec, + common: Vec, + server: Vec, +} + +#[derive(Deserialize, Debug)] +struct MainClass { + client: String, + server: String, + serverLauncher: Option, +} + +#[derive(Deserialize, Debug)] +struct LoaderMeta { + version: i32, + libraries: Libraries, + mainClass: MainClass, +}