├── .gitignore ├── Cargo.toml ├── README.md ├── src ├── lib.rs ├── yggdrasil │ └── mod.rs ├── requests │ └── mod.rs ├── parsing │ └── mod.rs ├── launcher │ └── mod.rs └── versions │ └── mod.rs └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # Whitelist Mode 2 | /* 3 | 4 | # Git Files 5 | !.gitignore 6 | 7 | # GitHub Files 8 | !README.md 9 | !LICENSE 10 | 11 | # Cargo Files 12 | !Cargo.toml 13 | 14 | # Rust Source 15 | !src/ 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rmcll" 3 | version = "0.1.0" 4 | authors = ["Yanbing Zhao "] 5 | 6 | [dependencies] 7 | futures = "0.1" 8 | hyper = "0.11" 9 | hyper-tls = "0.1" 10 | serde = "1.0" 11 | serde_derive = "1.0" 12 | serde_json = "1.0" 13 | tokio-core = "0.1" 14 | uuid = { version = "0.4", features = ["serde", "v4", "v5"] } 15 | zip = "0.2" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RMCLL - Rust MineCraft Launcher Library 2 | 3 | **Still work in progress!** 4 | 5 | An example for launching a minecraft 1.12.2 client in your home directory: 6 | 7 | ```rust 8 | fn main() { 9 | use std::env; 10 | use rmcll::launcher; 11 | use rmcll::yggdrasil::{self, Authenticator}; 12 | // prepare for starting minecraft client process 13 | let game_dir = env::home_dir().unwrap().join(".minecraft/"); 14 | let game_auth_info = yggdrasil::offline("zzzz").auth().unwrap(); 15 | let launcher = launcher::create(game_dir, game_auth_info); 16 | let args = launcher.to_arguments("1.12.2").unwrap(); 17 | // start the 1.12.2 client now 18 | println!("\nStarting minecraft with: {} {:?}", args.program(), args.args()); 19 | let minecraft_process = args.start().unwrap(); 20 | let output = minecraft_process.wait_with_output().unwrap(); 21 | let exit_code = output.status.code().unwrap(); 22 | println!("\nMinecraft client finished with exit code {}", exit_code); 23 | } 24 | ``` 25 | 26 | License: [Apache 2.0](LICENSE) 27 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate futures; 2 | extern crate hyper; 3 | extern crate hyper_tls; 4 | extern crate serde; 5 | #[macro_use] 6 | extern crate serde_json; 7 | #[macro_use] 8 | extern crate serde_derive; 9 | extern crate tokio_core; 10 | extern crate uuid; 11 | extern crate zip; 12 | 13 | pub mod launcher; 14 | pub mod parsing; 15 | pub mod requests; 16 | pub mod versions; 17 | pub mod yggdrasil; 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | #[test] 22 | fn start_minecraft() { 23 | use std::env; 24 | use launcher; 25 | use yggdrasil::{self, Authenticator}; 26 | let game_dir = env::home_dir().unwrap().join(".minecraft/"); 27 | let game_auth_info = yggdrasil::offline("zzzz").auth().unwrap(); 28 | let launcher = launcher::create(game_dir, game_auth_info); 29 | let args = launcher.to_arguments("1.12.2").unwrap(); 30 | println!("\nStarting minecraft with: {} {:?}", args.program(), args.args()); 31 | let minecraft_process = args.start().unwrap(); 32 | let output = minecraft_process.wait_with_output().unwrap(); 33 | let exit_code = output.status.code().unwrap(); 34 | println!("\nMinecraft client finished with exit code {}", exit_code); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/yggdrasil/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::fmt::{self, Display}; 4 | use std::collections::HashMap; 5 | 6 | use uuid::{Uuid, NAMESPACE_OID}; 7 | use serde_json; 8 | 9 | use requests; 10 | 11 | #[derive(Debug)] 12 | pub struct Profile { 13 | uuid: Uuid, 14 | name: String, 15 | properties: HashMap, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct AuthInfo { 20 | access_token: Uuid, 21 | user_profile: Profile, 22 | } 23 | 24 | pub struct OfflineAuthenticator(String); 25 | 26 | pub struct YggdrasilLoginAuthenticator { 27 | username: String, 28 | password: String, 29 | client_token: Uuid, 30 | } 31 | 32 | pub trait Authenticator { 33 | type Error; 34 | 35 | fn auth(&self) -> Result; 36 | } 37 | 38 | impl Profile { 39 | #[inline] 40 | pub fn new(uuid: Uuid, name: String, properties: HashMap) -> Profile { 41 | Profile { uuid, name, properties } 42 | } 43 | 44 | #[inline] 45 | pub fn uuid(&self) -> &Uuid { 46 | &self.uuid 47 | } 48 | 49 | #[inline] 50 | pub fn name(&self) -> &String { 51 | &self.name 52 | } 53 | 54 | #[inline] 55 | pub fn properties(&self) -> &HashMap { 56 | &self.properties 57 | } 58 | } 59 | 60 | impl Display for Profile { 61 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 62 | if self.properties.is_empty() { 63 | write!(f, "{}: {}", self.name, self.uuid.simple()) 64 | } else { 65 | write!(f, "{}: {} {}", self.name, self.uuid.simple(), 66 | serde_json::to_string(&self.properties).unwrap()) 67 | } 68 | } 69 | } 70 | 71 | impl AuthInfo { 72 | #[inline] 73 | pub fn new(access_token: Uuid, user_profile: Profile) -> AuthInfo { 74 | AuthInfo { access_token, user_profile } 75 | } 76 | 77 | #[inline] 78 | pub fn access_token(&self) -> &Uuid { 79 | &self.access_token 80 | } 81 | 82 | #[inline] 83 | pub fn user_profile(&self) -> &Profile { 84 | &self.user_profile 85 | } 86 | } 87 | 88 | impl Authenticator for OfflineAuthenticator { 89 | type Error = requests::Error; 90 | 91 | fn auth(&self) -> Result { 92 | let access_token = Uuid::new_v4(); 93 | let uuid = Uuid::new_v5(&NAMESPACE_OID, self.0.as_str()); 94 | let profile = Profile::new(uuid, self.0.clone(), HashMap::new()); 95 | Result::Ok(AuthInfo::new(access_token, profile)) 96 | } 97 | } 98 | 99 | impl Authenticator for YggdrasilLoginAuthenticator { 100 | type Error = requests::Error; 101 | 102 | fn auth(&self) -> Result { 103 | let username = self.username.as_str(); 104 | let password = self.password.as_str(); 105 | let (token, profile) = requests::req_authenticate(username, password, &self.client_token)?; 106 | Result::Ok(AuthInfo::new(token, profile)) 107 | } 108 | } 109 | 110 | #[inline] 111 | pub fn offline(offline_name: &str) -> OfflineAuthenticator { 112 | OfflineAuthenticator(offline_name.to_owned()) 113 | } 114 | 115 | #[inline] 116 | pub fn yggdrasil(username: &str, password: &str) -> YggdrasilLoginAuthenticator { 117 | yggdrasil_with_client_token(username.to_owned(), password.to_owned(), Uuid::new_v4()) 118 | } 119 | 120 | #[inline] 121 | pub fn yggdrasil_with_client_token(username: String, 122 | password: String, 123 | client_token: Uuid) -> YggdrasilLoginAuthenticator { 124 | YggdrasilLoginAuthenticator { username, password, client_token } 125 | } 126 | -------------------------------------------------------------------------------- /src/requests/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::fmt; 4 | use std::error; 5 | use std::result::Result; 6 | use std::collections::HashMap; 7 | 8 | use uuid::Uuid; 9 | use serde_json; 10 | use hyper::error::UriError; 11 | use hyper::client::FutureResponse; 12 | use hyper::header::{ContentType, ContentLength}; 13 | use hyper::{Client, Method, Request, Error as HyperError}; 14 | use hyper_tls::HttpsConnector; 15 | use tokio_core::reactor::{Core, Handle}; 16 | use futures::{Poll, Future, Stream, IntoFuture}; 17 | 18 | use versions; 19 | use yggdrasil; 20 | 21 | #[derive(Debug)] 22 | pub enum Error { 23 | UnrecognizedJson(String), 24 | NetworkIOError(Box), 25 | } 26 | 27 | pub struct RequestFuture(Box>); 28 | 29 | impl From for Error { 30 | fn from(e: serde_json::Error) -> Self { 31 | Error::NetworkIOError(Box::new(e)) 32 | } 33 | } 34 | 35 | impl From for Error { 36 | fn from(e: UriError) -> Self { 37 | Error::NetworkIOError(Box::new(e)) 38 | } 39 | } 40 | 41 | impl From for Error { 42 | fn from(e: HyperError) -> Self { 43 | Error::NetworkIOError(Box::new(e)) 44 | } 45 | } 46 | 47 | impl fmt::Display for Error { 48 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 49 | match *self { 50 | Error::UnrecognizedJson(ref s) => fmt::Display::fmt(s, f), 51 | Error::NetworkIOError(ref e) => fmt::Display::fmt(e, f), 52 | } 53 | } 54 | } 55 | 56 | impl RequestFuture { 57 | fn new + 'static>(future: F) -> RequestFuture { 58 | RequestFuture(Box::new(future)) 59 | } 60 | } 61 | 62 | impl Future for RequestFuture { 63 | type Item = T; 64 | type Error = Error; 65 | 66 | fn poll(&mut self) -> Poll { 67 | self.0.as_mut().poll() 68 | } 69 | } 70 | 71 | fn make_json_https_request(handle: Handle, 72 | url: &str, 73 | json_value: serde_json::Value) -> Result { 74 | let connector = HttpsConnector::new(4, &handle).unwrap(); 75 | let client = Client::configure().connector(connector).keep_alive(true).build(&handle); 76 | 77 | let request = match json_value { 78 | serde_json::Value::Null => Request::new(Method::Get, url.parse()?), 79 | _ => { 80 | let json = json_value.to_string(); 81 | let mut req = Request::new(Method::Post, url.parse()?); 82 | req.headers_mut().set(ContentType::json()); 83 | req.headers_mut().set(ContentLength(json.len() as u64)); 84 | req.set_body(json); 85 | req 86 | } 87 | }; 88 | 89 | Result::Ok(client.request(request)) 90 | } 91 | 92 | fn make_json_request(handle: Handle, 93 | url: &str, 94 | json_value: serde_json::Value) -> RequestFuture { 95 | RequestFuture::new(make_json_https_request(handle, url, json_value).into_future().and_then(|req| { 96 | req.map_err(Error::from).and_then(|res| { 97 | res.body().concat2().map_err(Error::from).and_then(|body| { 98 | serde_json::from_slice(&body).map_err(Error::from).into_future() 99 | }) 100 | }) 101 | })) 102 | } 103 | 104 | pub fn req_authenticate(username: &str, 105 | password: &str, 106 | client_token: &Uuid) -> Result<(Uuid, yggdrasil::Profile), Error> { 107 | let mut core = Core::new().unwrap(); 108 | 109 | let req = make_json_request(core.handle(), "https://authserver.mojang.com/authenticate", json!({ 110 | "username": username, 111 | "password": password, 112 | "clientToken": client_token.simple().to_string(), 113 | "agent": { "name": "Minecraft", "version": 1 } 114 | })); 115 | 116 | core.run(req.map(|json| { 117 | let error = || Error::UnrecognizedJson(json.to_string()); 118 | let uuid = Uuid::parse_str(json["selectedProfile"]["id"].as_str().ok_or(error())?).map_err(|_| error())?; 119 | let name = json["selectedProfile"]["name"].as_str().ok_or(error())?.to_owned(); 120 | let properties = HashMap::new(); // TODO: deserialize properties 121 | let access_token_string = json["accessToken"].as_str().ok_or(error())?; 122 | let access_token = Uuid::parse_str(access_token_string).map_err(|_| error())?; 123 | Result::Ok((access_token, yggdrasil::Profile::new(uuid, name, properties))) 124 | }))? 125 | } 126 | 127 | pub fn req_refresh(access_token: &Uuid, 128 | client_token: &Uuid) -> Result<(Uuid, yggdrasil::Profile), Error> { 129 | let mut core = Core::new().unwrap(); 130 | 131 | let req = make_json_request(core.handle(), "https://authserver.mojang.com/refresh", json!({ 132 | "accessToken": access_token.simple().to_string(), 133 | "clientToken": client_token.simple().to_string() 134 | })); 135 | 136 | core.run(req.map(|json| { 137 | let error = || Error::UnrecognizedJson(json.to_string()); 138 | let uuid = Uuid::parse_str(json["selectedProfile"]["id"].as_str().ok_or(error())?).map_err(|_| error())?; 139 | let name = json["selectedProfile"]["name"].as_str().ok_or(error())?.to_owned(); 140 | let properties = HashMap::new(); // TODO: deserialize properties 141 | let access_token_string = json["accessToken"].as_str().ok_or(error())?; 142 | let access_token = Uuid::parse_str(access_token_string).map_err(|_| error())?; 143 | Result::Ok((access_token, yggdrasil::Profile::new(uuid, name, properties))) 144 | }))? 145 | } 146 | 147 | pub fn req_versions() -> Result { 148 | let mut core = Core::new().unwrap(); 149 | let url = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; 150 | 151 | let req = make_json_request(core.handle(), url, serde_json::Value::Null); 152 | 153 | core.run(req) 154 | } 155 | 156 | pub fn req_deserialize_version(url: &str) -> Result { 157 | let mut core = Core::new().unwrap(); 158 | 159 | let req = make_json_request(core.handle(), url, serde_json::Value::Null); 160 | 161 | core.run(req.map(|json| { 162 | Result::Ok(serde_json::from_value(json.clone()).unwrap()) 163 | }))? 164 | } 165 | -------------------------------------------------------------------------------- /src/parsing/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::rc::Rc; 4 | 5 | #[derive(Clone)] 6 | pub enum ParameterStrategy { 7 | Ignore, 8 | Map(Rc String>), 9 | } 10 | 11 | pub struct ArgumentIterator<'a> { 12 | strategy: &'a ParameterStrategy, 13 | chars: Vec, 14 | index: usize, 15 | } 16 | 17 | impl ParameterStrategy { 18 | pub fn ignore() -> ParameterStrategy { 19 | ParameterStrategy::Ignore 20 | } 21 | 22 | pub fn map String + 'static>(function: F) -> ParameterStrategy { 23 | ParameterStrategy::Map(Rc::new(function)) 24 | } 25 | } 26 | 27 | impl<'a> Iterator for ArgumentIterator<'a> { 28 | type Item = String; 29 | 30 | fn next(&mut self) -> Option { 31 | let (index, result) = parse_whole_string(&self.chars, self.index, &self.strategy); 32 | self.index = index; 33 | result 34 | } 35 | } 36 | 37 | pub fn parse<'a>(string: &str, strategy: &'a ParameterStrategy) -> ArgumentIterator<'a> { 38 | ArgumentIterator { strategy, chars: string.chars().collect(), index: 0 } 39 | } 40 | 41 | fn parse_whole_string(chars: &Vec, original_pos: usize, strategy: &ParameterStrategy) -> (usize, Option) { 42 | let mut index = original_pos; 43 | let mut result: String = String::new(); 44 | while let Some(c) = chars.get(index) { if c.is_whitespace() { index += 1; } else { break; } } 45 | loop { 46 | match chars.get(index) { 47 | None => return (index, Some(result)), 48 | Some(c) if c.is_whitespace() => return (index, Some(result)), 49 | Some(&'$') => match parse_dollar_parameters(chars, index, strategy) { 50 | (i, None) => return (i, None), 51 | (i, Some(string)) => { 52 | result.push_str(&string); 53 | index = i; 54 | } 55 | } 56 | Some(&'\'') => match parse_single_quote(chars, index, strategy) { 57 | (i, None) => return (i, None), 58 | (i, Some(string)) => { 59 | result.push_str(&string); 60 | index = i; 61 | } 62 | } 63 | Some(&'\"') => match parse_double_quote(chars, index, strategy) { 64 | (i, None) => return (i, None), 65 | (i, Some(string)) => { 66 | result.push_str(&string); 67 | index = i; 68 | } 69 | } 70 | Some(&'\\') => { 71 | index += 1; 72 | match chars.get(index) { 73 | Some(c @ &'\n') | Some(c @ &'\r') => { 74 | if let &ParameterStrategy::Ignore = strategy { 75 | result.push('\\'); 76 | result.push(c.clone()); 77 | } 78 | index += 1; 79 | } 80 | Some(c) => { 81 | if let &ParameterStrategy::Ignore = strategy { result.push('\\') } 82 | index += 1; 83 | result.push(c.clone()); 84 | } 85 | None => return (original_pos, None) 86 | } 87 | } 88 | Some(c) => { 89 | index += 1; 90 | result.push(c.clone()); 91 | } 92 | } 93 | } 94 | } 95 | 96 | fn parse_single_quote(chars: &Vec, pos: usize, strategy: &ParameterStrategy) -> (usize, Option) { 97 | let mut index = pos; 98 | let mut result: String = String::new(); 99 | if let &ParameterStrategy::Ignore = strategy { result.push('\'') } 100 | index += 1; 101 | loop { 102 | if let Some(c) = chars.get(index) { 103 | if c == &'\'' { 104 | if let &ParameterStrategy::Ignore = strategy { result.push('\'') } 105 | index += 1; 106 | return (index, Some(result)); 107 | } 108 | result.push(c.clone()); 109 | index += 1; 110 | } else { 111 | return (pos, None); 112 | } 113 | } 114 | } 115 | 116 | fn parse_double_quote(chars: &Vec, pos: usize, strategy: &ParameterStrategy) -> (usize, Option) { 117 | let mut index = pos; 118 | let mut result: String = String::new(); 119 | if let &ParameterStrategy::Ignore = strategy { result.push('\"') } 120 | index += 1; 121 | loop { 122 | match chars.get(index) { 123 | Some(&'\\') => { 124 | index += 1; 125 | match chars.get(index) { 126 | Some(c @ &'\"') | Some(c @ &'$') => { 127 | if let &ParameterStrategy::Ignore = strategy { result.push('\\') } 128 | index += 1; 129 | result.push(c.clone()); 130 | } 131 | Some(c @ &'\n') | Some(c @ &'\r') => { 132 | if let &ParameterStrategy::Ignore = strategy { 133 | result.push('\\'); 134 | result.push(c.clone()); 135 | } 136 | index += 1; 137 | } 138 | Some(&_) => result.push('\\'), 139 | None => return (pos, None) 140 | } 141 | } 142 | Some(&'\"') => { 143 | if let &ParameterStrategy::Ignore = strategy { result.push('\"') } 144 | index += 1; 145 | return (index, Some(result)); 146 | } 147 | Some(&'$') => match parse_dollar_parameters(chars, index, strategy) { 148 | (i, None) => return (i, None), 149 | (i, Some(string)) => { 150 | result.push_str(&string); 151 | index = i; 152 | } 153 | } 154 | Some(c) => { 155 | result.push(c.clone()); 156 | index += 1; 157 | } 158 | None => return (pos, None) 159 | } 160 | } 161 | } 162 | 163 | fn parse_dollar_parameters(chars: &Vec, pos: usize, strategy: &ParameterStrategy) -> (usize, Option) { 164 | match strategy { 165 | &ParameterStrategy::Ignore => return (pos + 1, Some("$".to_owned())), 166 | &ParameterStrategy::Map(ref b) => { 167 | let mut index = pos + 1; 168 | let mut result = String::new(); 169 | loop { 170 | match chars.get(index) { 171 | Some(&'{') => { 172 | index += 1; 173 | loop { 174 | if let Some(c) = chars.get(index) { 175 | if c == &'}' { return (index + 1, Some(b.as_ref()(result))); } 176 | result.push(c.clone()); 177 | index += 1; 178 | } else { 179 | return (pos, None); 180 | } 181 | } 182 | } 183 | Some(c @ &'a' ... 'z') | Some(c @ &'A' ... 'Z') | 184 | Some(c @ &'0' ... '9') | Some(c @ &'_') => { 185 | result.push(c.clone()); 186 | index += 1; 187 | } 188 | _ if result.is_empty() => return (pos + 1, Some("$".to_owned())), 189 | _ => return (index, Some(b.as_ref()(result))) 190 | } 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/launcher/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::path; 4 | use std::result::Result; 5 | use std::collections::HashMap; 6 | use std::process::{Child, Command}; 7 | 8 | use parsing; 9 | use versions; 10 | use yggdrasil; 11 | 12 | #[derive(Debug)] 13 | pub struct JvmOption(String); 14 | 15 | #[derive(Debug)] 16 | pub struct GameOption(String, Option); 17 | 18 | #[derive(Default)] 19 | pub struct MinecraftLauncherBuilder { 20 | program_path: Option, 21 | game_root_dir: Option, 22 | assets_dir: Option, 23 | libraries_dir: Option, 24 | launcher_name_version: Option<(String, String)>, 25 | auth_info: Option, 26 | min_memory_mib: Option, 27 | max_memory_mib: Option, 28 | window_resolution: Option<(u32, u32)>, 29 | } 30 | 31 | pub struct MinecraftLauncher { 32 | program_path: String, 33 | game_root_dir: path::PathBuf, 34 | assets_dir: path::PathBuf, 35 | libraries_dir: path::PathBuf, 36 | manager: versions::VersionManager, 37 | launcher_name_version: (String, String), 38 | auth_info: yggdrasil::AuthInfo, 39 | min_max_memory_mib: (f32, f32), 40 | window_resolution: (u32, u32), 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct LaunchArguments { 45 | java_main_class: String, 46 | java_program_path: String, 47 | jvm_options: Vec, 48 | game_options: Vec, 49 | game_native_path: path::PathBuf, 50 | game_natives: versions::NativeCollection, 51 | } 52 | 53 | pub fn builder() -> MinecraftLauncherBuilder { 54 | Default::default() 55 | } 56 | 57 | pub fn create(game_dir: path::PathBuf, 58 | game_auth_info: yggdrasil::AuthInfo) -> MinecraftLauncher { 59 | builder().root_dir(game_dir.as_path()).auth(game_auth_info).build() 60 | } 61 | 62 | #[cfg(target_os = "windows")] 63 | pub fn find_jre() -> Vec { 64 | Vec::new() // TODO 65 | } 66 | 67 | #[cfg(target_os = "macos")] 68 | pub fn find_jre() -> Vec { 69 | Vec::new() // TODO: I cannot afford a mac 70 | } 71 | 72 | #[cfg(target_os = "linux")] 73 | pub fn find_jre() -> Vec { 74 | let program = "update-alternatives"; 75 | if let Result::Ok(output) = Command::new(program).arg("--list").arg("java").output() { 76 | if let Result::Ok(string) = String::from_utf8(output.stdout) { 77 | return string.trim().split_whitespace().map(String::from).collect(); 78 | } 79 | } 80 | let program = "which"; 81 | if let Result::Ok(output) = Command::new(program).arg("java").output() { 82 | if let Result::Ok(string) = String::from_utf8(output.stdout) { 83 | return vec![String::from(string.trim())]; 84 | } 85 | } 86 | Vec::new() 87 | } 88 | 89 | impl MinecraftLauncherBuilder { 90 | pub fn root_dir(mut self, dir: &path::Path) -> Self { 91 | self.game_root_dir = Some(dir.to_path_buf()); 92 | self 93 | } 94 | 95 | pub fn assets_dir(mut self, dir: &path::Path) -> Self { 96 | self.assets_dir = Some(dir.to_path_buf()); 97 | self 98 | } 99 | 100 | pub fn libraries_dir(mut self, dir: &path::Path) -> Self { 101 | self.libraries_dir = Some(dir.to_path_buf()); 102 | self 103 | } 104 | 105 | pub fn jre(mut self, path: &path::Path) -> Self { 106 | self.program_path = path.to_path_buf().into_os_string().into_string().ok(); 107 | self 108 | } 109 | 110 | pub fn auth(mut self, auth: yggdrasil::AuthInfo) -> Self { 111 | self.auth_info = Some(auth); 112 | self 113 | } 114 | 115 | pub fn launcher(mut self, name: &str, version: &str) -> Self { 116 | self.launcher_name_version = Some((name.to_owned(), version.to_owned())); 117 | self 118 | } 119 | 120 | pub fn min_memory(mut self, memory_mib: f32) -> Self { 121 | self.min_memory_mib = Some(memory_mib); 122 | self 123 | } 124 | 125 | pub fn max_memory(mut self, memory_mib: f32) -> Self { 126 | self.max_memory_mib = Some(memory_mib); 127 | self 128 | } 129 | 130 | pub fn resolution(mut self, width: u32, height: u32) -> Self { 131 | self.window_resolution = Some((width, height)); 132 | self 133 | } 134 | 135 | pub fn build(self) -> MinecraftLauncher { 136 | let root_dir = self.game_root_dir.expect("game root dir not specified"); 137 | MinecraftLauncher { 138 | program_path: self.program_path.unwrap_or_else(|| find_jre().pop().expect("jre not found")), 139 | assets_dir: self.assets_dir.unwrap_or_else(|| root_dir.as_path().join("assets/")), 140 | libraries_dir: self.libraries_dir.unwrap_or_else(|| root_dir.as_path().join("libraries/")), 141 | manager: versions::VersionManager::new(root_dir.as_path().join("versions/").as_path()), 142 | game_root_dir: root_dir, 143 | launcher_name_version: self.launcher_name_version.unwrap_or(("RMCLL".to_owned(), "0.1.0".to_owned())), 144 | auth_info: self.auth_info.expect("auth info not specified"), 145 | min_max_memory_mib: (self.min_memory_mib.unwrap_or(128f32), self.max_memory_mib.unwrap_or(0f32)), 146 | window_resolution: self.window_resolution.unwrap_or((854, 480)), 147 | } 148 | } 149 | } 150 | 151 | impl MinecraftLauncher { 152 | pub fn generate_argument_map(&self, 153 | version: &versions::MinecraftVersion) -> HashMap { 154 | let mut map: HashMap = HashMap::new(); 155 | let name = self.auth_info.user_profile().name(); 156 | let uuid = self.auth_info.user_profile().uuid().simple(); 157 | let access_token = self.auth_info.access_token().simple(); 158 | map.insert("auth_access_token".to_owned(), 159 | format!("{}", access_token)); 160 | map.insert("user_properties".to_owned(), 161 | "{}".to_owned()); // TODO 162 | map.insert("user_property_map".to_owned(), 163 | "{}".to_owned()); // TODO 164 | map.insert("auth_session".to_owned(), 165 | format!("token:{}:{}", access_token, uuid)); 166 | map.insert("auth_player_name".to_owned(), 167 | name.clone()); 168 | map.insert("auth_uuid".to_owned(), 169 | format!("{}", uuid)); 170 | map.insert("user_type".to_owned(), 171 | "legacy".to_owned()); 172 | map.insert("profile_name".to_owned(), 173 | name.clone()); 174 | map.insert("version_name".to_owned(), 175 | version.id().to_owned()); 176 | map.insert("game_directory".to_owned(), 177 | self.game_root_dir.to_str().unwrap_or("").to_owned()); 178 | map.insert("assets_root".to_owned(), 179 | self.assets_dir.to_str().unwrap_or("").to_owned()); 180 | map.insert("assets_index_name".to_owned(), 181 | version.asset_index(&self.manager).map(|i| i.id().to_owned()).unwrap_or_else(String::new)); 182 | map.insert("version_type".to_owned(), 183 | version.version_type().to_owned()); 184 | map.insert("resolution_width".to_owned(), 185 | format!("{}", self.window_resolution.0)); 186 | map.insert("resolution_height".to_owned(), 187 | format!("{}", self.window_resolution.1)); 188 | map.insert("language".to_owned(), 189 | "en-us".to_owned()); 190 | map.insert("launcher_name".to_owned(), 191 | self.launcher_name_version.0.clone()); 192 | map.insert("launcher_version".to_owned(), 193 | self.launcher_name_version.1.clone()); 194 | map.insert("natives_directory".to_owned(), 195 | self.manager.get_natives_path(version.id()).to_str().unwrap_or("").to_owned()); 196 | map.insert("primary_jar".to_owned(), 197 | version.version_jar_path(&self.manager).ok().and_then(|p| p.to_str().map(String::from)).unwrap_or_else(String::new)); 198 | map.insert("classpath".to_owned(), 199 | version.classpath(self.libraries_dir.as_path(), &self.manager).unwrap_or_else(|_| String::new())); 200 | map.insert("classpath_separator".to_owned(), 201 | ":".to_owned()); 202 | map 203 | } 204 | 205 | pub fn to_arguments(&self, version_id: &str) -> Result { 206 | let java_program_path = self.program_path.clone(); 207 | let minecraft_version = self.manager.version_of(version_id)?; 208 | let java_main_class = minecraft_version.main_class(&self.manager).unwrap_or_else(String::new); 209 | let game_natives = minecraft_version.to_native_collection(&self.manager, self.libraries_dir.as_path())?; 210 | let mut jvm_options = vec![ 211 | JvmOption::new("-XX:+UseG1GC".to_owned()), 212 | JvmOption::new("-XX:-UseAdaptiveSizePolicy".to_owned()), 213 | JvmOption::new("-XX:-OmitStackTraceInFastThrow".to_owned()), 214 | JvmOption::new("-Dfml.ignoreInvalidMinecraftCertificates=true".to_owned()), 215 | JvmOption::new("-Dfml.ignorePatchDiscrepancies=true".to_owned()), 216 | ]; 217 | let (min_mib, max_mib) = self.min_max_memory_mib; 218 | if min_mib > 0f32 { jvm_options.push(JvmOption::new(format!("-Xmn{}m", min_mib))) } 219 | if max_mib > 0f32 { jvm_options.push(JvmOption::new(format!("-Xmx{}m", max_mib))) } 220 | let mut game_options = Vec::new(); 221 | let map = self.generate_argument_map(&minecraft_version); 222 | let game_native_path = path::PathBuf::from(map.get("natives_directory").unwrap()); 223 | let strategy = parsing::ParameterStrategy::map(move |s| { 224 | let result = match map.get(&s) { 225 | Some(ref string) => (*string).clone(), 226 | None => String::new() 227 | }; 228 | result 229 | }); 230 | minecraft_version.collect_game_arguments(&self.manager, &mut game_options, &strategy)?; 231 | minecraft_version.collect_jvm_arguments(&self.manager, &mut jvm_options, &strategy)?; 232 | Result::Ok(LaunchArguments { 233 | game_natives, 234 | game_native_path, 235 | game_options, 236 | jvm_options, 237 | java_main_class, 238 | java_program_path, 239 | }) 240 | } 241 | } 242 | 243 | impl LaunchArguments { 244 | pub fn start(&self) -> Result { 245 | self.extract_natives()?; 246 | self.spawn_new_process() 247 | } 248 | 249 | pub fn spawn_new_process(&self) -> Result { 250 | Command::new(self.program()).args(self.args()).spawn().map_err(versions::Error::from) 251 | } 252 | 253 | pub fn extract_natives(&self) -> Result, versions::Error> { 254 | self.game_natives.extract_to(self.game_native_path.as_path()) 255 | } 256 | 257 | pub fn program(&self) -> String { 258 | self.java_program_path.clone() 259 | } 260 | 261 | pub fn args(&self) -> Vec { 262 | let mut result = Vec::new(); 263 | for option in self.jvm_options.iter() { 264 | match option { 265 | &JvmOption(ref name) => { 266 | result.push(name.clone()); 267 | } 268 | } 269 | } 270 | result.push(self.java_main_class.clone()); 271 | for option in self.game_options.iter() { 272 | match option { 273 | &GameOption(ref name, Some(ref arg)) => { 274 | result.push(name.clone()); 275 | result.push(arg.clone()); 276 | } 277 | &GameOption(ref name, None) => { 278 | result.push(name.clone()); 279 | } 280 | } 281 | } 282 | result 283 | } 284 | } 285 | 286 | impl JvmOption { 287 | pub fn new(arg: String) -> JvmOption { 288 | JvmOption(arg) 289 | } 290 | } 291 | 292 | impl GameOption { 293 | pub fn new_pair(name: String, arg: String) -> GameOption { 294 | GameOption(name, Some(arg)) 295 | } 296 | 297 | pub fn new_single(name: String) -> GameOption { 298 | GameOption(name, None) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/versions/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unreachable_patterns)] 3 | 4 | use std::io; 5 | use std::fs; 6 | use std::fmt; 7 | use std::error; 8 | use std::rc::Rc; 9 | use std::ffi::OsString; 10 | use std::path::{Path, PathBuf}; 11 | use std::result::Result; 12 | use std::collections::HashMap; 13 | use zip::read::ZipArchive; 14 | use zip::result::ZipError; 15 | use serde_json::{Value, self}; 16 | use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, self}; 17 | 18 | use launcher; 19 | use parsing; 20 | 21 | #[cfg(target_pointer_width = "32")] 22 | const OS_ARCH: &str = "32"; 23 | #[cfg(target_pointer_width = "64")] 24 | const OS_ARCH: &str = "64"; 25 | #[cfg(target_os = "windows")] 26 | const OS_PLATFORM: &str = "windows"; 27 | #[cfg(target_os = "macos")] 28 | const OS_PLATFORM: &str = "macos"; 29 | #[cfg(target_os = "linux")] 30 | const OS_PLATFORM: &str = "linux"; 31 | 32 | const CLASSPATH_SEPARATOR: &str = ":"; 33 | 34 | #[derive(Deserialize, Debug)] 35 | pub struct MinecraftVersion { 36 | id: String, 37 | #[serde(rename = "type")] 38 | version_type: String, 39 | #[serde(rename = "time")] 40 | publish_time: String, 41 | #[serde(rename = "releaseTime")] 42 | release_time: String, 43 | // TODO: 1.13+ arguments 44 | /* 45 | #[serde(default)] 46 | arguments: HashMap, 47 | */ 48 | #[serde(rename = "minecraftArguments")] 49 | minecraft_arguments: Option, 50 | #[serde(rename = "mainClass", default)] 51 | main_class: Option, 52 | #[serde(rename = "jar", default)] 53 | version_jar: Option, 54 | #[serde(rename = "assets")] 55 | assets_id: Option, 56 | #[serde(rename = "assetIndex")] 57 | asset_index: Option, 58 | #[serde(default)] 59 | assets: Option, 60 | #[serde(default)] 61 | libraries: Vec, 62 | #[serde(default)] 63 | downloads: HashMap, 64 | #[serde(rename = "inheritsFrom")] 65 | inherits_from: Option, 66 | } 67 | 68 | #[derive(Debug)] 69 | pub struct DownloadStrategy { 70 | with_classifier: HashMap, 71 | default: Option, 72 | rules: Vec<(String, String)>, 73 | } 74 | 75 | #[derive(Clone, Debug)] 76 | pub struct Library { 77 | name: String, 78 | is_native: bool, 79 | downloads: Rc, 80 | extract_ignored: Rc>, 81 | } 82 | 83 | #[derive(Clone, Debug)] 84 | pub struct NativeCollection { 85 | libraries: Vec<(PathBuf, Rc>)> 86 | } 87 | 88 | #[derive(Deserialize, Clone, Debug)] 89 | #[serde(untagged)] 90 | pub enum DownloadInfo { 91 | PreHashed { size: i32, url: String, sha1: String }, 92 | RawXzip { url: String }, 93 | Raw { url: String }, 94 | } 95 | 96 | #[derive(Deserialize, Clone, Debug)] 97 | pub struct AssetDownloadInfo { 98 | size: Option, 99 | url: Option, 100 | sha1: Option, 101 | #[serde(rename = "id")] 102 | asset_index_id: String, 103 | #[serde(rename = "totalSize")] 104 | total_size: Option, 105 | #[serde(rename = "known", default)] 106 | size_and_hash_known: bool, 107 | } 108 | 109 | pub struct VersionManager(Box); 110 | 111 | #[derive(Debug)] 112 | pub enum Error { 113 | FileUnavailableError(Box), 114 | UnrecognizedPathString(OsString), 115 | IOError(Box), 116 | } 117 | 118 | impl From for Error { 119 | fn from(e: serde_json::Error) -> Self { 120 | Error::IOError(Box::new(e)) 121 | } 122 | } 123 | 124 | impl From for Error { 125 | fn from(e: OsString) -> Self { 126 | Error::UnrecognizedPathString(e) 127 | } 128 | } 129 | 130 | impl From for Error { 131 | fn from(e: io::Error) -> Self { 132 | Error::IOError(Box::new(e)) 133 | } 134 | } 135 | 136 | impl From for Error { 137 | fn from(e: ZipError) -> Self { 138 | Error::IOError(Box::new(io::Error::from(e))) 139 | } 140 | } 141 | 142 | impl NativeCollection { 143 | fn is_file_included(&self, extract_ignored: &Vec, file_name: &str) -> bool { 144 | extract_ignored.iter().find(|rule| file_name.starts_with(rule.as_str())).is_none() 145 | } 146 | 147 | pub fn extract_to(&self, target_dir_path: &Path) -> Result, Error> { 148 | let mut result = Vec::new(); 149 | let target_path_buf = target_dir_path.to_path_buf(); 150 | if !target_dir_path.is_dir() { fs::create_dir_all(target_dir_path)? } 151 | for &(ref path_buf, ref extract_ignored) in self.libraries.iter() { 152 | let zip_file = fs::File::open(path_buf)?; 153 | let mut zip = ZipArchive::new(zip_file)?; 154 | for i in 0..zip.len() { 155 | let mut source = zip.by_index(i)?; 156 | let file_name = source.name().to_owned(); 157 | if self.is_file_included(&extract_ignored, file_name.as_str()) { 158 | let target_path = target_path_buf.join(file_name.as_str()); 159 | let mut target = fs::File::create(target_path)?; 160 | io::copy(&mut source, &mut target)?; 161 | result.push(file_name); 162 | } 163 | } 164 | } 165 | Result::Ok(result) 166 | } 167 | } 168 | 169 | impl VersionManager { 170 | pub fn new(path: &Path) -> VersionManager { 171 | VersionManager(Box::from(path)) 172 | } 173 | 174 | pub fn get_version_path(&self) -> PathBuf { 175 | self.0.to_path_buf() 176 | } 177 | 178 | pub fn get_natives_path(&self, id: &str) -> PathBuf { 179 | let sub_path = format!("{}-natives-{}-{}/", id, OS_PLATFORM, OS_ARCH); 180 | let mut path_buf = self.0.join(id); 181 | path_buf.push(sub_path); 182 | path_buf 183 | } 184 | 185 | pub fn extract_natives(&self, id: &str, library_path: &Path) -> Result, Error> { 186 | let info = self.version_of(id)?; 187 | let path_buf = self.get_natives_path(id); 188 | info.to_native_collection(self, library_path)?.extract_to(path_buf.as_path()) 189 | } 190 | 191 | pub fn version_of(&self, id: &str) -> Result { 192 | let path_buf = self.0.join(id); 193 | if !path_buf.is_dir() { fs::create_dir_all(path_buf.as_path())? } 194 | let path_buf_json = path_buf.join(format!("{}.json", id)); 195 | if path_buf_json.exists() { 196 | Result::Ok(serde_json::from_reader(fs::File::open(path_buf_json)?)?) 197 | } else { 198 | Result::Err(Error::FileUnavailableError(path_buf_json.into_boxed_path())) 199 | } 200 | } 201 | } 202 | 203 | impl MinecraftVersion { 204 | pub fn id(&self) -> &str { 205 | &self.id 206 | } 207 | 208 | pub fn version_type(&self) -> &str { 209 | &self.version_type 210 | } 211 | 212 | pub fn publish_time(&self) -> &str { 213 | &self.publish_time 214 | } 215 | 216 | pub fn release_time(&self) -> &str { 217 | &self.release_time 218 | } 219 | 220 | pub fn asset_index(&self, manager: &VersionManager) -> Option { 221 | self.asset_index.clone().or_else(|| self.assets_id.clone().map(AssetDownloadInfo::new)).or_else(|| { 222 | if let Some(ref inherits_from) = self.inherits_from { 223 | manager.version_of(&inherits_from).ok().and_then(|v| v.asset_index(manager)) 224 | } else { 225 | None 226 | } 227 | }) 228 | } 229 | 230 | pub fn main_class(&self, manager: &VersionManager) -> Option { 231 | self.main_class.clone().or_else(|| { 232 | if let Some(ref inherits_from) = self.inherits_from { 233 | manager.version_of(&inherits_from).ok().and_then(|v| v.main_class(manager)) 234 | } else { 235 | None 236 | } 237 | }) 238 | } 239 | 240 | pub fn libraries(&self, manager: &VersionManager) -> Result, Error> { 241 | if let Some(ref inherits_from) = self.inherits_from { 242 | let mut result = manager.version_of(&inherits_from)?.libraries(manager)?; 243 | result.extend(self.libraries.clone().into_iter()); 244 | Result::Ok(result) 245 | } else { 246 | Result::Ok(self.libraries.clone()) 247 | } 248 | } 249 | 250 | pub fn version_jar_path(&self, manager: &VersionManager) -> Result { 251 | match self.version_jar { 252 | Some(ref jar) => { 253 | let version_path = manager.get_version_path(); 254 | Result::Ok(version_path.join(format!("{0}/{0}.jar", jar))) 255 | }, 256 | None => if let Some(ref inherits_from) = self.inherits_from { 257 | manager.version_of(&inherits_from)?.version_jar_path(manager) 258 | } else { 259 | let version_path = manager.get_version_path(); 260 | Result::Ok(version_path.join(format!("{0}/{0}.jar", self.id))) 261 | } 262 | } 263 | } 264 | 265 | pub fn collect_game_arguments(&self, 266 | manager: &VersionManager, 267 | parameters: &mut Vec, 268 | s: &parsing::ParameterStrategy) -> Result<(), Error> { 269 | let mut option_name = None; 270 | match self.minecraft_arguments { 271 | Some(ref args) => { 272 | for arg in parsing::parse(&args, s) { 273 | if arg.is_empty() { return Result::Ok(()); } 274 | match option_name { 275 | None => if arg.starts_with("-") { 276 | option_name = Some(arg); 277 | } else { 278 | (*parameters).push(launcher::GameOption::new_single(arg)); 279 | } 280 | Some(name) => if arg.starts_with("-") { 281 | (*parameters).push(launcher::GameOption::new_single(name)); 282 | option_name = Some(arg); 283 | } else { 284 | (*parameters).push(launcher::GameOption::new_pair(name, arg)); 285 | option_name = None; 286 | } 287 | } 288 | } 289 | if let Some(name) = option_name { 290 | (*parameters).push(launcher::GameOption::new_single(name)); 291 | } 292 | parameters.push(launcher::GameOption::new_pair("--width".to_owned(), self.parse_token("${resolution_width}", s))); 293 | parameters.push(launcher::GameOption::new_pair("--height".to_owned(), self.parse_token("${resolution_height}", s))); 294 | } 295 | None => if let Some(ref inherits_from) = self.inherits_from { 296 | let version = manager.version_of(&inherits_from)?; 297 | return version.collect_game_arguments(manager, parameters, s); 298 | } 299 | } 300 | Result::Ok(()) 301 | } 302 | 303 | pub fn collect_jvm_arguments(&self, 304 | _: &VersionManager, 305 | parameters: &mut Vec, 306 | s: &parsing::ParameterStrategy) -> Result<(), Error> { 307 | if OS_PLATFORM == "windows" { parameters.push(launcher::JvmOption::new("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump".to_owned())); } 308 | parameters.push(launcher::JvmOption::new(self.parse_token("-Djava.library.path=${natives_directory}", s))); 309 | parameters.push(launcher::JvmOption::new(self.parse_token("-Dminecraft.launcher.brand=${launcher_name}", s))); 310 | parameters.push(launcher::JvmOption::new(self.parse_token("-Dminecraft.launcher.version=${launcher_version}", s))); 311 | parameters.push(launcher::JvmOption::new(self.parse_token("-Dminecraft.client.jar=${primary_jar}", s))); 312 | parameters.push(launcher::JvmOption::new("-cp".to_owned())); 313 | parameters.push(launcher::JvmOption::new(self.parse_token("${classpath}", s))); 314 | Result::Ok(()) 315 | } 316 | 317 | pub fn classpath(&self, 318 | library_path: &Path, 319 | manager: &VersionManager) -> Result { 320 | self.classpath_with_separator(library_path, CLASSPATH_SEPARATOR, manager) 321 | } 322 | 323 | pub fn classpath_with_separator(&self, 324 | library_path: &Path, 325 | classpath_separator: &str, 326 | manager: &VersionManager) -> Result { 327 | let libs = self.libraries(manager)?; 328 | let mut result = String::new(); 329 | for lib in libs.iter() { 330 | if !lib.is_native() { 331 | if let Some(path_buf) = lib.classpath_default(library_path) { 332 | let path = fs::canonicalize(path_buf.as_path())?.into_os_string(); 333 | result.push_str(&path.into_string()?); 334 | result.push_str(classpath_separator); 335 | } 336 | } 337 | } 338 | let primary_jar_path = self.version_jar_path(manager)?.into_os_string(); 339 | result.push_str(primary_jar_path.into_string()?.as_str()); 340 | Result::Ok(result) 341 | } 342 | 343 | pub fn to_native_collection(&self, 344 | manager: &VersionManager, 345 | library_path: &Path) -> Result { 346 | let mut collection = NativeCollection { libraries: Vec::new() }; 347 | for lib in self.libraries(manager)?.iter() { 348 | if lib.is_native() { 349 | if let Some(path_buf) = lib.classpath_default(library_path) { 350 | collection.libraries.push((path_buf, lib.extract_ignored.clone())) 351 | } 352 | } 353 | } 354 | Result::Ok(collection) 355 | } 356 | 357 | fn parse_token(&self, token: &str, s: &parsing::ParameterStrategy) -> String { 358 | match parsing::parse(token, s).next() { 359 | Some(parsed_token) => parsed_token, 360 | None => token.to_owned() 361 | } 362 | } 363 | } 364 | 365 | impl AssetDownloadInfo { 366 | pub fn new(id: String) -> AssetDownloadInfo { 367 | AssetDownloadInfo { 368 | size: None, 369 | url: None, 370 | sha1: None, 371 | asset_index_id: id, 372 | total_size: None, 373 | size_and_hash_known: false, 374 | } 375 | } 376 | 377 | pub fn id(&self) -> &str { 378 | &self.asset_index_id 379 | } 380 | } 381 | 382 | impl From for DownloadInfo { 383 | fn from(info: AssetDownloadInfo) -> Self { 384 | let id = info.asset_index_id; 385 | match (info.size, info.url, info.sha1, info.size_and_hash_known) { 386 | (Some(size), Some(url), Some(sha1), true) => DownloadInfo::PreHashed { size, url, sha1 }, 387 | (_, Some(url), _, _) => DownloadInfo::Raw { url }, 388 | _ => DownloadInfo::Raw { 389 | url: format!("https://s3.amazonaws.com/Minecraft.Download/indexes/{}.json", id), 390 | } 391 | } 392 | } 393 | } 394 | 395 | impl DownloadStrategy { 396 | fn get<'a>(&'a self, arg: &str) -> Option<(&'a str, &'a DownloadInfo)> { 397 | let mut allowed = self.rules.is_empty(); 398 | for &(ref action, ref os) in &self.rules { 399 | match action.as_str() { 400 | "allow" => allowed = os.is_empty() || os == OS_PLATFORM, 401 | "disallow" => allowed = !os.is_empty() && os != OS_PLATFORM, 402 | _ => () // just ignore it 403 | } 404 | } 405 | if allowed { 406 | match self.with_classifier.get(arg) { 407 | Some(&(ref classifier, ref info)) => Some((&classifier, &info)), 408 | None => self.default.as_ref().map(|v| ("", v)) 409 | } 410 | } else { 411 | None 412 | } 413 | } 414 | } 415 | 416 | impl Library { 417 | pub fn is_native(&self) -> bool { 418 | self.is_native 419 | } 420 | 421 | pub fn download_info_default(&self) -> Option<&DownloadInfo> { 422 | self.download_info_of(OS_ARCH, OS_PLATFORM) 423 | } 424 | 425 | pub fn download_info_of(&self, arch: &str, platform: &str) -> Option<&DownloadInfo> { 426 | match self.downloads.as_ref().get(&format!("{}bit {}", arch, platform)) { 427 | Some(ref info) => Some(info.1), 428 | None => None 429 | } 430 | } 431 | 432 | pub fn classpath_default(&self, path: &Path) -> Option { 433 | self.classpath_of(path, OS_ARCH, OS_PLATFORM) 434 | } 435 | 436 | pub fn classpath_of(&self, path: &Path, arch: &str, platform: &str) -> Option { 437 | match self.downloads.as_ref().get(&format!("{}bit {}", arch, platform)) { 438 | Some(ref info) => match Library::get_url_suffix(&self.name, info.0, false) { 439 | Some(suffix) => { 440 | let mut path_buf = path.to_path_buf(); 441 | path_buf.push(suffix); 442 | Some(path_buf) 443 | } 444 | None => None 445 | } 446 | None => None 447 | } 448 | } 449 | 450 | fn get_as_result(v: &Value, expected: &str) -> Result { 451 | v.as_str().map(String::from).ok_or_else(|| { 452 | de::Error::invalid_type(de::Unexpected::UnitVariant, &expected) 453 | }) 454 | } 455 | 456 | fn get_url_suffix(name: &str, classifier: &str, is_xz: bool) -> Option { 457 | let parts: Vec<_> = name.splitn(3, ':').collect(); 458 | if parts.len() != 3 { None } else { 459 | let suffix = if is_xz { "jar.pack.xz" } else { "jar" }; 460 | let dir = format!("{}/{}/{}", parts[0].replace(".", "/"), parts[1], parts[2]); 461 | if classifier.is_empty() { 462 | Some(format!("{}/{}-{}.{}", dir, parts[1], parts[2], suffix)) 463 | } else { 464 | Some(format!("{}/{}-{}-{}.{}", dir, parts[1], parts[2], classifier, suffix)) 465 | } 466 | } 467 | } 468 | 469 | fn deserialize_map<'de, A>(mut map: A) -> Result where A: MapAccess<'de> { 470 | let mut is_xz = false; 471 | let mut url_prefix: String = String::new(); 472 | let mut natives: HashMap = HashMap::new(); 473 | let mut downloads: Value = Value::Null; 474 | let mut name = String::new(); 475 | let mut extract_ignored = Vec::new(); 476 | let mut library_downloads = DownloadStrategy { 477 | with_classifier: HashMap::new(), 478 | rules: Vec::new(), 479 | default: None, 480 | }; 481 | while let Some((key, value)) = map.next_entry::()? { 482 | match key.as_str() { 483 | "name" => name = Library::get_as_result(&value, "library name")?, 484 | "url" => url_prefix = Library::get_as_result(&value, "library url prefix")?, 485 | "checksums" => is_xz = value.is_array(), 486 | "extract" => if let Some(extract_rules) = value.as_object().and_then(|o| { 487 | o.get("exclude").and_then(|v| v.as_array()) 488 | }) { 489 | for v in extract_rules.iter() { 490 | let rule = Library::get_as_result(v, "extract rules")?; 491 | extract_ignored.push(rule); 492 | } 493 | } 494 | "natives" => if let Some(map) = value.as_object() { 495 | for (k, v) in map.iter() { 496 | let classifier = Library::get_as_result(v, "os classifier")?; 497 | natives.insert(k.clone(), classifier); 498 | } 499 | } 500 | "rules" => if let Some(list) = value.as_array() { 501 | for v in list { 502 | if let Some(map) = v.as_object() { 503 | if let Some(value) = map.get("action") { 504 | let action = Library::get_as_result(value, "rule action")?; 505 | if let Some(os) = map.get("os").and_then(|v| { 506 | v.as_object().and_then(|v| v.get("name")) 507 | }).map(|v| Library::get_as_result(v, "rule os")) { 508 | library_downloads.rules.push((action, os?)); 509 | } else { 510 | library_downloads.rules.push((action, String::new())); 511 | } 512 | } 513 | } 514 | } 515 | } 516 | "downloads" => if value.is_object() { 517 | downloads = value.clone(); 518 | } 519 | _ => () // just ignore it 520 | } 521 | } 522 | if name.is_empty() { 523 | let err = de::Error::invalid_type(de::Unexpected::UnitVariant, &"library name"); 524 | return Result::Err(err); 525 | } 526 | if url_prefix.is_empty() { 527 | if let Some(map) = downloads.as_object() { 528 | if let Some(classifiers) = map.get("classifiers").and_then(|v| v.as_object()) { 529 | for (os, classifier) in natives.iter() { 530 | let classifier_32 = classifier.replace("${arch}", "32"); 531 | let classifier_64 = classifier.replace("${arch}", "64"); 532 | if let Some(download_info) = classifiers.get(&classifier_32).and_then(|v| { 533 | serde_json::from_value::(v.clone()).ok() 534 | }) { 535 | let key = format!("32bit {}", os); 536 | library_downloads.with_classifier.insert(key, (classifier_32, download_info)); 537 | } 538 | if let Some(download_info) = classifiers.get(&classifier_64).and_then(|v| { 539 | serde_json::from_value::(v.clone()).ok() 540 | }) { 541 | let key = format!("64bit {}", os); 542 | library_downloads.with_classifier.insert(key, (classifier_64, download_info)); 543 | } 544 | } 545 | } 546 | if let Some(download_info) = map.get("artifact").and_then(|v| { 547 | serde_json::from_value::(v.clone()).ok() 548 | }) { 549 | library_downloads.default = Some(download_info); 550 | } 551 | return Result::Ok(Library { 552 | name, 553 | is_native: !natives.is_empty(), 554 | downloads: Rc::new(library_downloads), 555 | extract_ignored: Rc::new(extract_ignored), 556 | }); 557 | } 558 | url_prefix.push_str("https://libraries.minecraft.net/"); 559 | } 560 | if natives.is_empty() { 561 | if let Some(suffix) = Library::get_url_suffix(&name, "", is_xz) { 562 | library_downloads.default = Some(if is_xz { 563 | DownloadInfo::RawXzip { url: format!("{}{}", url_prefix, suffix) } 564 | } else { 565 | DownloadInfo::Raw { url: format!("{}{}", url_prefix, suffix) } 566 | }); 567 | } 568 | } else { 569 | for (os, classifier) in natives.iter() { 570 | let classifier_32 = classifier.replace("${arch}", "32"); 571 | let classifier_64 = classifier.replace("${arch}", "64"); 572 | if let Some(suffix) = Library::get_url_suffix(&name, classifier_32.as_str(), is_xz) { 573 | library_downloads.with_classifier.insert(format!("32bit {}", os), (classifier_32, if is_xz { 574 | DownloadInfo::RawXzip { url: format!("{}{}", url_prefix, suffix) } 575 | } else { 576 | DownloadInfo::Raw { url: format!("{}{}", url_prefix, suffix) } 577 | })); 578 | } 579 | if let Some(suffix) = Library::get_url_suffix(&name, classifier_64.as_str(), is_xz) { 580 | library_downloads.with_classifier.insert(format!("64bit {}", os), (classifier_64, if is_xz { 581 | DownloadInfo::RawXzip { url: format!("{}{}", url_prefix, suffix) } 582 | } else { 583 | DownloadInfo::Raw { url: format!("{}{}", url_prefix, suffix) } 584 | })); 585 | } 586 | } 587 | } 588 | Result::Ok(Library { 589 | name, 590 | is_native: !natives.is_empty(), 591 | downloads: Rc::new(library_downloads), 592 | extract_ignored: Rc::new(extract_ignored), 593 | }) 594 | } 595 | } 596 | 597 | impl<'de> Deserialize<'de> for Library { 598 | fn deserialize>(deserializer: D) -> Result { 599 | struct LibraryVisitor; 600 | 601 | impl<'de> Visitor<'de> for LibraryVisitor { 602 | fn visit_map(self, map: A) -> Result where A: MapAccess<'de> { 603 | Library::deserialize_map(map) 604 | } 605 | 606 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 607 | formatter.write_str("minecraft libraries") 608 | } 609 | 610 | type Value = Library; 611 | } 612 | 613 | deserializer.deserialize_map(LibraryVisitor) 614 | } 615 | } 616 | --------------------------------------------------------------------------------