1#![warn(missing_docs)]
2#![feature(iter_intersperse)]
3#![feature(iter_array_chunks)]
4
5use std::{collections::HashMap, io::Write, path::Path};
8
9use anyhow::{Ok, Result};
10use async_trait::async_trait;
11#[cfg(feature="mod_loaders")]
12use components::install::{fabriclike::FabricLikeInstaller, forge::ForgeInstaller, neoforge::NeoForgeInstaller, ComponentInstaller};
13#[cfg(feature="content_services")]
14use content_services::{ContentService, curseforge::CurseforgeContentService, modrinth::ModrinthContentService};
15use futures_util::StreamExt;
16use map_macro::hash_map_e;
17#[cfg(feature="msa_auth")]
18use minecraft::login::microsoft::MicrosoftAccountConstructor;
19use minecraft::{login::{yggdrasil::{ali::AuthlibInjectorAccountConstructor, mul::MinecraftUniversalLoginAccountConstructor}, AccountConstructor, OfflineAccountConstructor}, schemas::VersionJSON, version::MinecraftInstallation};
20use reqwest::Client;
21use tokio::{fs::{self, create_dir_all}, io::AsyncWriteExt};
22use tokio_util::codec::{FramedRead, LinesCodec};
23use utils::{osstr_concat, BetterPath};
24
25use crate::utils::merge_version_json;
26#[macro_use]
27extern crate rust_i18n;
28
29#[cfg(feature="content_services")]
30pub mod content_services;
31pub mod minecraft;
32pub mod utils;
33pub mod components;
34
35i18n!("locales");
36
37const CACHEDIR_TAG: &str = r"Signature: 8a477f597d28d172789f06886806bc55
38# This file is a cache directory tag created by a Minecraft launcher.
39# For information about cache directory tags, see:
40# http://www.brynosaurus.com/cachedir/";
41pub struct LauncherContext {
44 root_path: BetterPath,
45 #[cfg(feature="msa_auth")]
46 ms_client_id: String,
47 http_client: Client,
48 ui: Box<dyn UserInterface>,
49 #[cfg(feature="components_installation")]
50 pub(crate) component_installers: HashMap<String, Box<dyn ComponentInstaller>>,
51 #[cfg(feature="content_services")]
52 pub content_services: HashMap<String, Box<dyn ContentService>>,
54 pub account_types: HashMap<String, Box<dyn AccountConstructor>>,
56 pub download_retries: usize,
58 pub download_threads_per_file: u16,
60 pub download_parallel_files: usize,
62 pub bmclapi_mirror: Option<String>
64}
65
66#[async_trait]
70pub trait UserInterface: Send + Sync {
71 async fn ask_user(&self, questions: Vec<(&str, &str)>, msg: Option<&str>) -> Option<HashMap<String, String>>;
80
81 async fn ask_user_one(&self, question: &str, msg: Option<&str>) -> Option<String>;
83
84 async fn ask_user_choose(&self, choices: Vec<&str>, msg: &str) -> Option<usize>;
92
93 async fn info(&self, msg: &str, title: &str);
95
96 async fn warn(&self, msg: &str, title: &str);
98
99 async fn error(&self, msg: &str, title: &str);
101}
102
103pub struct StdioUserInterface;
106
107#[async_trait]
108impl UserInterface for StdioUserInterface {
109 async fn ask_user(&self, questions: Vec<(&str, &str)>, msg: Option<&str>) -> Option<HashMap<String, String>> {
110 if msg.is_some() {
111 println!("{}", msg.unwrap());
112 }
113 let mut res = HashMap::<String, String>::new();
114 let mut stdin = FramedRead::new(tokio::io::stdin(), LinesCodec::new());
115 for (k, v) in questions {
116 println!("{v}: ");
117 res.insert(k.to_string(), stdin.next().await.unwrap().unwrap());
118 }
119 Some(res)
120 }
121 async fn ask_user_one(&self, question: &str, msg: Option<&str>) -> Option<String> {
122 if msg.is_some() {
123 println!("{}", msg.unwrap());
124 }
125 println!("{question}: ");
126 let mut stdin = FramedRead::new(tokio::io::stdin(), LinesCodec::new());
127 return Some(stdin.next().await
128 .unwrap().unwrap().to_string());
129 }
130
131 async fn ask_user_choose(&self, choices: Vec<&str>, msg: &str) -> Option<usize> {
132 println!("{msg}");
133 let mut index = 0;
134 for i in choices {
135 println!("{index}. {i}");
136 index += 1;
137 }
138 println!("Please choose: ");
139 let mut stdin = FramedRead::new(tokio::io::stdin(), LinesCodec::new());
140 return Some(stdin.next().await.unwrap().unwrap().parse().unwrap());
141 }
142
143 async fn info(&self, msg: &str, title: &str) {
144 println!("INFO: {title} {msg}");
145 }
146 async fn warn(&self, msg: &str, title: &str) {
147 eprintln!("WARN: {title} {msg}");
148 }
149 async fn error(&self, msg: &str, title: &str) {
150 eprintln!("ERROR: {title} {msg}");
151 }
152}
153
154impl LauncherContext {
155
156 #[cfg_attr(feature="msa_auth", doc=r" * `ms_client_id` - The client id for Microsoft auth. See [Microsoft's document](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).")]
161 pub async fn new(mc_path: &Path, #[cfg(feature="msa_auth")] ms_client_id: &str, ui: impl UserInterface + 'static) -> Result<Self> {
163 let root_path: BetterPath;
164 if let Err(_) | Result::Ok(false) = tokio::fs::try_exists(&mc_path).await {
165 tokio::fs::create_dir_all(&mc_path).await?;
166 root_path = BetterPath(mc_path.to_path_buf().canonicalize()?);
167 tokio::fs::create_dir(&root_path / "libraries").await?;
168 tokio::fs::create_dir(&root_path / "assets").await?;
169 } else {
170 root_path = BetterPath(mc_path.to_path_buf().canonicalize()?);
171 if let Err(_) | Result::Ok(false) = tokio::fs::try_exists(&root_path / "libraries").await {
172 tokio::fs::create_dir_all(&root_path / "libraries").await?;
173 }
174 if let Err(_) | Result::Ok(false) = tokio::fs::try_exists(&root_path / "assets").await {
175 tokio::fs::create_dir(&root_path / "assets").await?;
176 }
177 }
178 tokio::fs::File::create(&root_path / "libraries" / "CACHEDIR.TAG").await?.write(CACHEDIR_TAG.as_bytes()).await?;
179 tokio::fs::File::create(&root_path / "assets" / "CACHEDIR.TAG").await?.write(CACHEDIR_TAG.as_bytes()).await?;
180 #[allow(unused_mut)]
181 let mut ctx = LauncherContext {
182 root_path,
183 #[cfg(feature="msa_auth")]
184 ms_client_id: ms_client_id.to_string(),
185 http_client: Client::builder().user_agent("heipiao233/dmclc5 (heipiao233@outlook.com)").build()?,
186 ui: Box::new(ui),
187 #[cfg(feature="mod_loaders")]
188 component_installers: hash_map_e!{
189 "forge".to_string() => Box::new(ForgeInstaller),
190 "neoforge".to_string() => Box::new(NeoForgeInstaller),
191 "fabric".to_string() => Box::new(FabricLikeInstaller::fabric()),
192 "quilt".to_string() => Box::new(FabricLikeInstaller::quilt()),
193 },
194 #[cfg(feature="content_services")]
195 content_services: hash_map_e! {
196 "curseforge".to_string() => Box::new(CurseforgeContentService),
197 "modrinth".to_string() => Box::new(ModrinthContentService)
198 },
199 #[cfg(feature="msa_auth")]
200 account_types: hash_map_e! {
201 "offline".to_string() => Box::new(OfflineAccountConstructor),
202 "microsoft".to_string() => Box::new(MicrosoftAccountConstructor),
203 "minecraft_universal_login".to_string() => Box::new(MinecraftUniversalLoginAccountConstructor),
204 "authlib_injector".to_string() => Box::new(AuthlibInjectorAccountConstructor)
205 },
206 #[cfg(not(feature="msa_auth"))]
207 account_types: hash_map_e! {
208 "offline".to_string() => Box::new(OfflineAccountConstructor),
209 "minecraft_universal_login".to_string() => Box::new(MinecraftUniversalLoginAccountConstructor),
210 "authlib_injector".to_string() => Box::new(AuthlibInjectorAccountConstructor)
211 },
212 download_retries: 5,
213 download_threads_per_file: 8,
214 download_parallel_files: 8,
215 bmclapi_mirror: None
216 };
217 Ok(ctx)
218 }
219
220 pub async fn list_installations(&self) -> Result<Vec<String>> {
222 let version_dir = &*(&self.root_path / "versions");
223 let mut ret = Vec::new();
224 create_dir_all(version_dir).await?;
225 for i in std::fs::read_dir((&self.root_path / "versions").0)? {
226 let dir = i?;
227 if !dir.file_type()?.is_dir() {
228 continue;
229 }
230 let json_path = version_dir / dir.file_name() / osstr_concat(&dir.file_name(), &".json".to_string());
231 let m = std::fs::metadata(&json_path);
232 if let Err(_) = m {
233 continue;
234 }
235 if !m.unwrap().is_file() {
236 continue;
237 }
238 ret.push(dir.file_name().to_string_lossy().to_string());
239 }
240 Ok(ret)
241 }
242
243 pub async fn get_installation(&self, name: &str) -> Option<MinecraftInstallation<'_>> {
245 let version_dir = &*(&self.root_path / "versions" / name);
246 let meta = fs::metadata(version_dir).await;
247 if meta.is_err() || !meta.unwrap().is_dir() {
248 return None;
249 }
250 let json = fs::read(version_dir / (name.to_string() + ".json")).await.ok()?;
251 let json = serde_json::from_slice(&json).ok()?;
252 let json: VersionJSON = self.resolve_inherits_from(json).await;
253 Some(MinecraftInstallation::new(self, json, name, None))
254 }
255
256 async fn resolve_inherits_from(&self, base: VersionJSON) -> VersionJSON {
257 let mut current = base;
258 while let Some(father) = ¤t.get_base().inherits_from {
259 let version_dir = &*(&self.root_path / "versions" / father);
260 let father = fs::read(version_dir / (father.to_string() + ".json")).await.ok();
261 if let None = father {
262 return current;
263 }
264 let father = serde_json::from_slice(&father.unwrap());
265 if let Err(_) = father {
266 return current;
267 }
268 current = if let Result::Ok(current) = merge_version_json(&father.unwrap(), ¤t) {
269 current
270 } else {
271 return current;
272 };
273 }
274 return current;
275 }
276
277 pub fn set_root_path(&mut self, root_path: &Path) -> Result<()> {
279 self.root_path = BetterPath(root_path.to_path_buf().canonicalize()?);
280 std::fs::File::create(&self.root_path / "libraries" / "CACHEDIR.TAG")?.write(CACHEDIR_TAG.as_bytes())?;
281 std::fs::File::create(&self.root_path / "assets" / "CACHEDIR.TAG")?.write(CACHEDIR_TAG.as_bytes())?;
282 Ok(())
283 }
284}