349 lines
10 KiB
Rust
349 lines
10 KiB
Rust
//! Downloads Minecraft metadata to be launched
|
|
|
|
use futures::prelude::*;
|
|
use interpulse as ip;
|
|
use tokio::sync::OnceCell;
|
|
|
|
use interpulse::api::minecraft::{
|
|
Asset, AssetsIndex, Library, Os, Version as GameVersion, VersionInfo as GameVersionInfo,
|
|
};
|
|
use interpulse::api::modded::LoaderVersion;
|
|
|
|
use crate::proxy::send::send_ingress;
|
|
use crate::proxy::utils::ingress_try_for_each;
|
|
use crate::proxy::IngressId;
|
|
use crate::store::State;
|
|
use crate::utils::http::{fetch, fetch_json, write};
|
|
use onelauncher_utils::io;
|
|
use onelauncher_utils::platform::OsExt;
|
|
|
|
#[tracing::instrument(skip(st, version))]
|
|
pub async fn download_minecraft(
|
|
st: &State,
|
|
version: &GameVersionInfo,
|
|
ingress: &IngressId,
|
|
java_arch: &str,
|
|
force: bool,
|
|
updated: bool,
|
|
) -> crate::Result<()> {
|
|
tracing::info!("downloading minecraft version {}", version.id);
|
|
let assets_index = download_assets_index(st, version, Some(ingress), force).await?;
|
|
let processors = if version.processors.as_ref().is_some_and(|p| !p.is_empty()) {
|
|
25.0
|
|
} else {
|
|
40.0
|
|
};
|
|
|
|
tokio::try_join! {
|
|
download_client(st, version, Some(ingress), force),
|
|
download_assets(st, version.assets == "legacy", &assets_index, Some(ingress), processors, force),
|
|
download_libraries(st, version.libraries.as_slice(), &version.id, Some(ingress), processors, java_arch, force, updated)
|
|
}?;
|
|
|
|
tracing::info!("downloaded minecraft version {}", version.id);
|
|
Ok(())
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
|
|
#[onelauncher_macros::memory]
|
|
pub async fn download_version_info(
|
|
st: &State,
|
|
version: &GameVersion,
|
|
loader: Option<&LoaderVersion>,
|
|
force: Option<bool>,
|
|
ingress: Option<&IngressId>,
|
|
) -> crate::Result<GameVersionInfo> {
|
|
let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
|
|
tracing::debug!("loading minecraft version info for minecraft version {version_id}");
|
|
let path = st
|
|
.directories
|
|
.version_dir(&version_id)
|
|
.await
|
|
.join(format!("{version_id}.json"));
|
|
let result = if path.exists() && !force.unwrap_or(false) {
|
|
io::read(path)
|
|
.err_into::<crate::Error>()
|
|
.await
|
|
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
|
} else {
|
|
tracing::info!(
|
|
"downloading minecraft version info for minecraft version {}",
|
|
&version_id
|
|
);
|
|
let mut info = fetch_json(
|
|
reqwest::Method::GET,
|
|
&version.url,
|
|
None,
|
|
None,
|
|
&st.fetch_semaphore,
|
|
)
|
|
.await?;
|
|
|
|
if let Some(loader) = loader {
|
|
let partial: ip::api::modded::PartialVersionInfo = fetch_json(
|
|
reqwest::Method::GET,
|
|
&loader.url,
|
|
None,
|
|
None,
|
|
&st.fetch_semaphore,
|
|
)
|
|
.await?;
|
|
info = ip::api::modded::merge_partial_version(partial, info);
|
|
}
|
|
|
|
info.id.clone_from(&version_id);
|
|
|
|
write(&path, &serde_json::to_vec(&info)?, &st.io_semaphore).await?;
|
|
Ok(info)
|
|
}?;
|
|
|
|
if let Some(ingress) = ingress {
|
|
send_ingress(ingress, 5.0, None).await?;
|
|
}
|
|
|
|
tracing::debug!("loaded minecraft version info for minecraft version {version_id}");
|
|
Ok(result)
|
|
}
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
#[onelauncher_macros::memory]
|
|
pub async fn download_assets_index(
|
|
st: &State,
|
|
version: &GameVersionInfo,
|
|
ingress: Option<&IngressId>,
|
|
force: bool,
|
|
) -> crate::Result<AssetsIndex> {
|
|
tracing::debug!("loading assets index");
|
|
let path = st
|
|
.directories
|
|
.index_dir()
|
|
.await
|
|
.join(format!("{}.json", &version.asset_index.id));
|
|
|
|
let result = if path.exists() && !force {
|
|
io::read(path)
|
|
.err_into::<crate::Error>()
|
|
.await
|
|
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
|
} else {
|
|
let index = fetch_json(
|
|
reqwest::Method::GET,
|
|
&version.asset_index.url,
|
|
None,
|
|
None,
|
|
&st.fetch_semaphore,
|
|
)
|
|
.await?;
|
|
write(&path, &serde_json::to_vec(&index)?, &st.io_semaphore).await?;
|
|
tracing::info!("downloaded assets index");
|
|
Ok(index)
|
|
}?;
|
|
|
|
if let Some(ingress) = ingress {
|
|
send_ingress(ingress, 5.0, None).await?;
|
|
}
|
|
|
|
tracing::debug!("loaded assets index");
|
|
Ok(result)
|
|
}
|
|
|
|
#[tracing::instrument(skip(st, index))]
|
|
#[onelauncher_macros::memory]
|
|
pub async fn download_assets(
|
|
st: &State,
|
|
legacy: bool,
|
|
index: &AssetsIndex,
|
|
ingress: Option<&IngressId>,
|
|
ingress_amount: f64,
|
|
force: bool,
|
|
) -> crate::Result<()> {
|
|
tracing::debug!("loading minecraft assets");
|
|
let num_futs = index.objects.len();
|
|
let assets = stream::iter(index.objects.iter()).map(Ok::<(&String, &Asset), crate::Error>);
|
|
|
|
ingress_try_for_each(
|
|
assets,
|
|
None,
|
|
ingress,
|
|
ingress_amount,
|
|
num_futs,
|
|
None,
|
|
|(name, asset)| async move {
|
|
let hash = &asset.hash;
|
|
let resources = st.directories.object_dir(hash).await;
|
|
let url = format!(
|
|
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
|
|
sub_hash = &hash[..2]
|
|
);
|
|
let fetch_cell = OnceCell::<bytes::Bytes>::new();
|
|
tokio::try_join! {
|
|
async {
|
|
if !resources.exists() || force {
|
|
let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?;
|
|
write(&resources, resource, &st.io_semaphore).await?;
|
|
tracing::trace!("fetched asset resource with hash {hash}");
|
|
}
|
|
Ok::<_, crate::Error>(())
|
|
},
|
|
async {
|
|
let resources = st.directories.legacy_assets_dir().await.join(
|
|
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
|
|
);
|
|
|
|
if legacy && !resources.exists() || force {
|
|
let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?;
|
|
write(&resources, resource, &st.io_semaphore).await?;
|
|
tracing::trace!("fetched legacy asset resource with hash {hash}");
|
|
}
|
|
Ok::<_, crate::Error>(())
|
|
},
|
|
}?;
|
|
|
|
tracing::trace!("loaded asset resource with hash {hash}");
|
|
Ok(())
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
tracing::debug!("loaded minecraft assets");
|
|
Ok(())
|
|
}
|
|
|
|
#[tracing::instrument(skip(st, libraries))]
|
|
#[onelauncher_macros::memory]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn download_libraries(
|
|
st: &State,
|
|
libraries: &[Library],
|
|
version: &str,
|
|
ingress: Option<&IngressId>,
|
|
ingress_amount: f64,
|
|
java_arch: &str,
|
|
force: bool,
|
|
updated: bool,
|
|
) -> crate::Result<()> {
|
|
tracing::debug!("loading minecraft libraries");
|
|
|
|
tokio::try_join! {
|
|
io::create_dir_all(st.directories.libraries_dir().await),
|
|
io::create_dir_all(st.directories.version_natives_dir(version).await)
|
|
}?;
|
|
|
|
let num_files = libraries.len();
|
|
// TODO: figure out why rustfmt won't format this (it's not a macro...)
|
|
ingress_try_for_each(
|
|
stream::iter(libraries.iter())
|
|
.map(Ok::<&Library, crate::Error>),
|
|
None,
|
|
ingress,
|
|
ingress_amount,
|
|
num_files,
|
|
None,
|
|
|lib| async move {
|
|
if let Some(rules) = &lib.rules {
|
|
if !super::rules(rules, java_arch, updated) {
|
|
tracing::trace!("skipped library {} due to rule", &lib.name);
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
if !lib.downloadable {
|
|
tracing::trace!("skipped non-downloadable library {}", &lib.name);
|
|
return Ok(());
|
|
}
|
|
|
|
tokio::try_join! {
|
|
async {
|
|
let artifact_path = ip::utils::get_path_from_artifact(&lib.name)?;
|
|
let path = st.directories.libraries_dir().await.join(&artifact_path);
|
|
|
|
if path.exists() && !force {
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(ip::api::minecraft::LibraryDownloads {
|
|
artifact: Some(ref artifact), ..
|
|
}) = lib.downloads {
|
|
if !artifact.url.is_empty() {
|
|
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore).await?;
|
|
write(&path, &bytes, &st.io_semaphore).await?;
|
|
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
|
|
return Ok::<_, crate::Error>(());
|
|
}
|
|
}
|
|
|
|
let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat();
|
|
let bytes = fetch(&url, None, &st.fetch_semaphore).await?;
|
|
write(&path, &bytes, &st.io_semaphore).await?;
|
|
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
|
|
Ok::<_, crate::Error>(())
|
|
},
|
|
async {
|
|
if let Some((os_key, classifiers)) = None.or_else(|| Some((
|
|
lib.natives.as_ref()?.get(&Os::native_arch(java_arch))?,
|
|
lib.downloads.as_ref()?.classifiers.as_ref()?
|
|
))) {
|
|
let parsed = os_key.replace("${arch}", crate::constants::ARCH_WIDTH);
|
|
if let Some(native) = classifiers.get(&parsed) {
|
|
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?;
|
|
let reader = std::io::Cursor::new(&data);
|
|
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
|
match archive.extract(st.directories.version_natives_dir(version).await) {
|
|
Ok(()) => tracing::debug!("downloaded native {}", &lib.name),
|
|
Err(err) => tracing::error!("failed to download native {}: {}", &lib.name, err)
|
|
}
|
|
} else {
|
|
tracing::error!("failed to extract native {}", &lib.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}?;
|
|
|
|
tracing::debug!("loaded minecraft library {}", lib.name);
|
|
Ok(())
|
|
}
|
|
).await?;
|
|
|
|
tracing::debug!("downloaded all minecraft libraries");
|
|
Ok(())
|
|
}
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
#[onelauncher_macros::memory]
|
|
pub async fn download_client(
|
|
st: &State,
|
|
version: &GameVersionInfo,
|
|
ingress: Option<&IngressId>,
|
|
force: bool,
|
|
) -> crate::Result<()> {
|
|
let version_id = &version.id;
|
|
tracing::debug!("loading minecraft client for minecraft version {version_id}");
|
|
|
|
let client = version
|
|
.downloads
|
|
.get(&ip::api::minecraft::DownloadType::Client)
|
|
.ok_or_else(|| anyhow::anyhow!("no client downloads exist for {version_id}"))?;
|
|
|
|
let path = st
|
|
.directories
|
|
.version_dir(version_id)
|
|
.await
|
|
.join(format!("{version_id}.jar"));
|
|
|
|
if !path.exists() || force {
|
|
let result = fetch(&client.url, Some(&client.sha1), &st.fetch_semaphore).await?;
|
|
|
|
write(&path, &result, &st.io_semaphore).await?;
|
|
tracing::trace!("fetched minecraft client version {version_id}");
|
|
}
|
|
|
|
if let Some(ingress) = ingress {
|
|
send_ingress(ingress, 9.0, None).await?;
|
|
}
|
|
|
|
tracing::debug!("downloaded client for minecraft version {version_id}");
|
|
Ok(())
|
|
}
|