OneLauncher-Cracked/packages/core/src/api/game/metadata.rs
2025-06-10 20:00:41 +02:00

444 lines
12 KiB
Rust

use std::path::PathBuf;
use futures::{StreamExt, stream};
use interpulse::api::minecraft::{AssetsIndex, DownloadType, Library, Os, Version, VersionInfo};
use interpulse::api::modded::LoaderVersion;
use onelauncher_entity::loader::GameLoader;
use reqwest::Method;
use crate::api::ingress::{init_ingress, IngressSendExt};
use crate::error::{LauncherError, LauncherResult};
use crate::store::ingress::{IngressType, SubIngress, SubIngressExt};
use crate::store::metadata::MetadataError;
use crate::store::{Core, Dirs, State};
use crate::utils::crypto::HashAlgorithm;
use crate::utils::os_ext::OsExt;
use crate::utils::{http, io};
pub async fn download_minecraft_ingressed(
version: &VersionInfo,
java_arch: &str,
force: Option<bool>,
) -> LauncherResult<()> {
let id = init_ingress(
IngressType::MinecraftDownload,
&format!("downloading Minecraft {}", version.id),
100.0,
)
.await?;
download_minecraft(version, java_arch, Some(&SubIngress::new(&id, 100.0)), force).await
}
// MARK: Main
#[tracing::instrument(skip_all)]
pub async fn download_minecraft(
version: &VersionInfo,
java_arch: &str,
sub_ingress: Option<&SubIngress<'_>>,
force: Option<bool>,
) -> LauncherResult<()> {
let asset_index = download_assets_index(version, sub_ingress, force).await?;
tokio::try_join! {
download_assets(version.assets == "legacy", asset_index, sub_ingress, force),
download_client(version, sub_ingress, force),
download_libraries(version.id.clone(), version.libraries.clone(), java_arch.to_string(), sub_ingress, force),
}?;
Ok(())
}
// MARK: Version Info
#[tracing::instrument(skip(ingress, force))]
pub async fn download_version_info(
version: &Version,
loader: Option<&LoaderVersion>,
ingress: Option<&SubIngress<'_>>,
force: Option<bool>,
) -> LauncherResult<VersionInfo> {
const TASKS: f64 = 3.33;
tracing::debug!("loading version info for version {}", version.id);
let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
ingress.set_ingress_message("fetching version info").await?;
tracing::debug!("loading minecraft version info for minecraft version {version_id}");
let path = Dirs::get_versions_dir()
.await?
.join(&version_id)
.join(format!("{version_id}.json"));
let result = if path.exists() && !force.unwrap_or(false) {
let data = io::read(path).await?;
serde_json::from_slice(&data)?
} else {
tracing::info!(
"downloading minecraft version info for minecraft version {}",
&version_id
);
let mut info =
http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress)
.await?;
if let Some(loader) = loader {
let partial: interpulse::api::modded::PartialVersionInfo =
http::fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress)
.await?;
info = interpulse::api::modded::merge_partial_version(partial, info);
}
info.id.clone_from(&version_id);
io::create_dir_all(
&path
.parent()
.unwrap_or_else(|| panic!("couldn't get version path parent")),
)
.await?;
io::write(&path, &serde_json::to_vec(&info)?).await?;
info
};
ingress.send_ingress(ingress.ingress_total().map(|i| i / TASKS).unwrap_or_default()).await?;
tracing::debug!("loaded minecraft version info for minecraft version {version_id}");
Ok(result)
}
// MARK: Assets Index
#[tracing::instrument(skip_all)]
pub async fn download_assets_index(
version: &VersionInfo,
ingress: Option<&SubIngress<'_>>,
force: Option<bool>,
) -> LauncherResult<AssetsIndex> {
tracing::debug!("loading assets index for version {}", version.id);
let path = Dirs::get_assets_index_dir()
.await?
.join(format!("{}.json", version.asset_index.id));
let ingress_step = ingress.ingress_total().map(|i| i / 2.0).unwrap_or_default();
ingress.set_ingress_message("fetching assets index").await?;
if path.exists() && !force.unwrap_or(false) {
match io::read_json::<AssetsIndex>(&path).await {
Ok(data) => {
ingress.send_ingress(ingress_step).await?;
return Ok(data);
}
Err(err) => {
tracing::error!("failed to read assets index from cache: {err:?}, downloading...",);
}
}
}
let data = serde_json::from_slice(
&http::download(
Method::GET,
&version.asset_index.url,
path,
None,
ingress.map(|i| SubIngress::from_sub(i, ingress_step)).as_ref(),
)
.await?,
)?;
tracing::debug!("loaded assets index for version {}", version.id);
Ok(data)
}
// MARK: Assets
/// Downloads the assets for a given asset index, returns the number of failed downloads
#[tracing::instrument(skip_all)]
pub async fn download_assets<'a>(
legacy: bool,
assets_index: AssetsIndex,
ingress: Option<&SubIngress<'_>>,
force: Option<bool>,
) -> LauncherResult<usize> {
tracing::debug!("loading asssets");
let len = assets_index.objects.len();
let dir = if legacy {
Dirs::get_legacy_assets_dir().await
} else {
Dirs::get_assets_object_dir().await
}?;
io::create_dir_all(&dir).await?;
ingress.set_ingress_message("fetching assets").await?;
let requests = stream::iter(assets_index.objects.into_iter().map(|(name, asset)| {
let dir = dir.clone();
async move {
let hash = &asset.hash;
let subhash = &hash[0..2];
let path = if legacy {
dir.join(name.replace('/', std::path::MAIN_SEPARATOR_STR))
} else {
dir.join(subhash).join(hash)
};
if !path.exists() || force.unwrap_or(false) {
http::download_advanced(
Method::GET,
&format!("https://resources.download.minecraft.net/{subhash}/{hash}"),
path,
None,
None,
Some((HashAlgorithm::Sha1, hash)),
ingress.ingress_sub(|total| total / len as f64).as_ref(),
)
.await
.map(|_| ())
} else {
// TODO: Possibly check hash? (not sure if this is a good idea here)
Ok(())
}
}
}))
.buffer_unordered(Core::get().fetch_attempts.min(7))
.collect::<Vec<_>>();
let mut failed = 0;
for res in requests.await {
if let Err(err) = res {
tracing::error!("failed to download asset: {err:?}");
failed += 1;
}
}
tracing::debug!("loaded assets");
Ok(failed)
}
// MARK: Client
pub async fn download_client(
version: &VersionInfo,
ingress: Option<&SubIngress<'_>>,
force: Option<bool>,
) -> LauncherResult<PathBuf> {
tracing::debug!("loading client for version {}", version.id);
let client = version
.downloads
.get(&DownloadType::Client)
.ok_or_else(|| anyhow::anyhow!("no client downloads exist for {}", &version.id))?;
let path = Dirs::get_versions_dir()
.await?
.join(version.id.clone())
.join(format!("{}.jar", version.id));
if path.exists() && !force.unwrap_or(false) {
let calculated_hash = HashAlgorithm::Sha1
.hash_file(&path)
.await
.map_err(|_| anyhow::anyhow!("failed to hash file"))?;
if client.sha1 == calculated_hash {
tracing::debug!("client already exists, skipping download");
return Ok(path);
}
tracing::warn!("client hash mismatch, redownloading");
}
http::download(
Method::GET,
&client.url,
&path,
Some((HashAlgorithm::Sha1, &client.sha1)),
ingress,
)
.await?;
Ok(path)
}
// MARK: Libraries
/// Downloads the libraries and returns the number of failed downloads
pub async fn download_libraries(
version: String,
libraries: Vec<Library>,
java_arch: String,
ingress: Option<&SubIngress<'_>>,
force: Option<bool>,
) -> LauncherResult<usize> {
tracing::debug!("loading libraries for version {}", version);
let lib_dir = Dirs::get_libraries_dir().await?;
let natives_dest = Dirs::get_natives_dir().await?.join(version);
io::create_dir_all(&lib_dir).await?;
io::create_dir_all(&natives_dest).await?;
let num_files = libraries.len();
let ingress = ingress.ingress_sub(|total| total / num_files as f64);
let ingress = ingress.as_ref();
ingress.set_ingress_message("fetching libraries").await?;
let requests = stream::iter(libraries.into_iter().map(|lib| {
let lib_dir = lib_dir.clone();
let natives_dest = natives_dest.clone();
let java_arch = java_arch.clone();
async move {
if let Some(rules) = &lib.rules
&& !super::rules::validate_rules(rules, &java_arch, lib.natives.is_some()) {
tracing::debug!("skipping library {} due to rules", lib.name);
return Ok::<(), LauncherError>(());
}
if !lib.downloadable {
tracing::debug!("skipping library {} due to downloadability", lib.name);
return Ok(());
}
let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?;
let path = lib_dir.join(&artifact_path);
if path.exists() && !force.unwrap_or(false) {
tracing::debug!("library {} is installed, skipping", &lib.name);
return Ok(());
}
tokio::try_join! {
async {
if let Some(interpulse::api::minecraft::LibraryDownloads {
artifact: Some(ref artifact), ..
}) = lib.downloads
&& !artifact.url.is_empty() {
http::download(
Method::GET,
&artifact.url,
&path,
Some((HashAlgorithm::Sha1, &artifact.sha1)),
ingress,
).await?;
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
return Ok::<_, LauncherError>(());
}
let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat();
http::download(
Method::GET,
&url,
&path,
None,
ingress,
).await?;
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
Ok::<_, LauncherError>(())
},
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) {
tracing::trace!("found native library {}", &lib.name);
let data = http::fetch_advanced(
Method::GET,
&native.url,
None,
Some((HashAlgorithm::Sha1, &native.sha1)),
None,
ingress,
).await?;
io::unzip_bytes_filtered(
data.to_vec(),
Some(|name: &str| !name.starts_with("META-INF")),
&natives_dest
).await?;
tracing::trace!("extracted native {} to path {:?}", &lib.name, &natives_dest);
}
}
Ok(())
}
}?;
Ok(())
}
})).buffer_unordered(7).collect::<Vec<_>>();
let mut failed = 0;
for res in requests.await {
if let Err(err) = res {
tracing::error!("failed to download asset: {err:?}");
failed += 1;
}
}
Ok(failed)
}
// MARK: Get Loader
/// Gets the loader version for a given Minecraft version and loader
/// If `loader_version` is `None`, it will return the latest stable version else
/// it will return the specified version if found
#[tracing::instrument]
pub async fn get_loader_version(
mc_version: &str,
loader: GameLoader,
loader_version: Option<&str>,
) -> LauncherResult<Option<LoaderVersion>> {
if loader == GameLoader::Vanilla {
return Ok(None);
}
let state = State::get().await?;
let metadata = state.metadata.read().await;
let manifest = metadata.get_modded(&loader)?;
let Some(loaders) = manifest.game_versions.iter().find(|it| {
it.id
.replace(interpulse::api::modded::DUMMY_REPLACE_STRING, mc_version)
== mc_version
}) else {
return Err(MetadataError::NoMatchingVersion.into());
};
let loader_version = loaders
.loaders
.iter()
.find(|it| loader_version.map_or(it.stable, |version| it.id == version))
.or_else(|| loaders.loaders.first())
.cloned()
.ok_or(MetadataError::NoMatchingLoader)?;
Ok(Some(loader_version))
}
// MARK: Get Versions
/// Gets the list of Minecraft versions from the metadata store
#[tracing::instrument]
pub async fn get_game_versions() -> LauncherResult<Vec<Version>> {
let state = State::get().await?;
let metadata = state.metadata.read().await;
let manifest = metadata.get_vanilla()?;
Ok(manifest.versions.clone())
}
/// Gets the list of modded loaders and their versions from the metadata store
#[tracing::instrument]
pub async fn get_loaders_for_version(mc_version: &str) -> LauncherResult<Vec<GameLoader>> {
let state = State::get().await?;
let mut metadata = state.metadata.write().await;
Ok(metadata.get_loaders_for_version(mc_version).await?)
}