Proof of concept of launching Minecraft from Rust

This commit is contained in:
Jai A
2021-07-04 22:46:56 -07:00
parent 93418edbe7
commit a0e35ad853
11 changed files with 1324 additions and 71 deletions

View File

@@ -0,0 +1,219 @@
use crate::launcher::meta::{Argument, ArgumentValue, Library, Os, VersionType};
use crate::launcher::rules::parse_rules;
use std::path::Path;
use uuid::Uuid;
pub fn get_class_paths(libraries_path: &Path, libraries: &[Library], client_path: &Path) -> String {
let mut class_paths = Vec::new();
for library in libraries {
if library.downloads.artifact.is_some() {
if let Some(rules) = &library.rules {
if !super::rules::parse_rules(rules.as_slice()) {
continue;
}
}
let name_items = library.name.split(':').collect::<Vec<&str>>();
let package = name_items.get(0).unwrap();
let name = name_items.get(1).unwrap();
let version = name_items.get(2).unwrap();
let mut path = libraries_path.to_path_buf();
for directory in package.split(".") {
path.push(directory);
}
path.push(name);
path.push(version);
path.push(format!("{}-{}.jar", name, version));
class_paths.push(
std::fs::canonicalize(&path)
.unwrap()
.to_string_lossy()
.to_string(),
)
}
}
class_paths.push(
std::fs::canonicalize(&client_path)
.unwrap()
.to_string_lossy()
.to_string(),
);
class_paths.join(match super::download::get_os() {
Os::Osx | Os::Linux | Os::Unknown => ":",
Os::Windows => ";",
})
}
pub fn get_jvm_arguments(
arguments: Option<&[Argument]>,
natives_path: &Path,
class_paths: &str,
) -> Vec<String> {
let mut parsed_arguments = Vec::new();
if let Some(args) = arguments {
parse_arguments(args, &mut parsed_arguments, |arg| {
parse_jvm_argument(arg, natives_path, class_paths)
});
} else {
parsed_arguments.push(format!(
"-Djava.library.path={}",
&*std::fs::canonicalize(natives_path)
.unwrap()
.to_string_lossy()
.to_string()
));
parsed_arguments.push("-cp".to_string());
parsed_arguments.push(class_paths.to_string());
}
parsed_arguments
}
fn parse_jvm_argument(argument: &str, natives_path: &Path, class_paths: &str) -> String {
argument
.replace(
"${natives_directory}",
&*std::fs::canonicalize(natives_path)
.unwrap()
.to_string_lossy()
.to_string(),
)
.replace("${launcher_name}", "theseus")
.replace("${launcher_version}", env!("CARGO_PKG_VERSION"))
.replace("${classpath}", class_paths)
}
pub fn get_minecraft_arguments(
arguments: Option<&[Argument]>,
legacy_arguments: Option<&str>,
access_token: &str,
username: &str,
uuid: &Uuid,
version: &str,
asset_index_name: &str,
game_directory: &Path,
assets_directory: &Path,
version_type: &VersionType,
) -> Vec<String> {
if let Some(arguments) = arguments {
let mut parsed_arguments = Vec::new();
parse_arguments(arguments, &mut parsed_arguments, |arg| {
parse_minecraft_argument(
arg,
access_token,
username,
uuid,
version,
asset_index_name,
game_directory,
assets_directory,
version_type,
)
});
parsed_arguments
} else if let Some(legacy_arguments) = legacy_arguments {
parse_minecraft_argument(
legacy_arguments,
access_token,
username,
uuid,
version,
asset_index_name,
game_directory,
assets_directory,
version_type,
)
.split(" ")
.into_iter()
.map(|x| x.to_string())
.collect()
} else {
Vec::new()
}
}
fn parse_minecraft_argument(
argument: &str,
access_token: &str,
username: &str,
uuid: &Uuid,
version: &str,
asset_index_name: &str,
game_directory: &Path,
assets_directory: &Path,
version_type: &VersionType,
) -> String {
argument
.replace("${auth_access_token}", access_token)
.replace("${auth_session}", access_token)
.replace("${auth_player_name}", username)
.replace("${auth_uuid}", &*uuid.to_hyphenated().to_string())
.replace("${user_properties}", "{}")
.replace("${user_type}", "mojang")
.replace("${version_name}", version)
.replace("${assets_index_name}", asset_index_name)
.replace(
"${game_directory}",
&*std::fs::canonicalize(game_directory)
.unwrap()
.to_string_lossy()
.to_string(),
)
.replace(
"${assets_root}",
&*std::fs::canonicalize(assets_directory)
.unwrap()
.to_string_lossy()
.to_string(),
)
.replace(
"${game_assets}",
&*std::fs::canonicalize(assets_directory)
.unwrap()
.to_string_lossy()
.to_string(),
)
.replace("${version_type}", version_type.as_str())
}
fn parse_arguments<F>(arguments: &[Argument], parsed_arguments: &mut Vec<String>, parse_function: F)
where
F: Fn(&str) -> String,
{
for argument in arguments {
match argument {
Argument::Normal(arg) => {
let parsed = parse_function(arg);
for arg in parsed.split(" ") {
parsed_arguments.push(arg.to_string());
}
}
Argument::Ruled { rules, value } => {
if parse_rules(rules.as_slice()) {
match value {
ArgumentValue::Single(arg) => {
//parsed_arguments.push(parse_function(arg));
}
ArgumentValue::Many(args) => {
for arg in args {
//parsed_arguments.push(parse_function(arg));
}
}
}
}
}
}
}
}

