dmclc5/
lib.rs

1#![warn(missing_docs)]
2#![feature(iter_intersperse)]
3#![feature(iter_array_chunks)]
4
5//! A Minecraft launcher library.
6
7use 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/";
41/// The core struct for DMCLC.
42/// It contains everything we need.
43pub 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    /// A HashMap of [ContentService]s.
53    pub content_services: HashMap<String, Box<dyn ContentService>>,
54    /// A HashMap of [AccountConstructor]s.
55    pub account_types: HashMap<String, Box<dyn AccountConstructor>>,
56    /// Max download retry times.
57    pub download_retries: usize,
58    /// Max download threads per file.
59    pub download_threads_per_file: u16,
60    /// Max parallel downloading files.
61    pub download_parallel_files: usize,
62    /// BMCLAPI mirror.
63    pub bmclapi_mirror: Option<String>
64}
65
66/// A trait for interacting with users that should be implemented by the client.
67/// # Examples
68/// See [StdioUserInterface] for example.
69#[async_trait]
70pub trait UserInterface: Send + Sync {
71    /// Asks the user some questions.
72    /// 
73    /// # Arguments
74    /// * `questions` - A vector of questions. The first element in the tuple is the keys of the return value. and the second is what you should show to your user.
75    /// * `msg` - An optional message to user.
76    /// 
77    /// # Returns
78    /// A HashMap. The keys are the first element of each item in the argument `questions`, the values is the answers from the user for each questions.
79    async fn ask_user(&self, questions: Vec<(&str, &str)>, msg: Option<&str>) -> Option<HashMap<String, String>>;
80
81    /// Asks the user a question.
82    async fn ask_user_one(&self, question: &str, msg: Option<&str>) -> Option<String>;
83
84    /// Asks the user to choose one choice.
85    /// 
86    /// # Arguments
87    /// * `msg` - The question.
88    /// 
89    /// # Returns
90    /// The index of `choices`.
91    async fn ask_user_choose(&self, choices: Vec<&str>, msg: &str) -> Option<usize>;
92
93    /// Shows a information to the user.
94    async fn info(&self, msg: &str, title: &str);
95
96    /// Shows a warning to the user.
97    async fn warn(&self, msg: &str, title: &str);
98
99    /// Shows an error to the user.
100    async fn error(&self, msg: &str, title: &str);
101}
102
103/// An example implementation of [UserInterface]
104/// It is for some simple use cases.
105pub 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    /// Creates a new [LauncherContext].
157    /// 
158    /// # Arguments
159    /// * `root_path` - The `.minecraft` directory.
160    #[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    /// * `ui` - Your implementation of [UserInterface].
162    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    /// List the names of minecraft installations in the `root_path`.
221    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    /// Get one [MinecraftInstallation] by name in the `root_path`.
244    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) = &current.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(), &current) {
269                current
270            } else {
271                return current;
272            };
273        }
274        return current;
275    }
276
277    /// Set a new `root_path`.
278    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}