1pub mod classpath;
2pub mod manifest;
3pub mod rules;
4
5use manifest::{read_manifest_from_file, JvmArgument};
6use rules::is_all_rules_satisfied;
7use std::{path::PathBuf, process::Command};
8use thiserror::Error;
9
10#[derive(Error, Debug)]
11pub enum ClientBootstrapError {
12 #[error("The game directory doesn't exist.")]
13 GameDirNotExist,
14
15 #[error("The java bin doesn't exist.")]
16 JavaBinNotExist,
17
18 #[error("The version file (.json) doesn't exist.")]
19 VersionFileNotFound,
20
21 #[error("An unexpected error has ocurred.")]
22 UnknownError,
23
24 #[error("{0}")]
25 Json(#[from] serde_json::Error),
26}
27
28pub struct ClientAuth {
29 pub access_token: Option<String>,
30 pub username: String,
31 pub uuid: Option<String>,
32}
33
34pub struct ClientVersion {
35 pub version: String,
36 pub version_type: String,
37}
38
39pub struct ClientSettings {
40 pub assets: PathBuf,
41 pub auth: ClientAuth,
42 pub game_dir: PathBuf,
43 pub java_bin: PathBuf,
44 pub libraries_dir: PathBuf,
45 pub manifest_file: PathBuf,
46 pub natives_dir: PathBuf,
47 pub version: ClientVersion,
48 pub version_jar_file: PathBuf,
49}
50
51pub struct ClientBootstrap {
52 pub settings: ClientSettings,
53}
54
55impl ClientBootstrap {
56 pub fn new(settings: ClientSettings) -> Self {
57 Self { settings }
58 }
59
60 pub fn get_assets_dir(&self) -> PathBuf {
61 return self.settings.assets.clone();
62 }
63
64 pub fn get_game_dir(&self) -> PathBuf {
65 return self.settings.game_dir.clone();
66 }
67
68 pub fn get_json_file(&self) -> PathBuf {
69 return self.settings.manifest_file.clone();
70 }
71
72 pub fn get_jar_file(&self) -> PathBuf {
73 return self.settings.version_jar_file.clone();
74 }
75
76 pub fn get_libs_dir(&self) -> PathBuf {
77 return self.settings.libraries_dir.clone();
78 }
79
80 pub fn get_natives_dir(&self) -> PathBuf {
81 return self.settings.natives_dir.clone();
82 }
83
84 pub fn build_args(&self) -> Result<Vec<String>, ClientBootstrapError> {
85 let auth = &self.settings.auth;
86 let assets_dir = self.get_assets_dir();
87 let game_dir = self.get_game_dir();
88 let java_bin = self.settings.java_bin.clone();
89 let json_file = self.get_json_file();
90 let natives_dir = self.get_natives_dir();
91 let version = &self.settings.version;
92
93 if !game_dir.is_dir() {
94 return Err(ClientBootstrapError::GameDirNotExist);
95 }
96
97 if !java_bin.is_file() {
98 return Err(ClientBootstrapError::JavaBinNotExist);
99 }
100
101 if !json_file.is_file() {
102 return Err(ClientBootstrapError::VersionFileNotFound);
103 }
104
105 let manifest = read_manifest_from_file(json_file).unwrap();
106
107 let assets_index = &manifest.asset_index.id;
108 let classpath = classpath::create_classpath(
109 self.get_jar_file(),
110 self.get_libs_dir(),
111 manifest.libraries,
112 );
113
114 let mut args: Vec<String> = vec![];
115
116 for arg in manifest.arguments.jvm {
117 match arg {
118 JvmArgument::String(value) => {
119 args.push(value);
120 }
121 JvmArgument::Struct { value, rules, .. } => {
122 if !is_all_rules_satisfied(&rules) {
123 continue;
124 }
125
126 if let Some(value) = value.as_str() {
127 args.push(value.to_string());
128 } else if let Some(value_arr) = value.as_array() {
129 for value in value_arr {
130 if let Some(value) = value.as_str() {
131 args.push(value.to_string());
132 }
133 }
134 }
135 }
136 }
137 }
138
139 args.push(manifest.main_class);
140
141 for arg in manifest.arguments.game {
142 match arg {
143 JvmArgument::String(value) => {
144 args.push(value);
145 }
146 JvmArgument::Struct { value, rules, .. } => {
147 if !is_all_rules_satisfied(&rules) {
148 continue;
149 }
150
151 if let Some(value) = value.as_str() {
152 args.push(value.to_string());
153 } else if let Some(value_arr) = value.as_array() {
154 for value in value_arr {
155 if let Some(value) = value.as_str() {
156 args.push(value.to_string());
157 }
158 }
159 }
160 }
161 }
162 }
163
164 args = args
165 .iter()
166 .map(|x| {
167 x.replace("${assets_root}", &assets_dir.to_str().unwrap())
168 .replace("${game_directory}", &game_dir.to_str().unwrap())
169 .replace("${natives_directory}", &natives_dir.to_str().unwrap())
170 .replace("${launcher_name}", "minecraft-rs/bootstrap")
171 .replace("${launcher_version}", "0.1.1")
172 .replace(
173 "${auth_access_token}",
174 auth.access_token
175 .clone()
176 .unwrap_or("null".to_string())
177 .as_str(),
178 )
179 .replace("${auth_player_name}", auth.username.as_str())
180 .replace(
181 "${auth_uuid}",
182 auth.uuid.clone().unwrap_or("null".to_string()).as_str(),
183 )
184 .replace("${version_type}", &version.version_type)
185 .replace("${version_name}", &version.version)
186 .replace("${assets_index_name}", &assets_index)
187 .replace("${user_properties}", "{}")
188 .replace("${classpath}", &classpath)
189 })
190 .collect();
191
192 return Ok(args);
193 }
194
195 pub fn launch(&self) -> Result<i32, ClientBootstrapError> {
196 let args = self.build_args().unwrap();
197
198 let mut process = Command::new(&self.settings.java_bin)
199 .args(args)
200 .current_dir(&self.settings.game_dir)
201 .spawn()
202 .expect("command failed to start");
203
204 let status = process.wait().unwrap().code().unwrap();
205 return Ok(status);
206 }
207}