diff --git a/scl-core/Cargo.toml b/scl-core/Cargo.toml index 8030d65..1e323ae 100644 --- a/scl-core/Cargo.toml +++ b/scl-core/Cargo.toml @@ -16,7 +16,7 @@ rust-version = "1.75" anyhow = "^1.0" base64 = "^0.21" futures = "^0.3.21" -surf = { version = "^2.3", default-features = false, features = [ "h1-client", "encoding" ] } +surf = { version = "^2.3", default-features = false, features = [ "h1-client" ] } image = { version = "^0.24", default-features = false, features = ["jpeg", "png", "gif", "bmp", "rgb"] } webp = "^0.2" nom = "^7.1" diff --git a/scl-core/src/download/mod.rs b/scl-core/src/download/mod.rs index 341c605..afc830a 100644 --- a/scl-core/src/download/mod.rs +++ b/scl-core/src/download/mod.rs @@ -6,6 +6,7 @@ pub mod fabric; pub mod forge; pub mod mcmod; pub mod modrinth; +pub mod neoforge; pub mod optifine; pub mod quiltmc; pub mod structs; @@ -17,6 +18,7 @@ use anyhow::Context; pub use authlib::AuthlibDownloadExt; pub use fabric::FabricDownloadExt; pub use forge::ForgeDownloadExt; +pub use neoforge::NeoForgeDownloadExt; pub use optifine::OptifineDownloadExt; pub use quiltmc::QuiltMCDownloadExt; use serde::{Deserialize, Serialize}; @@ -249,6 +251,7 @@ pub trait GameDownload: FabricDownloadExt + ForgeDownloadExt + VanillaDownloadExt + QuiltMCDownloadExt { /// 根据参数安装一个游戏,允许安装模组加载器 + #[allow(clippy::too_many_arguments)] async fn download_game( &self, version_name: &str, @@ -256,6 +259,7 @@ pub trait GameDownload: fabric: &str, quiltmc: &str, forge: &str, + neoforge: &str, optifine: &str, ) -> DynResult; } @@ -268,6 +272,7 @@ impl GameDownload for Downloader { fabric: &str, quiltmc: &str, forge: &str, + neoforge: &str, optifine: &str, ) -> DynResult { self.reporter @@ -304,6 +309,15 @@ impl GameDownload for Downloader { .await?; self.install_forge_post(version_name, &vanilla.id, forge) .await?; + } else if !neoforge.is_empty() { + self.install_vanilla(&vanilla.id, &vanilla).await?; // NeoForge 安装需要原版,如果安装器没有解析到则会从官方源下载,速度很慢 + crate::prelude::inner_future::future::try_zip( + self.install_vanilla(version_name, &vanilla), + self.install_neoforge_pre(version_name, &vanilla.id, neoforge), + ) + .await?; + self.install_neoforge_post(version_name, &vanilla.id, neoforge) + .await?; } else { self.install_vanilla(version_name, &vanilla).await?; } diff --git a/scl-core/src/download/neoforge.rs b/scl-core/src/download/neoforge.rs new file mode 100644 index 0000000..1488779 --- /dev/null +++ b/scl-core/src/download/neoforge.rs @@ -0,0 +1,499 @@ +//! NeoForge 模组加载器的下载模块 +use std::{ + io::{Read, Write}, + process::Stdio, + sync::atomic::AtomicBool, + time::{Duration, Instant}, +}; + +use anyhow::Context; +use inner_future::io::{AsyncBufReadExt, AsyncWriteExt}; +use serde_json::Value; + +use super::{ + structs::{NeoForgeItemInfo, NeoForgeVersionsData}, + Downloader, +}; +use crate::{download::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 = ":"; + +/// NeoForge 模组加载器的安装特质 +/// +/// 可以通过引入本特质和使用 [`crate::download::Downloader`] 来安装模组加载器 +pub trait NeoForgeDownloadExt: Sync { + /// 根据纯净版本号获取当前可用的所有 NeoForge 版本 + async fn get_avaliable_installers( + &self, + vanilla_version: &str, + ) -> DynResult; + /// 运行安装 NeoForge 模组加载器的预安装步骤 + /// + /// 一般是下载各种库和依赖 + async fn install_neoforge_pre( + &self, + version_id: &str, + vanilla_version: &str, + neoforge_version: &str, + ) -> DynResult; + /// 运行安装 NeoForge 模组加载器的后安装步骤 + /// + /// 通常是修改安装器信息,然后执行安装器完成最后的安装步骤 + async fn install_neoforge_post( + &self, + version_name: &str, + version_id: &str, + neoforge_version: &str, + ) -> DynResult; + /// 将安装器的部分信息进行修改,如版本名称,下载源等 + async fn modify_neoforge_installer( + &self, + from_reader: std::fs::File, + to_writer: std::fs::File, + name: &str, + ) -> DynResult; +} + +impl NeoForgeDownloadExt for Downloader { + async fn get_avaliable_installers( + &self, + vanilla_version: &str, + ) -> DynResult { + let mut versions_data = + crate::http::retry_get_json::>(match self.source { + DownloadSource::BMCLAPI => { + format!("https://bmclapi2.bangbang93.com/neoforge/list/{vanilla_version}") + } + _ => format!("https://bmclapi2.bangbang93.com/neoforge/list/{vanilla_version}"), + }) + .await + .map_err(|e| { + anyhow::anyhow!( + "下载 NeoForge {} 安装器版本元数据失败:{:?}", + vanilla_version, + e + ) + })?; + + versions_data.retain(|x| x.raw_version.starts_with("neoforge")); // 只保留 NeoForge 的包 + versions_data.iter_mut().for_each(|x| { + if let Some(s) = x.version.strip_prefix("neoforge-") { + x.version = s.to_owned(); + } + }); + versions_data.reverse(); // 调转顺序,从最新的开始 + let latest_version = versions_data.first().cloned(); + + Ok(NeoForgeVersionsData { + recommended: None, + latest: latest_version, + all_versions: versions_data, + }) + } + + async fn install_neoforge_pre( + &self, + version_id: &str, + vanilla_version: &str, + neoforge_version: &str, + ) -> DynResult { + let r = self.reporter.fork(); + + let full_path = format!( + "{root}/net/neoforged/neoforge/{mc}-{neoforge}/neoforge-{mc}-{neoforge}-installer.jar", + root = self.minecraft_library_path.as_str(), + mc = vanilla_version, + neoforge = neoforge_version + ); + tracing::trace!("Downloading NeoForge 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!("下载 NeoForge 安装器 {neoforge_version}")); + r.add_max_progress(1.); + + let uris = [ + match self.source { + DownloadSource::Default => format!("https://maven.neoforged.net/releases/net/neoforged/neoforge/{neoforge_version}/neoforge-{neoforge_version}-installer.jar"), + DownloadSource::BMCLAPI => format!("https://bmclapi2.bangbang93.com/maven/net/neoforged/neoforge/{neoforge_version}/neoforge-{neoforge_version}-installer.jar"), + DownloadSource::MCBBS => format!("https://download.mcbbs.net/maven/net/neoforged/neoforge/{neoforge_version}/neoforge-{neoforge_version}-installer.jar"), + _ => format!("https://maven.neoforged.net/releases/net/neoforged/neoforge/{neoforge_version}/neoforge-{neoforge_version}-installer.jar") + }, + format!("https://bmclapi2.bangbang93.com/maven/net/neoforged/neoforge/{neoforge_version}/neoforge-{neoforge_version}-installer.jar"), + format!("https://download.mcbbs.net/maven/net/neoforged/neoforge/{neoforge_version}/neoforge-{neoforge_version}-installer.jar"), + format!("https://maven.neoforged.net/releases/net/neoforged/neoforge/{neoforge_version}/neoforge-{neoforge_version}-installer.jar"), + ]; + + crate::http::download(&uris, &full_path, 0) + .await + .map_err(|e| { + anyhow::anyhow!( + "下载 NeoForge {}-{} 安装器失败:{:?}", + version_id, + neoforge_version, + e + ) + })?; + + r.add_progress(1.); + Ok(()) + } + + async fn install_neoforge_post( + &self, + version_name: &str, + version_id: &str, + neoforge_version: &str, + ) -> DynResult { + let r = self.reporter.fork(); + // 新版本均使用安装器安装 + + // Install helper + 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; + + // TODO: 换成自己的安装代码 + // Run installer + let full_path = format!( + "{root}/net/neoforged/neoforge/{mc}-{neoforge}/neoforge-{mc}-{neoforge}-installer.jar", + root = self.minecraft_library_path.as_str(), + mc = version_id, + neoforge = neoforge_version + ); + let tmp_full_path = format!( + "{root}/net/neoforged/neoforge/{mc}-{neoforge}/neoforge-{mc}-{neoforge}-installer.tmp.{tempid}.jar", + root = self.minecraft_library_path.as_str(), + mc = version_id, + neoforge = neoforge_version, + tempid = std::time::SystemTime::now() + .elapsed() + .unwrap_or_default() + .as_secs() + ); + tracing::trace!("Writing temp neoforge 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_neoforge_installer(from_file, to_file, &version_name) + .await + .with_context(|| { + format!( + "修改 NeoForge 模组安装器文件 {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("运行 NeoForge 安装器安装 NeoForge".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!( + "执行 NeoForge {}-{} 安装器失败,运行器返回值:{}", + version_id, + neoforge_version, + status + ) + } + } + + async fn modify_neoforge_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("打开 NeoForge 安装器时发生错误")?; + 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() { + // tracing::trace!("Writting file {}", entry.name()); + 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://maven.neoforged.net/releases", + }; + 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.neoforged.net/releases", + ) { + *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.neoforged.net/releases", + ) { + *down_url = + format!("{replace_source}{down_path}"); + tracing::trace!( + "已修改 libraries[{i}].url 字段" + ); + } + } + } + } + } + // 1.12.2 之前的镜像源 + 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.neoforged.net/releases", + ) { + *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.neoforged.net/releases/", + ) { + *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)?; + } + _ => { + // tracing::trace!("Copied file {}", entry.name()); + out_file.raw_copy_file(entry)? + } + } + } else if entry.is_dir() { + // tracing::trace!("Added dir {}", entry.name()); + out_file.add_directory(entry.name(), Default::default())?; + } + } + } + let mut to_writer = out_file.finish()?; + let _ = to_writer.flush(); + let _ = to_writer.sync_all(); + // std::io::copy(&mut from_reader, &mut to_writer)?; + // to_writer.sync_all()?; + Ok(()) + } +} diff --git a/scl-core/src/download/structs.rs b/scl-core/src/download/structs.rs index 515eb2e..29578c5 100644 --- a/scl-core/src/download/structs.rs +++ b/scl-core/src/download/structs.rs @@ -63,6 +63,36 @@ pub struct AssetItem { pub size: usize, } +/// NeoForge 加载器的版本信息 +#[derive(Clone, Debug)] +pub struct NeoForgeVersionsData { + /// 推荐下载的加载器版本 + pub recommended: Option, + /// 最新的加载器版本 + pub latest: Option, + /// 所有加载器版本 + pub all_versions: Vec, +} + +/// 一个 NeoForge 加载器的版本信息 +#[derive(Clone, Default, Debug, Deserialize)] +pub struct NeoForgeItemInfo { + /// 该加载器的版本号 + /// + /// 从 NeoForge 建立(1.20.1)到现在,版本格式保持 `[MC版本号]-[主版本号].[次版本号].[修订版本号][-[测试版后缀]]` + /// + /// 例如 `1.20.1-47.1.99` 或 `neoforge-20.4.0-beta` + pub version: String, + /// 该加载器的原始版本号 + #[serde(default, rename = "rawVersion")] + pub raw_version: String, + /// 该加载器的安装包下载链接 + #[serde(default, rename = "installerPath")] + pub installer_path: String, + /// 该加载器对应支持的原版版本号 + pub mcversion: String, +} + /// Forge 加载器的版本信息 #[derive(Clone, Debug)] pub struct ForgeVersionsData { @@ -74,7 +104,7 @@ pub struct ForgeVersionsData { pub all_versions: Vec, } -/// 一个加载器的版本信息 +/// 一个 Forge 加载器的版本信息 #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] pub struct ForgeItemInfo { /// 该加载器的版本号 diff --git a/scl-core/src/version/mod.rs b/scl-core/src/version/mod.rs index 0073e6f..73b8a06 100644 --- a/scl-core/src/version/mod.rs +++ b/scl-core/src/version/mod.rs @@ -102,6 +102,8 @@ pub enum VersionType { Vanilla, /// Forge 版本 Forge, + /// NeoForge 版本 + NeoForge, /// Fabric 版本 Fabric, /// QuiltMC 版本 diff --git a/scl-core/src/version/structs.rs b/scl-core/src/version/structs.rs index 216059c..0dce950 100644 --- a/scl-core/src/version/structs.rs +++ b/scl-core/src/version/structs.rs @@ -582,6 +582,8 @@ impl VersionInfo { if lib.name.starts_with("net.fabricmc:") { // 也有可能是 QuiltMC has_fabric = true; + } else if lib.name.starts_with("net.neoforged:") { + return VersionType::NeoForge; } else if lib.name.starts_with("net.minecraftforge:") { return VersionType::Forge; } else if lib.name.starts_with("org.quiltmc:") { diff --git a/scl-gui-widgets/src/widgets/download_module_item.rs b/scl-gui-widgets/src/widgets/download_module_item.rs index 1aa76a7..dd2bafc 100644 --- a/scl-gui-widgets/src/widgets/download_module_item.rs +++ b/scl-gui-widgets/src/widgets/download_module_item.rs @@ -186,18 +186,22 @@ impl Widget for DownloadModuleItem { } fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &D, data: &D, env: &druid::Env) { - if ctx.env_key_changed(&self.icon_key.0) { - self.reload_icon(env); - } - if ctx.env_key_changed(&self.icon_key.1) { - ctx.request_paint(); - } if ctx.has_requested_update() || !old_data.same(data) { self.text.update(ctx, data, env); self.desc.update(ctx, data, env); } - if let Icon::Image(img) = &mut self.icon { - img.update(ctx, data, env); + match &mut self.icon { + Icon::Image(img) => { + img.update(ctx, data, env); + } + Icon::BezPath(_) => { + if ctx.env_key_changed(&self.icon_key.0) { + self.reload_icon(env); + } + if ctx.env_key_changed(&self.icon_key.1) { + ctx.request_paint(); + } + } } } @@ -228,11 +232,6 @@ impl Widget for DownloadModuleItem { } fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &D, env: &druid::Env) { - let icon_brush = PaintBrush::Color(env.get(if env.get(IS_DARK) { - &self.icon_key.2 - } else { - &self.icon_key.1 - })); let size = ctx.size(); let is_hot = ctx.is_hot(); let is_active = ctx.is_active(); @@ -250,6 +249,11 @@ impl Widget for DownloadModuleItem { let icon_size = druid::Size::new(size.height, size.height); ctx.with_save(|ctx| match &mut self.icon { Icon::BezPath(p) => { + let icon_brush = PaintBrush::Color(env.get(if env.get(IS_DARK) { + &self.icon_key.2 + } else { + &self.icon_key.1 + })); ctx.transform(Affine::translate( ((icon_size - p.bounding_box().size()) / 2.).to_vec2(), ));