View File

@@ -1 +1,162 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
pub struct GameProfile {
pub id: Uuid,
pub name: String,
}
#[derive(Debug, Deserialize)]
pub struct UserProperty {
pub name: String,
pub value: String,
}
#[derive(Debug, Deserialize)]
pub struct User {
pub id: String,
pub username: String,
pub properties: Option<Vec<UserProperty>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticateResponse {
pub user: Option<User>,
pub client_token: Uuid,
pub access_token: String,
pub available_profiles: Vec<GameProfile>,
pub selected_profile: Option<GameProfile>,
}
pub async fn login(username: &str, password: &str, request_user: bool) -> AuthenticateResponse {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/authenticate")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"agent": {
"name": "Minecraft",
"version": 1
},
"username": username,
"password": password,
"clientToken": Uuid::new_v4(),
"requestUser": request_user
}
)
.to_string(),
)
.send()
.await
.unwrap()
.json()
.await
.unwrap()
}
pub async fn sign_out(username: &str, password: &str) {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/signout")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"username": username,
"password": password
}
)
.to_string(),
)
.send()
.await
.unwrap();
}
pub async fn validate(access_token: &str, client_token: &str) {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/validate")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"accessToken": access_token,
"clientToken": client_token
}
)
.to_string(),
)
.send()
.await
.unwrap();
}
pub async fn invalidate(access_token: &str, client_token: &str) {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/invalidate")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"accessToken": access_token,
"clientToken": client_token
}
)
.to_string(),
)
.send()
.await
.unwrap();
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshResponse {
pub user: Option<User>,
pub client_token: Uuid,
pub access_token: String,
pub selected_profile: Option<GameProfile>,
}
pub async fn refresh(
access_token: &str,
client_token: &str,
selected_profile: &GameProfile,
request_user: bool,
) -> RefreshResponse {
let client = reqwest::Client::new();
client
.post("https://authserver.mojang.com/refresh")
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::json!(
{
"accessToken": access_token,
"clientToken": client_token,
"selectedProfile": {
"id": selected_profile.id,
"name": selected_profile.name,
},
"requestUser": request_user,
}
)
.to_string(),
)
.send()
.await
.unwrap()
.json()
.await
.unwrap()
}

View File

