1use std::path::{Path, PathBuf};
2use tokio::fs;
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 download::{download_batch, download_file, DownloadTask},
7 error::{HexoError, Result},
8 version::manifest::{
9 fetch_asset_index, fetch_version_manifest, fetch_version_json,
10 check_library_rule, check_jvm_rule,
11 Argument, ArgumentValue, VersionJson,
12 },
13};
14
15#[derive(Debug, Deserialize, Serialize, Clone)]
16pub struct LibEntry {
17 pub name: String,
18 pub path: PathBuf,
19 pub sha1: String,
20}
21
22#[derive(Debug, Deserialize, Serialize, Clone)]
23pub struct NativeEntry {
24 pub name: String,
25 pub path: PathBuf,
26 pub sha1: String,
27}
28
29#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
30#[serde(rename_all = "lowercase")]
31pub enum LoaderType {
32 Vanilla,
33 Fabric,
34 Forge,
35 NeoForge,
36}
37
38#[derive(Debug, Deserialize, Serialize, Clone)]
39pub struct InstanceConfig {
40 pub version_id: String,
41 pub loader_type: LoaderType,
42 pub main_class: String,
43 pub start_args: Vec<String>,
45 pub lib_list: Vec<LibEntry>,
46 pub natives: Vec<NativeEntry>,
47 pub assets_id: String,
48 pub java_version: u32,
49}
50
51impl InstanceConfig {
52 pub fn save_path(instance_dir: &Path) -> PathBuf {
53 instance_dir.join("instance_config.json")
54 }
55
56 pub async fn save(&self, instance_dir: &Path) -> Result<()> {
57 let json = serde_json::to_string_pretty(self)?;
58 fs::write(Self::save_path(instance_dir), json).await?;
59 Ok(())
60 }
61
62 pub async fn load(instance_dir: &Path) -> Result<Self> {
63 let data = fs::read_to_string(Self::save_path(instance_dir)).await?;
64 let config: Self = serde_json::from_str(&data)?;
65 Ok(config)
66 }
67}
68
69pub async fn install_vanilla<F>(
76 version_id: &str,
77 instance_name: &str,
78 base_dir: &Path,
79 progress: F,
80) -> Result<InstanceConfig>
81where
82 F: Fn(usize, usize, &str) + Send + Sync + Clone + 'static,
83{
84 let manifest = fetch_version_manifest().await?;
85 let entry = manifest
86 .versions
87 .iter()
88 .find(|v| v.id == version_id)
89 .ok_or_else(|| HexoError::VersionNotFound(version_id.to_string()))?;
90
91 progress(0, 4, "fetching version info");
92 let version_json = fetch_version_json(&entry.url).await?;
93
94 let instance_dir = base_dir.join("instance").join(instance_name);
95 let lib_dir = base_dir.join("libraries");
96 let assets_dir = base_dir.join("assets");
97
98 init_instance_dirs(&instance_dir, &assets_dir, &lib_dir).await?;
99
100 progress(1, 4, "downloading libraries");
101 let (lib_list, natives) = download_libraries(&version_json, &lib_dir, progress.clone()).await?;
102
103 progress(2, 4, "downloading assets");
104 download_assets(&version_json, &assets_dir, progress.clone()).await?;
105
106 progress(3, 4, "downloading client jar");
107 let client_jar = instance_dir.join(format!("{}.jar", version_id));
108 download_file(
109 &DownloadTask::new(
110 version_json.downloads.client.url.clone(),
111 &client_jar,
112 )
113 .with_sha1(version_json.downloads.client.sha1.clone()),
114 )
115 .await?;
116
117 progress(4, 4, "resolving launch arguments");
118 let start_args = parse_start_args(&version_json);
119
120 let config = InstanceConfig {
121 version_id: version_id.to_string(),
122 loader_type: LoaderType::Vanilla,
123 main_class: version_json.main_class.clone(),
124 start_args,
125 lib_list,
126 natives,
127 assets_id: version_json.asset_index.id.clone(),
128 java_version: version_json.java_version.major_version,
129 };
130
131 config.save(&instance_dir).await?;
132
133 Ok(config)
134}
135
136async fn init_instance_dirs(
137 instance_dir: &Path,
138 assets_dir: &Path,
139 lib_dir: &Path,
140) -> Result<()> {
141 let minecraft_dir = instance_dir.join(".minecraft");
142 for dir in &[
143 instance_dir.to_path_buf(),
144 minecraft_dir.join("mods"),
145 minecraft_dir.join("resourcepacks"),
146 minecraft_dir.join("saves"),
147 minecraft_dir.join("screenshots"),
148 minecraft_dir.join("shaderpacks"),
149 minecraft_dir.join("logs"),
150 assets_dir.join("objects"),
151 assets_dir.join("indexes"),
152 lib_dir.to_path_buf(),
153 ] {
154 fs::create_dir_all(dir).await?;
155 }
156 Ok(())
157}
158
159async fn download_libraries(
160 version_json: &VersionJson,
161 lib_dir: &Path,
162 progress: impl Fn(usize, usize, &str) + Send + Sync + Clone + 'static,
163) -> Result<(Vec<LibEntry>, Vec<NativeEntry>)> {
164 let mut lib_tasks: Vec<(DownloadTask, LibEntry)> = Vec::new();
165 let mut native_tasks: Vec<(DownloadTask, NativeEntry)> = Vec::new();
166
167 for lib in &version_json.libraries {
168 if let Some(rules) = &lib.rules {
169 if !check_library_rule(rules) {
170 continue;
171 }
172 }
173
174 if let Some(downloads) = &lib.downloads {
175 if let Some(artifact) = &downloads.artifact {
176 let path = lib_dir.join(&artifact.path);
177 lib_tasks.push((
178 DownloadTask::new(&artifact.url, &path)
179 .with_sha1(artifact.sha1.clone()),
180 LibEntry {
181 name: lib.name.clone(),
182 path: path.clone(),
183 sha1: artifact.sha1.clone(),
184 },
185 ));
186 }
187
188 if let Some(natives_map) = &lib.natives {
189 let native_key = native_key_for_platform();
190 if let Some(classifier_key) = natives_map.get(native_key) {
191 if let Some(classifiers) = &downloads.classifiers {
192 if let Some(native_artifact) = classifiers.get(classifier_key) {
193 let path = lib_dir.join(&native_artifact.path);
194 native_tasks.push((
195 DownloadTask::new(&native_artifact.url, &path)
196 .with_sha1(native_artifact.sha1.clone()),
197 NativeEntry {
198 name: lib.name.clone(),
199 path: path.clone(),
200 sha1: native_artifact.sha1.clone(),
201 },
202 ));
203 }
204 }
205 }
206 }
207 }
208 }
209
210 let total = lib_tasks.len() + native_tasks.len();
211 let p = progress.clone();
212
213 let lib_download_tasks: Vec<DownloadTask> =
214 lib_tasks.iter().map(|(t, _)| t.clone()).collect();
215 let native_download_tasks: Vec<DownloadTask> =
216 native_tasks.iter().map(|(t, _)| t.clone()).collect();
217
218 download_batch(lib_download_tasks, 16, move |done, _| {
219 p(done, total, "downloading libraries");
220 })
221 .await?;
222
223 download_batch(native_download_tasks, 8, move |done, _| {
224 progress(done, total, "downloading native libraries");
225 })
226 .await?;
227
228 Ok((
229 lib_tasks.into_iter().map(|(_, e)| e).collect(),
230 native_tasks.into_iter().map(|(_, e)| e).collect(),
231 ))
232}
233
234async fn download_assets(
235 version_json: &VersionJson,
236 assets_dir: &Path,
237 progress: impl Fn(usize, usize, &str) + Send + Sync + Clone + 'static,
238) -> Result<()> {
239 let index = &version_json.asset_index;
240
241 let index_path = assets_dir
242 .join("indexes")
243 .join(format!("{}.json", index.id));
244
245 let asset_data = fetch_asset_index(&index.url).await?;
246 let json = serde_json::to_string_pretty(&asset_data)?;
247 fs::write(&index_path, json).await?;
248
249 let tasks: Vec<DownloadTask> = asset_data
250 .objects
251 .values()
252 .map(|obj| DownloadTask::asset(&obj.hash, assets_dir))
253 .collect();
254
255 let total = tasks.len();
256 download_batch(tasks, 32, move |done, _| {
257 progress(done, total, "downloading assets");
258 })
259 .await
260}
261
262pub fn parse_start_args(version_json: &VersionJson) -> Vec<String> {
264 let mut args: Vec<String> = Vec::new();
265
266 if let Some(arguments) = &version_json.arguments {
267 for arg in &arguments.jvm {
268 collect_arg(arg, &mut args, true);
269 }
270 args.push("${mainClass}".to_string());
272 for arg in &arguments.game {
273 collect_arg(arg, &mut args, false);
274 }
275 } else if let Some(mc_args) = &version_json.minecraft_arguments {
276 args.extend(mc_args.split_whitespace().map(|s| s.to_string()));
278 }
279
280 args
281}
282
283fn collect_arg(arg: &Argument, out: &mut Vec<String>, is_jvm: bool) {
284 match arg {
285 Argument::Simple(s) => out.push(s.clone()),
286 Argument::Conditional { rules, value } => {
287 let allowed = if is_jvm {
288 check_jvm_rule(rules)
289 } else {
290 check_library_rule(rules)
291 };
292 if allowed {
293 match value {
294 ArgumentValue::Single(s) => out.push(s.clone()),
295 ArgumentValue::Multiple(vs) => out.extend(vs.iter().cloned()),
296 }
297 }
298 }
299 }
300}
301
302fn native_key_for_platform() -> &'static str {
303 if cfg!(target_os = "windows") {
304 "windows"
305 } else if cfg!(target_os = "macos") {
306 "osx"
307 } else {
308 "linux"
309 }
310}