use std::{borrow::Cow, collections::HashMap, path::Path};
use anyhow::Context;
use async_trait::async_trait;
use inner_future::{fs::create_dir_all, io::AsyncWriteExt};
use tracing::*;
use super::{
structs::{AssetIndexes, VersionManifest},
DownloadSource, Downloader,
};
use crate::{
download::VersionInfo,
prelude::*,
progress::Reporter,
version::structs::{Allowed, Library, VersionMeta},
};
#[async_trait]
pub trait VanillaDownloadExt: Sync {
async fn get_avaliable_vanilla_versions(&self) -> DynResult<VersionManifest>;
async fn download_vanilla_jar(&self, path: &str, save_path: &str, sha1: &str) -> DynResult;
async fn download_library(&self, sha1: String, path: String, save_path: &str) -> DynResult;
async fn download_libraries(
&self,
libraries: &[Library],
) -> DynResult<HashMap<String, Vec<String>>>;
async fn download_asset_index(
&self,
name: &str,
url: &str,
save_path: &str,
) -> DynResult<AssetIndexes>;
async fn download_asset(
&self,
sha1: &str,
name: &str,
save_path: &str,
is_pre: bool,
r: Option<impl Reporter>,
) -> DynResult;
async fn download_vanilla(
&self,
version_name: &str,
version_meta: &VersionMeta,
is_repair: bool,
) -> DynResult;
async fn install_vanilla(&self, version_name: &str, version_info: &VersionInfo) -> DynResult;
}
#[async_trait]
impl<R: Reporter> VanillaDownloadExt for Downloader<R> {
async fn get_avaliable_vanilla_versions(&self) -> DynResult<VersionManifest> {
let res = crate::http::retry_get_json(match self.source {
DownloadSource::Default => {
"https://piston-meta.mojang.com/mc/game/version_manifest.json"
}
DownloadSource::BMCLAPI => {
"https://bmclapi2.bangbang93.com/mc/game/version_manifest.json"
}
DownloadSource::MCBBS => "https://download.mcbbs.net/mc/game/version_manifest.json",
_ => "https://piston-meta.mojang.com/mc/game/version_manifest.json",
})
.await
.map_err(|e| anyhow::anyhow!("获取可用原版列表失败:{:?}", e))?;
Ok(res)
}
async fn download_vanilla_jar(&self, path: &str, save_path: &str, sha1: &str) -> DynResult {
let l = self.parallel_lock.acquire().await;
let r = self.reporter.sub();
r.add_max_progress(1.);
let name = &save_path[save_path.rfind(std::path::is_separator).unwrap_or(0) + 1..];
r.set_message(format!("正在下载原版 {name}"));
if std::path::Path::new(&save_path).is_file() {
if self.verify_data {
let mut file = inner_future::fs::OpenOptions::new()
.read(true)
.open(&save_path)
.await?;
let current_sha1 = crate::utils::get_data_sha1(&mut file).await?;
if sha1 == current_sha1 {
r.add_progress(1.);
return Ok(());
}
} else {
r.add_progress(1.);
return Ok(());
}
} else {
inner_future::fs::create_dir_all(
&save_path[..save_path
.rfind(std::path::is_separator)
.unwrap_or(save_path.len())],
)
.await?;
}
let path = path.parse::<url::Url>()?;
let uris = [
match self.source {
DownloadSource::Default => {
format!("https://launcher.mojang.com{}", path.path())
}
DownloadSource::BMCLAPI => {
format!("https://bmclapi2.bangbang93.com{}", path.path())
}
DownloadSource::MCBBS => format!("https://download.mcbbs.net{}", path.path()),
_ => format!("https://launcher.mojang.com{}", path.path()),
},
format!("https://bmclapi2.bangbang93.com{}", path.path()),
format!("https://download.mcbbs.net{}", path.path()),
format!("https://launcher.mojang.com{}", path.path()),
];
crate::http::download(&uris, save_path, 0)
.await
.map_err(|e| anyhow::anyhow!("下载原版游戏 Jar 失败:{:?}", e))?;
r.add_progress(1.);
drop(l);
Ok(())
}
async fn download_library(&self, sha1: String, path: String, save_path: &str) -> DynResult {
let l = self.parallel_lock.acquire().await;
let r = self.reporter.sub();
let full_path = format!("{save_path}/{path}");
r.set_message(format!("正在下载原版库 {path}"));
r.add_max_progress(1.);
if std::path::Path::new(&full_path).is_file() {
if self.verify_data {
let mut file = inner_future::fs::OpenOptions::new()
.read(true)
.open(&full_path)
.await?;
let current_sha1 = crate::utils::get_data_sha1(&mut file).await?;
if sha1 == current_sha1 {
r.add_progress(1.);
return Ok(());
}
} else {
r.add_progress(1.);
return Ok(());
}
} else {
inner_future::fs::create_dir_all(
&full_path[..full_path
.rfind(std::path::is_separator)
.unwrap_or(full_path.len())],
)
.await?;
}
let default_uris = [
match self.source {
DownloadSource::Default => {
format!("https://libraries.minecraft.net/{path}")
}
DownloadSource::BMCLAPI => {
format!("https://bmclapi2.bangbang93.com/maven/{path}")
}
DownloadSource::MCBBS => format!("https://download.mcbbs.net/maven/{path}"),
_ => format!("https://libraries.minecraft.net/{path}"),
},
format!("https://bmclapi2.bangbang93.com/maven/{path}"),
format!("https://download.mcbbs.net/maven/{path}"),
format!("https://libraries.minecraft.net/{path}"),
];
crate::http::download(&default_uris, &full_path, 0)
.await
.map_err(|e| anyhow::anyhow!("下载库 {} 失败:{:?}", path, e))?;
r.add_progress(1.);
drop(l);
Ok(())
}
async fn download_asset_index(
&self,
name: &str,
url: &str,
save_path: &str,
) -> DynResult<AssetIndexes> {
let r = self.reporter.sub();
r.set_message(format!("正在下载原版资源索引 {name}"));
let full_path = format!("{save_path}/indexes/{name}.json");
inner_future::fs::create_dir_all(
&full_path[..full_path
.rfind(std::path::is_separator)
.unwrap_or(full_path.len())],
)
.await?;
let p = {
let url = url.parse::<url::Url>()?;
let p = url.path();
p.to_owned()
};
let uris = [
match self.source {
DownloadSource::Default => {
format!("https://launchermeta.mojang.com{p}")
}
DownloadSource::BMCLAPI => {
format!("https://bmclapi2.bangbang93.com{p}")
}
DownloadSource::MCBBS => format!("https://download.mcbbs.net{p}"),
_ => format!("https://launchermeta.mojang.com{p}"),
},
format!("https://bmclapi2.bangbang93.com{p}"),
format!("https://download.mcbbs.net{p}"),
format!("https://launchermeta.mojang.com{p}"),
];
for uri in &uris {
let res = crate::http::retry_get_bytes(uri).await;
if let Ok(res) = res {
inner_future::fs::write(full_path, &res).await?;
return Ok(serde_json::from_slice(&res)?);
}
}
anyhow::bail!("获取素材索引失败,已尝试的链接:{}", uris.join("\n"))
}
async fn download_asset(
&self,
sha1: &str,
name: &str,
save_path: &str,
is_pre: bool,
r: Option<impl Reporter>,
) -> DynResult {
let sub_hash = &sha1[..2];
let full_path = if is_pre {
format!("{save_path}/../virtual/pre-1.6/{name}")
} else {
format!("{save_path}/{sub_hash}/{sha1}")
};
r.set_message(format!("正在下载原版资源 {name}"));
let l = self.parallel_lock.acquire().await;
if if is_pre {
Path::new(&full_path).exists()
} else {
is_asset_exists(sha1, save_path)
} {
if self.verify_data {
let mut file = inner_future::fs::OpenOptions::new()
.read(true)
.open(&full_path)
.await?;
let current_sha1 = crate::utils::get_data_sha1(&mut file).await?;
if sha1 == current_sha1 {
r.add_progress(1.);
return Ok(());
}
} else {
r.add_progress(1.);
return Ok(());
}
}
inner_future::fs::create_dir_all(
&full_path[..full_path
.rfind(std::path::is_separator)
.unwrap_or(full_path.len())],
)
.await?;
let uris = [
match self.source {
DownloadSource::Default => {
format!("https://resources.download.minecraft.net/{sub_hash}/{sha1}")
}
DownloadSource::BMCLAPI => {
format!("https://bmclapi2.bangbang93.com/assets/{sub_hash}/{sha1}")
}
DownloadSource::MCBBS => {
format!("https://download.mcbbs.net/assets/{sub_hash}/{sha1}")
}
_ => format!("https://resources.download.minecraft.net/{sub_hash}/{sha1}"),
},
format!("https://bmclapi2.bangbang93.com/assets/{sub_hash}/{sha1}"),
format!("https://download.mcbbs.net/assets/{sub_hash}/{sha1}"),
format!("https://resources.download.minecraft.net/{sub_hash}/{sha1}"),
];
crate::http::download(&uris, &full_path, 0)
.await
.map_err(|e| {
anyhow::anyhow!(
"下载资源文件失败 {:?},已尝试的链接:{}",
e,
uris.join("\n")
)
})?;
r.add_progress(1.);
drop(l);
Ok(())
}
async fn download_libraries(
&self,
libraries: &[Library],
) -> DynResult<HashMap<String, Vec<String>>> {
let mut _libraries_size = 0;
let mut native_jars_mapping: HashMap<String, Vec<String>> =
HashMap::with_capacity(libraries.len());
let lr = self.reporter.sub();
lr.set_message("正在检索并安装需要安装的依赖库".into());
enum LibraryTask<'a> {
Common {
sha1: Cow<'a, str>,
path: Cow<'a, str>,
},
Native {
platform: Cow<'a, str>,
sha1: Cow<'a, str>,
path: Cow<'a, str>,
},
}
let mut tasks = Vec::with_capacity(libraries.len() * 2);
for lib in libraries.iter() {
if let Some(downloads) = &lib.downloads {
if let Some(classifier) = &downloads.classifiers {
for (platform, meta) in classifier.iter() {
let target_platform = match platform.as_str() {
"natives-osx" => "natives-macos",
other => other,
};
tasks.push(LibraryTask::Native {
platform: target_platform.into(),
sha1: meta.sha1.as_str().into(),
path: meta.path.as_str().into(),
});
}
}
if let Some(artifact) = &downloads.artifact {
if let Some(i) = lib.name.find(":natives-") {
let (_, platform) = lib.name.split_at(i + 1);
tasks.push(LibraryTask::Native {
platform: platform.into(),
sha1: artifact.sha1.as_str().into(),
path: artifact.path.as_str().into(),
});
} else if lib.rules.is_allowed() {
tasks.push(LibraryTask::Common {
sha1: artifact.sha1.as_str().into(),
path: artifact.path.as_str().into(),
});
}
}
}
}
for task in &tasks {
if let LibraryTask::Native {
platform,
sha1,
path,
} = task
{
debug!("原生库 {platform} {sha1} {path}");
let p = format!("{}/{}", self.minecraft_library_path.as_str(), path);
if let Some(native_jars) = native_jars_mapping.get_mut(&platform.to_string()) {
if !native_jars.contains(&p) {
native_jars.push(p);
}
} else {
let mut native_jars = Vec::with_capacity(tasks.len());
native_jars.push(p);
native_jars_mapping.insert(platform.to_string(), native_jars);
}
}
}
let libraries_threads = tasks.into_iter().map(|x| match x {
LibraryTask::Common { sha1, path } => self.download_library(
sha1.to_string(),
path.to_string(),
self.minecraft_library_path.as_str(),
),
LibraryTask::Native { sha1, path, .. } => self.download_library(
sha1.to_string(),
path.to_string(),
self.minecraft_library_path.as_str(),
),
});
for v in futures::future::join_all(libraries_threads).await {
v?;
}
lr.remove_progress();
Ok(native_jars_mapping)
}
async fn download_vanilla(
&self,
version_name: &str,
version_meta: &VersionMeta,
is_repair: bool,
) -> DynResult {
info!("开始下载原版游戏 {version_name}");
let r = self.reporter.sub();
let game_file = format!(
"{}/{}/{}.jar",
self.minecraft_version_path.as_str(),
version_name,
version_name
);
r.set_message(format!("正在下载原版游戏 {version_name}"));
let main_jar = version_meta
.downloads
.as_ref()
.ok_or_else(|| anyhow::anyhow!("无法获取下载清单"))?
.get("client")
.ok_or_else(|| anyhow::anyhow!("无法获取客户端下载元数据"))?;
let main_jar_thread = self.download_vanilla_jar(&main_jar.url, &game_file, &main_jar.sha1);
if is_repair {
let lib_path = std::path::Path::new(self.minecraft_library_path.as_str());
let lib_path = lib_path
.join("org")
.join("glavo")
.join("1.0")
.join("log4j-patch");
if !lib_path.is_dir() {
inner_future::fs::create_dir_all(&lib_path).await?;
}
let log4j_path = lib_path.join("log4j-patch-agent-1.0.jar");
inner_future::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&log4j_path)
.await?
.write_all(crate::client::LOG4J_PATCH)
.await?;
}
let libraries_thread = self.download_libraries(&version_meta.libraries);
let is_pre = &version_meta.asset_index.as_ref().unwrap().id == "pre-1.6";
let assets_index = self
.download_asset_index(
&version_meta.asset_index.as_ref().unwrap().id,
&version_meta.asset_index.as_ref().unwrap().url,
self.minecraft_assets_path.as_str(),
)
.await?;
let mut assets_hashes = Vec::with_capacity(assets_index.objects.len());
let ar = r.sub();
let minecraft_assets_objects_path = format!("{}/objects", self.minecraft_assets_path);
let amounts = assets_index
.objects
.iter()
.filter(|a| {
if assets_hashes.contains(&a.1.hash) {
false
} else {
assets_hashes.push(a.1.hash.to_owned());
true
}
})
.filter(|(path, obj)| {
if is_pre {
!Path::new(&minecraft_assets_objects_path)
.parent()
.unwrap()
.join("virtual")
.join("pre-1.6")
.join(path)
.exists()
} else {
!is_asset_exists(&obj.hash, &minecraft_assets_objects_path)
}
})
.count();
assets_hashes.clear();
let assets_download_tasks = assets_index
.objects
.iter()
.filter(|a| {
if assets_hashes.contains(&a.1.hash) {
false
} else {
assets_hashes.push(a.1.hash.to_owned());
true
}
})
.filter(|(path, obj)| {
if is_pre {
!Path::new(&minecraft_assets_objects_path)
.parent()
.unwrap()
.join("virtual")
.join("pre-1.6")
.join(path)
.exists()
} else {
!is_asset_exists(&obj.hash, &minecraft_assets_objects_path)
}
});
ar.set_message("下载资源文件".into());
ar.add_max_progress(amounts as _);
let assets_index_objects = assets_download_tasks.map(|(rpath, obj)| {
self.download_asset(
&obj.hash,
rpath,
&minecraft_assets_objects_path,
is_pre,
ar.sub(),
)
});
let native_jars = if is_repair {
futures::future::join(
libraries_thread,
futures::future::join_all(assets_index_objects),
)
.await
.0?
} else {
futures::future::join3(
main_jar_thread,
libraries_thread,
futures::future::join_all(assets_index_objects),
)
.await
.1?
};
let native_dir = format!(
"{}/{}/natives",
self.minecraft_version_path.as_str(),
version_name
);
let nr = r.sub();
nr.set_max_progress(native_jars.len() as f64);
nr.set_message("正在解压原生库".into());
for (platform, items) in native_jars.iter() {
let native_dir = format!("{native_dir}/{platform}");
for item in items {
unzip_natives(item, &native_dir).await?;
}
nr.add_progress(1.);
}
r.remove_progress();
info!("原版游戏 {version_name} 下载完成!");
Ok(())
}
async fn install_vanilla(&self, version_name: &str, version_info: &VersionInfo) -> DynResult {
self.reporter.set_max_progress(4.);
self.reporter
.set_message(format!("正在获取版本元数据 {version_name}"));
create_dir_all(format!("{}/indexes", self.minecraft_assets_path)).await?;
create_dir_all(format!("{}/objects", self.minecraft_assets_path)).await?;
create_dir_all(self.minecraft_library_path.as_str()).await?;
create_dir_all(format!(
"{}/{}",
self.minecraft_version_path.as_str(),
version_name
))
.await?;
let version_file = format!(
"{}/{}/{}.json",
self.minecraft_version_path.as_str(),
version_name,
version_name
);
let url = version_info.url.parse::<url::Url>()?;
let url_path = url.path();
let res = crate::http::retry_get_bytes(match self.source {
DownloadSource::Default => format!("https://launchermeta.mojang.com{url_path}"),
DownloadSource::BMCLAPI => format!("https://bmclapi2.bangbang93.com{url_path}"),
DownloadSource::MCBBS => format!("https://download.mcbbs.net{url_path}"),
_ => format!("https://launchermeta.mojang.com{url_path}"),
})
.await
.map_err(|e| anyhow::anyhow!("下载版本元数据失败:{:?}", e))?;
inner_future::fs::write(&version_file, &res).await?;
self.reporter
.set_message(format!("正在下载游戏文件 {version_name}"));
let mut version_meta: VersionMeta = serde_json::from_slice(&res)?;
version_meta.fix_libraries();
self.download_vanilla(version_name, &version_meta, false)
.await?;
Ok(())
}
}
fn is_asset_exists(hash: &str, save_path: &str) -> bool {
let sub_hash = &hash[..2];
let full_path = format!("{save_path}/{sub_hash}/{hash}");
std::path::Path::new(&full_path).is_file()
}
const NATIVE_EXTS: &[&str] = &["dll", "so", "dylib", "jnilib"];
pub async fn unzip_natives(unzip_file: &str, unzip_dir: &str) -> DynResult {
let unzip_file = unzip_file.to_owned();
let unzip_dir = unzip_dir.to_owned();
inner_future::unblock(move || -> DynResult {
let file = std::fs::File::open(&unzip_file)?;
let dir = std::path::PathBuf::from(unzip_dir);
let mut archive = zip::ZipArchive::new(file)
.with_context(|| format!("解压原生库 {unzip_file} 时发生错误"))?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let p = match file.enclosed_name().and_then(|p| p.file_name()) {
Some(p) => p.to_owned(),
None => continue,
};
if let Some(ext) = Path::new(&p).extension() {
if !NATIVE_EXTS.contains(&ext.to_str().unwrap_or_default()) {
continue;
}
} else {
continue;
}
let save_path = dir.join(&p);
let save_dir = save_path.parent().unwrap();
let _ = std::fs::create_dir_all(save_dir);
debug!(
"解压原生库 {} 到 {}",
p.to_string_lossy(),
save_path.display()
);
let mut output = std::fs::File::create(save_path)?;
std::io::copy(&mut file, &mut output)?;
}
Ok(())
})
.await
}