@@ -1,7 +1,9 @@
use crate::launcher::meta::{
Asset, AssetIndex, AssetsIndex, DownloadType, Library, Os, RuleAction, VersionInfo,
Asset, AssetIndex, AssetsIndex, DownloadType, Library, Os, OsRule, RuleAction, VersionInfo,
};
use futures::future;
use regex::Regex;
use reqwest::{Error, Response};
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::Path;
@@ -33,7 +35,7 @@ pub async fn download_client(client_path: &Path, version_info: &VersionInfo) {
pub async fn download_assets(
assets_path: &Path,
legacy_path: Option<&Path>,
meta: AssetIndex,
meta: &AssetIndex,
index: &AssetsIndex,
) {
save_file(
@@ -66,7 +68,7 @@ async fn download_asset(
))
.await;
let resource_path = assets_path.join(sub_hash).join(&asset.hash);
let resource_path = assets_path.join("objects").join(sub_hash).join(&asset.hash);
save_file(resource_path.as_path(), &resource);
if let Some(legacy_path) = legacy_path {
@@ -87,28 +89,7 @@ pub async fn download_libraries(libraries_path: &Path, natives_path: &Path, libr
async fn download_library(libraries_path: &Path, natives_path: &Path, library: &Library) {
if let Some(rules) = &library.rules {
let mut allowed = true;
for rule in rules {
match rule.action {
RuleAction::Allow => {
if let Some(os) = &rule.os {
allowed = os.name == &get_os()
} else {
allowed = true
}
}
RuleAction::Disallow => {
if let Some(os) = &rule.os {
allowed = os.name != &get_os()
} else {
allowed = false
}
}
}
}
if !allowed {
if !super::rules::parse_rules(rules.as_slice()) {
return;
}
}
@@ -143,14 +124,17 @@ async fn download_library_jar(
if let Some(library) = &library.downloads.artifact {
let bytes = download_file(&library.url).await;
save_file(
&libraries_path
.join(package)
.join(name)
.join(version)
.join(format!("{}-{}.jar", name, version)),
&bytes,
);
let mut path = libraries_path.to_path_buf();
for directory in package.split(".") {
path.push(directory);
}
path.push(name);
path.push(version);
path.push(format!("{}-{}.jar", name, version));
save_file(&path, &bytes);
}
}
@@ -171,17 +155,21 @@ async fn download_native(
let parsed_key = os_key.replace("${arch}", "32");
if let Some(native) = classifiers.get(&*parsed_key) {
let path = &libraries_path
.join(package)
.join(name)
.join(version)
.join(format!("{}-{}-{}.jar", name, version, parsed_key));
let mut path = libraries_path.to_path_buf();
for directory in package.split(".") {
path.push(directory);
}
path.push(name);
path.push(version);
path.push(format!("{}-{}-{}.jar", name, version, parsed_key));
let bytes = download_file(&native.url).await;
save_file(path, &bytes);
save_file(&path, &bytes);
let file = File::open(path).unwrap();
let file = File::open(&path).unwrap();
let reader = BufReader::new(file);
let mut archive = zip::ZipArchive::new(reader).unwrap();
@@ -199,20 +187,26 @@ fn save_file(path: &Path, bytes: &bytes::Bytes) {
}
async fn download_file(url: &str) -> bytes::Bytes {
reqwest::Client::builder()
let client = reqwest::Client::builder()
.pool_max_idle_per_host(0)
.tcp_keepalive(Some(std::time::Duration::from_secs(10)))
.build()
.unwrap()
.get(url)
.send()
.await
.unwrap()
.bytes()
.await
.unwrap()
.unwrap();
for attempt in 1..4 {
let result = client.get(url).send().await;
match result {
Ok(x) => return x.bytes().await.unwrap(),
Err(e) if attempt <= 3 => continue,
Err(e) => panic!(e),
}
}
unreachable!()
}
fn get_os() -> Os {
pub fn get_os() -> Os {
match std::env::consts::OS {
"windows" => Os::Windows,
"macos" => Os::Osx,

View File

@@ -11,6 +11,17 @@ pub enum VersionType {
OldBeta,
}
impl VersionType {
pub fn as_str(&self) -> &'static str {
match self {
VersionType::Release => "release",
VersionType::Snapshot => "snapshot",
VersionType::OldAlpha => "old_alpha",
VersionType::OldBeta => "old_beta",
}
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Version {
@@ -100,14 +111,22 @@ pub enum Os {
#[derive(Serialize, Deserialize, Debug)]
pub struct OsRule {
pub name: Os,
pub name: Option<Os>,
pub version: Option<String>,
pub arch: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LibraryRule {
pub struct FeatureRule {
pub is_demo_user: Option<bool>,
pub has_custom_resolution: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Rule {
pub action: RuleAction,
pub os: Option<OsRule>,
pub feature: Option<FeatureRule>,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -121,19 +140,44 @@ pub struct Library {
pub extract: Option<LibraryExtract>,
pub name: String,
pub natives: Option<HashMap<Os, String>>,
pub rules: Option<Vec<LibraryRule>>,
pub rules: Option<Vec<Rule>>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum ArgumentValue {
Single(String),
Many(Vec<String>),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Argument {
Normal(String),
Ruled {
rules: Vec<Rule>,
value: ArgumentValue,
},
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ArgumentType {
Game,
Jvm,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct VersionInfo {
pub arguments: Option<HashMap<ArgumentType, Vec<Argument>>>,
pub asset_index: AssetIndex,
pub assets: String,
pub downloads: HashMap<DownloadType, Download>,
pub id: String,
pub libraries: Vec<Library>,
pub main_class: String,
pub minecraft_arguments: String,
pub minecraft_arguments: Option<String>,
pub minimum_launcher_version: u32,
pub release_time: DateTime<Utc>,
pub time: DateTime<Utc>,

View File

@@ -1,4 +1,95 @@
mod auth;
use std::path::Path;
use std::process::{Command, Stdio};
mod args;
pub mod auth;
pub mod download;
pub mod java;
pub mod meta;
mod rules;
pub async fn launch_minecraft(version_name: &str, root_dir: &Path) {
let manifest = meta::fetch_version_manifest().await.unwrap();
let version = meta::fetch_version_info(
manifest
.versions
.iter()
.find(|x| x.id == version_name)
.unwrap(),
)
.await
.unwrap();
//download_minecraft(&version, root_dir).await;
let auth = auth::login("username", "password", true).await;
let arguments = version.arguments.unwrap();
let profile = auth.selected_profile.unwrap();
let mut child = Command::new("java")
.args(args::get_jvm_arguments(
arguments
.get(&meta::ArgumentType::Jvm)
.map(|x| x.as_slice()),
&*root_dir.join("natives").join(&version.id),
&*args::get_class_paths(
&*root_dir.join("libraries"),
version.libraries.as_slice(),
&*root_dir
.join("versions")
.join(&version.id)
.join(format!("{}.jar", &version.id)),
),
))
.arg(version.main_class)
.args(args::get_minecraft_arguments(
arguments
.get(&meta::ArgumentType::Game)
.map(|x| x.as_slice()),
version.minecraft_arguments.as_deref(),
&*auth.access_token,
&*profile.name,
&profile.id,
&*version.id,
&version.asset_index.id,
root_dir,
&*root_dir.join("assets"),
&version.type_,
))
.current_dir(root_dir)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
child.wait().unwrap();
}
pub async fn download_minecraft(version: &meta::VersionInfo, root_dir: &Path) {
let assets_dir = meta::fetch_assets_index(&version).await.unwrap();
let legacy_dir = root_dir.join("resources");
futures::future::join3(
download::download_client(&*root_dir.join("versions"), &version),
download::download_assets(
&*root_dir.join("assets"),
if version.assets == "legacy" {
Some(legacy_dir.as_path())
} else {
None
},
&version.asset_index,
&assets_dir,
),
download::download_libraries(
&*root_dir.join("libraries"),
&*root_dir.join("natives").join(&version.id),
version.libraries.as_slice(),
),
)
.await;
}

View File

@@ -0,0 +1,52 @@
use crate::launcher::download::get_os;
use crate::launcher::meta::{OsRule, Rule, RuleAction};
use regex::Regex;
pub fn parse_rules(rules: &[Rule]) -> bool {
rules.iter().all(|x| parse_rule(x))
}
pub fn parse_rule(rule: &Rule) -> bool {
let result = if let Some(os) = &rule.os {
parse_os_rule(os)
} else if let Some(feature) = &rule.feature {
false
} else {
true
};
return match rule.action {
RuleAction::Allow => result,
RuleAction::Disallow => !result,
};
}
pub fn parse_os_rule(rule: &OsRule) -> bool {
if let Some(arch) = &rule.arch {
match arch.as_str() {
"x86" => {
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
return false;
}
"arm" => {
#[cfg(not(target_arch = "arm"))]
return false;
}
_ => {}
}
}
if let Some(name) = &rule.name {
if &get_os() != name {
return false;
}
}
if let Some(version) = &rule.version {
let regex = Regex::new(version.as_str()).unwrap();
if !regex.is_match(&*sys_info::os_release().unwrap()) {
return false;
}
}
true
}