scl_core/download/
mod.rs

1//! 游戏资源下载模块,所有的游戏/模组/模组中文名称等数据的获取和安装都在这里
2
3pub mod authlib;
4pub mod curseforge;
5pub mod fabric;
6pub mod forge;
7pub mod mcmod;
8pub mod modrinth;
9pub mod optifine;
10pub mod quiltmc;
11pub mod structs;
12pub mod vanilla;
13
14use std::{fmt::Display, path::Path, str::FromStr};
15
16use anyhow::Context;
17use async_trait::async_trait;
18pub use authlib::AuthlibDownloadExt;
19pub use fabric::FabricDownloadExt;
20pub use forge::ForgeDownloadExt;
21pub use optifine::OptifineDownloadExt;
22pub use quiltmc::QuiltMCDownloadExt;
23use serde::{Deserialize, Serialize};
24pub use vanilla::VanillaDownloadExt;
25
26use self::structs::VersionInfo;
27use crate::{path::*, prelude::*, progress::*};
28
29/// 游戏的下载来源,支持和 BMCLAPI 同格式的自定义镜像源
30///
31/// 通常国内的镜像源速度是比官方快的,但是更新不如官方的及时
32#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
33pub enum DownloadSource {
34    /// 全部使用原始来源下载
35    Default,
36    /// 全部使用 BMCLAPI 提供的镜像源下载
37    ///
38    /// 为了支持镜像源,在这里鼓励大家前去支持一下:<https://afdian.net/a/bangbang93>
39    BMCLAPI,
40    /// 全部使用 MCBBS 提供的镜像源下载
41    MCBBS,
42    /// 使用符合 BMCLAPI 镜像链接格式的自定义镜像源下载
43    Custom(url::Url),
44}
45
46impl Default for DownloadSource {
47    fn default() -> Self {
48        Self::Default
49    }
50}
51
52impl Display for DownloadSource {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(
55            f,
56            "{}",
57            match self {
58                DownloadSource::Default => "默认(官方)下载源",
59                DownloadSource::BMCLAPI => "BMCLAPI 下载源",
60                DownloadSource::MCBBS => "MCBBS 下载源",
61                DownloadSource::Custom(_) => "自定义",
62            }
63        )
64    }
65}
66
67impl FromStr for DownloadSource {
68    type Err = ();
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        match s {
72            "Offical" => Ok(Self::Default),
73            "BMCLAPI" => Ok(Self::BMCLAPI),
74            "MCBBS" => Ok(Self::MCBBS),
75            s => {
76                let url = s.parse::<url::Url>();
77                if let Ok(url) = url {
78                    Ok(Self::Custom(url))
79                } else {
80                    Ok(Self::Default)
81                }
82            }
83        }
84    }
85}
86
87/// 下载结构,用于存储下载所需的信息,并通过附带的扩展特质下载需要的东西
88#[derive(Debug)]
89pub struct Downloader<R> {
90    /// 使用的下载源
91    pub source: DownloadSource,
92    /// 当前的 Minecraft 游戏目录路径
93    pub(crate) minecraft_path: String,
94    /// 当前的 Minecraft 依赖库目录路径
95    pub(crate) minecraft_library_path: String,
96    /// 当前的 Minecraft 版本文件夹目录路径
97    pub(crate) minecraft_version_path: String,
98    /// 当前的 Minecraft 资源文件夹目录路径
99    pub(crate) minecraft_assets_path: String,
100    /// 是否使用版本独立方式安装
101    ///
102    /// 这个会影响 Optifine 以模组形式的安装路径
103    ///
104    /// (会被安装在 版本/mods 文件夹里还是 .minecraft/mods 文件夹里)
105    pub game_independent: bool,
106    /// 是否验证已存在的文件是否正确
107    pub verify_data: bool,
108    /// 任意的 Java 运行时执行文件目录
109    ///
110    /// 在安装 Forge 时会使用
111    pub java_path: String,
112    /// 下载并发量
113    parallel_amount: usize,
114    /// 下载并发锁
115    pub(crate) parallel_lock: inner_future::lock::Semaphore,
116    /// 下载的进度报告对象
117    pub reporter: Option<R>,
118}
119
120// let l = self.parallel_amount.acquire().await;
121
122impl<R> Downloader<R> {
123    /// 设置安装的目录,传入一个 `.minecraft` 文件夹路径作为参数
124    ///
125    /// 游戏将会被安装到此处
126    pub fn set_minecraft_path(&mut self, dot_minecraft_path: impl AsRef<Path>) {
127        let dot_minecraft_path = dot_minecraft_path.as_ref().to_path_buf();
128        self.minecraft_path = dot_minecraft_path.to_string_lossy().to_string();
129        self.minecraft_library_path = dot_minecraft_path
130            .join("libraries")
131            .to_string_lossy()
132            .to_string();
133        self.minecraft_version_path = dot_minecraft_path
134            .join("versions")
135            .to_string_lossy()
136            .to_string();
137        self.minecraft_assets_path = dot_minecraft_path
138            .join("assets")
139            .to_string_lossy()
140            .to_string();
141    }
142
143    /// Builder 模式的 [`Downloader::set_minecraft_path`]
144    pub fn with_minecraft_path(mut self, dot_minecraft_path: impl AsRef<Path>) -> Self {
145        self.set_minecraft_path(dot_minecraft_path);
146        self
147    }
148}
149
150impl<R: Reporter> Clone for Downloader<R> {
151    fn clone(&self) -> Self {
152        Self {
153            source: self.source.clone(),
154            minecraft_path: self.minecraft_path.clone(),
155            minecraft_library_path: self.minecraft_library_path.clone(),
156            minecraft_version_path: self.minecraft_version_path.clone(),
157            minecraft_assets_path: self.minecraft_assets_path.clone(),
158            game_independent: self.game_independent,
159            verify_data: self.verify_data,
160            java_path: self.java_path.clone(),
161            parallel_lock: if self.parallel_amount == 0 {
162                inner_future::lock::Semaphore::new(usize::MAX)
163            } else {
164                inner_future::lock::Semaphore::new(self.parallel_amount)
165            },
166            reporter: self.reporter.clone(),
167            parallel_amount: self.parallel_amount,
168        }
169    }
170}
171
172impl<R: Reporter> Downloader<R> {
173    /// 设置一个进度报告对象,下载进度将会被上报给这个对象
174    #[must_use]
175    pub fn with_reporter(mut self, reporter: R) -> Self {
176        self.reporter = Some(reporter);
177        self
178    }
179    /// 设置一个下载源
180    #[must_use]
181    pub fn with_source(mut self, source: DownloadSource) -> Self {
182        self.source = source;
183        self
184    }
185    /// 设置一个 Java 运行时,安装 Forge 和 Optifine 时需要用到
186    #[must_use]
187    pub fn with_java(mut self, java_path: String) -> Self {
188        self.java_path = java_path;
189        self
190    }
191    /// 设置是否使用版本独立方式安装
192    ///
193    /// 这个会影响 Optifine 以模组形式的安装路径
194    ///
195    /// (会被安装在 版本/mods 文件夹里还是 .minecraft/mods 文件夹里)
196    #[must_use]
197    pub fn with_game_independent(mut self, game_independent: bool) -> Self {
198        self.game_independent = game_independent;
199        self
200    }
201    /// 设置下载时的并发量,如果为 0 则不限制
202    #[must_use]
203    pub fn with_parallel_amount(mut self, limit: usize) -> Self {
204        self.parallel_amount = limit;
205        if limit == 0 {
206            self.parallel_lock = inner_future::lock::Semaphore::new(usize::MAX);
207        } else {
208            self.parallel_lock = inner_future::lock::Semaphore::new(limit);
209        }
210        self
211    }
212    /// 是否强制校验已下载的文件以确认是否需要重新下载
213    ///
214    /// 如不强制则仅检测文件是否存在
215    #[must_use]
216    pub fn with_verify_data(mut self) -> Self {
217        self.verify_data = true;
218        self
219    }
220}
221impl<R: Reporter> Default for Downloader<R> {
222    fn default() -> Self {
223        Self {
224            source: DownloadSource::Default,
225            minecraft_path: MINECRAFT_PATH.to_owned(),
226            minecraft_library_path: MINECRAFT_LIBRARIES_PATH.to_owned(),
227            minecraft_version_path: MINECRAFT_VERSIONS_PATH.to_owned(),
228            minecraft_assets_path: MINECRAFT_ASSETS_PATH.to_owned(),
229            game_independent: false,
230            verify_data: false,
231            java_path: {
232                #[cfg(windows)]
233                {
234                    "javaw.exe".into()
235                }
236                #[cfg(not(windows))]
237                {
238                    "java".into()
239                }
240            },
241            reporter: None,
242            parallel_amount: 64,
243            parallel_lock: inner_future::lock::Semaphore::new(64),
244        }
245    }
246}
247
248/// 一个游戏安装特质,如果你并不需要单独安装其它部件,则可以单独引入这个特质来安装游戏
249#[async_trait]
250pub trait GameDownload<'a>:
251    FabricDownloadExt + ForgeDownloadExt + VanillaDownloadExt + QuiltMCDownloadExt
252{
253    /// 根据参数安装一个游戏,允许安装模组加载器
254    async fn download_game(
255        &self,
256        version_name: &str,
257        vanilla: VersionInfo,
258        fabric: &str,
259        quiltmc: &str,
260        forge: &str,
261        optifine: &str,
262    ) -> DynResult;
263}
264
265#[async_trait]
266impl<R: Reporter> GameDownload<'_> for Downloader<R> {
267    async fn download_game(
268        &self,
269        version_name: &str,
270        vanilla: VersionInfo,
271        fabric: &str,
272        quiltmc: &str,
273        forge: &str,
274        optifine: &str,
275    ) -> DynResult {
276        self.reporter
277            .set_message(format!("正在下载游戏 {version_name}"));
278
279        let launcher_profiles_path =
280            std::path::Path::new(&self.minecraft_path).join("launcher_profiles.json");
281
282        if !launcher_profiles_path.exists() {
283            inner_future::fs::create_dir_all(launcher_profiles_path.parent().unwrap()).await?;
284            inner_future::fs::write(launcher_profiles_path, r#"{"profiles":{},"selectedProfile":null,"authenticationDatabase":{},"selectedUser":{"account":"00000111112222233333444445555566","profile":"66666555554444433333222221111100"}}"#).await?;
285        }
286
287        if !fabric.is_empty() {
288            crate::prelude::inner_future::future::try_zip(
289                self.install_vanilla(version_name, &vanilla),
290                self.download_fabric_pre(version_name, &vanilla.id, fabric),
291            )
292            .await?;
293            self.download_fabric_post(version_name).await?;
294        } else if !quiltmc.is_empty() {
295            crate::prelude::inner_future::future::try_zip(
296                self.install_vanilla(version_name, &vanilla),
297                self.download_quiltmc_pre(version_name, &vanilla.id, quiltmc),
298            )
299            .await?;
300            self.download_quiltmc_post(version_name).await?;
301        } else if !forge.is_empty() {
302            self.install_vanilla(&vanilla.id, &vanilla).await?; // Forge 安装需要原版,如果安装器没有解析到则会从官方源下载,速度很慢
303            crate::prelude::inner_future::future::try_zip(
304                self.install_vanilla(version_name, &vanilla),
305                self.install_forge_pre(version_name, &vanilla.id, forge),
306            )
307            .await?;
308            self.install_forge_post(version_name, &vanilla.id, forge)
309                .await?;
310        } else {
311            self.install_vanilla(version_name, &vanilla).await?;
312        }
313        if !optifine.is_empty() {
314            if forge.is_empty() && fabric.is_empty() {
315                self.install_vanilla(&vanilla.id, &vanilla).await?; // Optifine 安装需要原版,如果安装器没有解析到则会从官方源下载,速度很慢
316            }
317            let (optifine_type, optifine_patch) =
318                optifine.split_at(optifine.find(' ').context("Optifine 版本字符串不合法!")?);
319            self.install_optifine(
320                version_name,
321                &vanilla.id,
322                optifine_type,
323                &optifine_patch[1..],
324                !forge.is_empty() || !fabric.is_empty(),
325            )
326            .await?;
327        }
328
329        // 这俩都需要安装器,而安装后会生成一个新的版本元数据
330        // 因此需要最后扫描一遍生成出来的版本元数据依赖,再进行一次下载
331        if !optifine.is_empty() || !forge.is_empty() {
332            let mut version_info = crate::version::structs::VersionInfo {
333                version_base: self.minecraft_version_path.to_owned(),
334                version: version_name.to_owned(),
335                ..Default::default()
336            };
337
338            if version_info
339                .load()
340                .await
341                .context("无法读取安装完成后的版本元数据!")
342                .is_ok()
343            {
344                if let Some(meta) = &mut version_info.meta {
345                    meta.fix_libraries();
346                    self.download_libraries(&meta.libraries).await?;
347                }
348            }
349        }
350        Ok(())
351    }
352}