use std::{
io::{Read, Write},
process::Stdio,
sync::atomic::AtomicBool,
time::{Duration, Instant},
};
use anyhow::Context;
use async_trait::async_trait;
use inner_future::io::{AsyncBufReadExt, AsyncWriteExt};
use serde_json::Value;
use super::{structs::ForgeVersionsData, Downloader};
use crate::{
download::{
structs::{ForgeItemInfo, ForgePromoItem},
DownloadSource,
},
prelude::*,
};
const FORGE_INSTALL_HELPER: &[u8] = include_bytes!("../../assets/forge-install-bootstrapper.jar");
#[cfg(target_os = "windows")]
const CLASS_PATH_SPAREATOR: &str = ";";
#[cfg(target_os = "linux")]
const CLASS_PATH_SPAREATOR: &str = ":";
#[cfg(target_os = "macos")]
const CLASS_PATH_SPAREATOR: &str = ":";
#[async_trait]
pub trait ForgeDownloadExt: Sync {
async fn get_avaliable_installers(&self, vanilla_version: &str)
-> DynResult<ForgeVersionsData>;
async fn install_forge_pre(
&self,
version_id: &str,
vanilla_version: &str,
forge_version: &str,
) -> DynResult;
async fn install_forge_post(
&self,
version_name: &str,
version_id: &str,
forge_version: &str,
) -> DynResult;
async fn modify_forge_installer(
&self,
from_reader: std::fs::File,
to_writer: std::fs::File,
name: &str,
) -> DynResult;
}
#[async_trait]
impl<R: Reporter> ForgeDownloadExt for Downloader<R> {
async fn get_avaliable_installers(
&self,
vanilla_version: &str,
) -> DynResult<ForgeVersionsData> {
let (mut versions_data, mut version_promo) = futures::future::try_join(
crate::http::retry_get(match self.source {
DownloadSource::BMCLAPI => {
format!("https://bmclapi2.bangbang93.com/forge/minecraft/{vanilla_version}")
}
_ => format!("https://bmclapi2.bangbang93.com/forge/minecraft/{vanilla_version}"),
}),
crate::http::retry_get(match self.source {
DownloadSource::BMCLAPI => "https://bmclapi2.bangbang93.com/forge/promos",
_ => "https://bmclapi2.bangbang93.com/forge/promos",
}),
)
.await
.map_err(|e| {
anyhow::anyhow!(
"下载 Forge {} 安装器版本元数据失败:{:?}",
vanilla_version,
e
)
})?;
let (version_promo, mut info): (Vec<ForgePromoItem>, Vec<ForgeItemInfo>) =
futures::future::try_join(version_promo.body_json(), versions_data.body_json())
.await
.map_err(|e| anyhow::anyhow!(e))?;
info.sort_by(|a, b| {
let a: u64 = {
let mut r = 0;
for (i, x) in a
.version
.split('.')
.map(|x| str::parse::<u16>(x).unwrap_or_default())
.enumerate()
{
r |= (x as u64) << (64 - (i + 1) * 16);
}
r
};
let b: u64 = {
let mut r = 0;
for (i, x) in b
.version
.split('.')
.map(|x| str::parse::<u16>(x).unwrap_or_default())
.enumerate()
{
r |= (x as u64) << (64 - (i + 1) * 16);
}
r
};
a.cmp(&b)
});
let recommended_version = version_promo
.iter()
.find(|a| a.name == format!("{vanilla_version}-recommended"))
.and_then(|a| a.build.to_owned());
let latest_version = version_promo
.iter()
.find(|a| a.name == format!("{vanilla_version}-latest"))
.and_then(|a| a.build.to_owned());
info.reverse(); Ok(ForgeVersionsData {
recommended: recommended_version,
latest: latest_version,
all_versions: info,
})
}
async fn install_forge_pre(
&self,
version_id: &str,
vanilla_version: &str,
forge_version: &str,
) -> DynResult {
let r = self.reporter.fork();
if vanilla_version
.parse::<crate::semver::MinecraftVersion>()
.map(|x| x.should_forge_use_override_installiation())
.unwrap_or(false)
{
let client_or_universal = vanilla_version
.parse::<crate::semver::MinecraftVersion>()
.map(|x| x.should_forge_use_client_or_universal())
.unwrap_or(false);
let suffix = if client_or_universal {
"client" } else {
"universal" };
let full_path = format!(
"{root}/net/minecraftforge/forge/{mc}-{forge}/forge-{mc}-{forge}-{suffix}.zip",
root = self.minecraft_library_path.as_str(),
mc = vanilla_version,
forge = forge_version,
suffix = suffix,
);
if std::path::Path::new(&full_path).is_file() {
return Ok(());
}
inner_future::fs::create_dir_all(
&full_path[..full_path.rfind('/').unwrap_or(full_path.len())],
)
.await?;
r.set_message(format!("下载 Forge 安装覆盖包 {forge_version}"));
r.add_max_progress(1.);
let uris = [
match self.source {
DownloadSource::Default => format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-{suffix}.zip"),
DownloadSource::BMCLAPI => format!("https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-{suffix}.zip"),
DownloadSource::MCBBS => format!("https://download.mcbbs.net/maven/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-{suffix}.zip"),
_ => format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-{suffix}.zip")
},
format!("https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-{suffix}.zip"),
format!("https://download.mcbbs.net/maven/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-{suffix}.zip"),
format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-{suffix}.zip"),
];
crate::http::download(&uris, &full_path, 0)
.await
.map_err(|e| {
anyhow::anyhow!(
"下载 Forge {}-{} 安装覆盖包失败:{:?}",
version_id,
forge_version,
e
)
})?;
} else {
let full_path = format!(
"{root}/net/minecraftforge/forge/{mc}-{forge}/forge-{mc}-{forge}-installer.jar",
root = self.minecraft_library_path.as_str(),
mc = vanilla_version,
forge = forge_version
);
tracing::trace!("Downloading Forge Installer {full_path}");
if std::path::Path::new(&full_path).is_file() {
return Ok(());
}
inner_future::fs::create_dir_all(
&full_path[..full_path.rfind('/').unwrap_or(full_path.len())],
)
.await?;
r.set_message(format!("下载 Forge 安装器 {forge_version}"));
r.add_max_progress(1.);
let build_id =
&forge_version[forge_version.rfind('.').map(|x| x + 1).unwrap_or_default()..];
let uris = if forge_version.split('.').count() == 3 {
[
match self.source {
DownloadSource::Default => format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar"),
DownloadSource::BMCLAPI => format!("https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar"),
DownloadSource::MCBBS => format!("https://download.mcbbs.net/maven/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar"),
_ => format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar")
},
format!("https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar"),
format!("https://download.mcbbs.net/maven/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar"),
format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar"),
]
} else {
[
match self.source {
DownloadSource::Default => format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar"),
DownloadSource::BMCLAPI => format!("https://bmclapi2.bangbang93.com/forge/download/{build_id}"),
DownloadSource::MCBBS => format!("https://download.mcbbs.net/forge/download/{build_id}"),
_ => format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar")
},
format!("https://bmclapi2.bangbang93.com/forge/download/{build_id}"),
format!("https://download.mcbbs.net/forge/download/{build_id}"),
format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{vanilla_version}-{forge_version}/forge-{vanilla_version}-{forge_version}-installer.jar"),
]
};
crate::http::download(&uris, &full_path, 0)
.await
.map_err(|e| {
anyhow::anyhow!(
"下载 Forge {}-{} 安装器失败:{:?}",
version_id,
forge_version,
e
)
})?;
}
r.add_progress(1.);
Ok(())
}
async fn install_forge_post(
&self,
version_name: &str,
version_id: &str,
forge_version: &str,
) -> DynResult {
let r = self.reporter.fork();
if version_id
.parse::<crate::semver::MinecraftVersion>()
.map(|x| x.should_forge_use_override_installiation())
.unwrap_or(false)
{
let client_or_universal = version_id
.parse::<crate::semver::MinecraftVersion>()
.map(|x| x.should_forge_use_client_or_universal())
.unwrap_or(false);
let suffix = if client_or_universal {
"client"
} else {
"universal"
};
let full_path = format!(
"{root}/net/minecraftforge/forge/{mc}-{forge}/forge-{mc}-{forge}-{suffix}.zip",
root = self.minecraft_library_path.as_str(),
mc = version_id,
forge = forge_version,
suffix = suffix,
);
let full_override_path = format!(
"{root}/{version_name}/{version_name}.jar",
root = self.minecraft_version_path.as_str(),
version_name = version_name,
);
let full_temp_override_path = format!(
"{root}/{version_name}/{version_name}.tmp.jar",
root = self.minecraft_version_path.as_str(),
version_name = version_name,
);
inner_future::unblock(move || -> DynResult {
let full_file = std::fs::OpenOptions::new().read(true).open(&full_path)?;
let mut full_file = zip::ZipArchive::new(full_file)?;
let override_file = std::fs::OpenOptions::new()
.read(true)
.open(&full_override_path)?;
let mut override_file = zip::ZipArchive::new(override_file)?;
let temp_override_file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&full_temp_override_path)?;
let mut temp_override_file = zip::ZipWriter::new(temp_override_file);
for index in 0..override_file.len() {
if let Ok(entry) = override_file.by_index(index) {
if entry.name().starts_with("META-INF") {
continue;
} else if entry.is_file() {
temp_override_file.raw_copy_file(entry)?;
} else if entry.is_dir() {
temp_override_file.add_directory(entry.name(), Default::default())?;
}
}
}
for index in 0..full_file.len() {
if let Ok(entry) = full_file.by_index(index) {
if entry.name().starts_with("META-INF") {
continue;
} else if entry.is_file() {
temp_override_file.raw_copy_file(entry)?;
} else if entry.is_dir() {
temp_override_file.add_directory(entry.name(), Default::default())?;
}
}
}
temp_override_file.finish()?.sync_all()?;
std::fs::remove_file(&full_override_path)?;
std::fs::rename(full_temp_override_path, full_override_path)?;
Ok(())
})
.await?;
Ok(())
} else {
let installer_path = format!(
"{}/com/bangbang93/forge-install-bootstrapper/0.0.0/forge-install-bootstrapper.jar",
self.minecraft_library_path.as_str()
);
inner_future::fs::create_dir_all(
std::path::Path::new(&installer_path).parent().unwrap(),
)
.await?;
let mut file = inner_future::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&installer_path)
.await?;
file.write_all(FORGE_INSTALL_HELPER).await?;
let _ = file.flush().await;
let _ = file.sync_all().await;
let full_path = format!(
"{root}/net/minecraftforge/forge/{mc}-{forge}/forge-{mc}-{forge}-installer.jar",
root = self.minecraft_library_path.as_str(),
mc = version_id,
forge = forge_version
);
let tmp_full_path = format!(
"{root}/net/minecraftforge/forge/{mc}-{forge}/forge-{mc}-{forge}-installer.tmp.{tempid}.jar",
root = self.minecraft_library_path.as_str(),
mc = version_id,
forge = forge_version,
tempid = std::time::SystemTime::now().elapsed().unwrap_or_default().as_secs()
);
tracing::trace!("Writing temp forge installer from {full_path} to {tmp_full_path}");
{
let version_name = version_name.to_owned();
let full_path = full_path.to_owned();
let tmp_full_path = tmp_full_path.to_owned();
let full_path_c = full_path.to_owned();
let tmp_full_path_c = tmp_full_path.to_owned();
let (from_file, to_file) = futures::future::try_join(
inner_future::unblock(move || {
std::fs::OpenOptions::new().read(true).open(full_path)
}),
inner_future::unblock(move || {
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(tmp_full_path)
}),
)
.await?;
tracing::trace!("Modifying");
self.modify_forge_installer(from_file, to_file, &version_name)
.await
.with_context(|| {
format!(
"修改 Forge 模组安装器文件 {full_path_c} 到 {tmp_full_path_c} 时发生错误"
)
})?;
}
r.add_max_progress(2.);
r.set_message("正在修改安装器参数".into());
#[cfg(not(windows))]
let mut cmd = inner_future::process::Command::new(&self.java_path);
#[cfg(windows)]
let mut cmd = {
use inner_future::process::windows::CommandExt;
let mut cmd = inner_future::process::Command::new(&self.java_path);
cmd.creation_flags(0x08000000);
cmd
};
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::inherit());
cmd.stdin(Stdio::null());
cmd.arg("-cp");
cmd.arg(format!(
"{}{}{}",
&installer_path, CLASS_PATH_SPAREATOR, &tmp_full_path
));
cmd.arg("com.bangbang93.ForgeInstaller");
cmd.arg(self.minecraft_path.as_str()); r.add_progress(1.);
r.set_message("运行 Forge 安装器安装 Forge".into());
tracing::trace!("Start running installer bootstrapper {cmd:?}");
let mut child = cmd.spawn()?;
let install_succeed = AtomicBool::new(false);
let ir = r.fork();
ir.set_message("这需要一点时间……".into());
let pr = r.fork();
let mut delay_timer = Instant::now();
if let Some(stdout) = child.stdout.take() {
let mut stdout = inner_future::io::BufReader::new(stdout);
let mut buf = String::with_capacity(256);
loop {
if let Ok(len) = stdout.read_line(&mut buf).await {
if len == 0 {
break;
} else {
let line = buf[..len].trim();
let delayed = delay_timer.elapsed() > Duration::from_millis(16);
if line.starts_with("Patching ") {
if delayed {
pr.set_message(line.to_owned());
}
} else if delayed {
pr.set_message(line.to_owned());
}
tracing::trace!("[FIB] {line}");
if let Some(class_name) = line.strip_prefix("Patching ") {
if delayed {
ir.set_message(format!("正在修补类 {class_name}"));
}
} else if let Some(url) = line.strip_prefix("Downloading library from ")
{
ir.set_message(format!("正在下载依赖 {url}"));
} else if let Some(url) = line.strip_prefix("Following redirect: ") {
ir.set_message(format!("下载重定向至 {url}"));
} else if let Some(class_name) = line.strip_prefix("Reading patch ") {
ir.set_message(format!("正在读取修补信息 {class_name}"));
} else if line == "Task: DOWNLOAD_MOJMAPS" {
ir.set_message("正在下载源码对照表".into());
} else if line == "Task: MERGE_MAPPING" {
ir.set_message("正在合并源码对照表".into());
} else if line == "Injecting profile" {
ir.set_message("正在注入版本元数据".into());
} else if line == "true" {
install_succeed.store(true, std::sync::atomic::Ordering::SeqCst)
}
if delayed {
delay_timer = Instant::now();
}
buf.clear()
}
}
}
}
drop(ir);
drop(pr);
let status = child.status().await?;
r.add_progress(1.);
r.remove_progress();
inner_future::fs::remove_file(tmp_full_path).await?;
if status.success() && install_succeed.load(std::sync::atomic::Ordering::SeqCst) {
Ok(())
} else {
anyhow::bail!(
"执行 Forge {}-{} 安装器失败,运行器返回值:{}",
version_id,
forge_version,
status
)
}
}
}
async fn modify_forge_installer(
&self,
from_reader: std::fs::File,
to_writer: std::fs::File,
name: &str,
) -> DynResult {
tracing::trace!("Opening file");
let mut file = zip::ZipArchive::new(std::io::BufReader::new(from_reader))
.context("打开 Forge 安装器时发生错误")?;
tracing::trace!("Opening file");
let mut out_file = zip::ZipWriter::new(to_writer);
tracing::trace!("Reading file");
for index in 0..file.len() {
if let Ok(mut entry) = file.by_index(index) {
if entry.name().starts_with("META-INF") {
continue;
}
if entry.is_file() {
match entry.name() {
"install_profile.json" => {
let mut data = String::with_capacity(entry.size() as usize);
entry.read_to_string(&mut data)?;
let mut install_profile: Value = serde_json::from_str(&data)?;
if let Value::Object(obj) = &mut install_profile {
if let Some(Value::String(version)) = obj.get_mut("version") {
*version = name.to_owned();
tracing::trace!("已修改 version 字段为 {version}");
}
if let Some(Value::Object(obj)) = obj.get_mut("install") {
if let Some(Value::String(target)) = obj.get_mut("target") {
*target = name.to_owned();
tracing::trace!("已修改 install.target 字段为 {target}");
}
}
let replace_source = match self.source {
DownloadSource::BMCLAPI => {
"https://bmclapi2.bangbang93.com/maven"
}
DownloadSource::MCBBS => "https://download.mcbbs.net/maven",
_ => "https://files.minecraftforge.net",
};
if let Some(Value::Array(array)) = obj.get_mut("libraries") {
for (i, lib) in array.iter_mut().enumerate() {
if let Value::Object(obj) = lib {
obj.remove("serverreq");
obj.insert("clientreq".into(), Value::Bool(true));
if let Some(Value::Object(obj)) =
obj.get_mut("downloads")
{
if let Some(Value::Object(obj)) =
obj.get_mut("artifact")
{
if let Some(Value::String(down_url)) =
obj.get_mut("url")
{
if let Some(down_path) = down_url
.strip_prefix(
"https://maven.minecraftforge.net",
)
{
*down_url = format!(
"{replace_source}{down_path}"
);
tracing::trace!(
"已修改 libraries[{i}].download.artifact.url 字段"
);
}
}
}
}
if let Some(Value::String(down_url)) =
obj.get_mut("url")
{
if let Some(down_path) = down_url.strip_prefix(
"https://maven.minecraftforge.net",
) {
*down_url =
format!("{replace_source}{down_path}");
tracing::trace!(
"已修改 libraries[{i}].url 字段"
);
}
if let Some(down_path) = down_url.strip_prefix(
"https://files.minecraftforge.net",
) {
*down_url =
format!("{replace_source}{down_path}");
tracing::trace!(
"已修改 libraries[{i}].url 字段"
);
}
}
}
}
}
if let Some(Value::Object(obj)) = obj.get_mut("versionInfo") {
if let Some(Value::Array(array)) = obj.get_mut("libraries") {
for (i, lib) in array.iter_mut().enumerate() {
if let Value::Object(obj) = lib {
obj.remove("serverreq");
obj.insert("clientreq".into(), Value::Bool(true));
if let Some(Value::Object(obj)) =
obj.get_mut("downloads")
{
if let Some(Value::Object(obj)) =
obj.get_mut("artifact")
{
if let Some(Value::String(down_url)) =
obj.get_mut("url")
{
if let Some(down_path) = down_url
.strip_prefix(
"https://maven.minecraftforge.net",
) {
*down_url = format!(
"{replace_source}{down_path}"
);
tracing::trace!(
"已修改 libraries[{i}].download.artifact.url 字段"
);
}
}
}
}
if let Some(Value::String(down_url)) =
obj.get_mut("url")
{
if let Some(down_path) = down_url.strip_prefix(
"https://maven.minecraftforge.net/",
) {
*down_url =
format!("{replace_source}{down_path}");
tracing::trace!("已修改 versionInfo.libraries[{i}].url 字段");
}
if let Some(down_path) = down_url.strip_prefix(
"https://files.minecraftforge.net",
) {
*down_url =
format!("{replace_source}{down_path}");
tracing::trace!("已修改 versionInfo.libraries[{i}].url 字段");
}
}
}
}
}
}
}
#[cfg(debug_assertions)]
tracing::trace!(
"修改完毕:\n{}",
serde_json::to_string_pretty(&install_profile)?
);
let output = serde_json::to_vec_pretty(&install_profile)?;
out_file.start_file(entry.name(), Default::default())?;
out_file.write_all(&output)?;
}
_ => {
out_file.raw_copy_file(entry)?
}
}
} else if entry.is_dir() {
out_file.add_directory(entry.name(), Default::default())?;
}
}
}
let mut to_writer = out_file.finish()?;
let _ = to_writer.flush();
let _ = to_writer.sync_all();
Ok(())
}
}