diff --git a/Cargo.lock b/Cargo.lock index 30a32ba..40e3da1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,8 +8,10 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "dirs", + "env_logger", "futures", "java-locator", + "log", "rand 0.9.0", "serde", "serde_json", @@ -130,6 +132,15 @@ dependencies = [ "zerocopy 0.7.35", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -157,6 +168,56 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.97" @@ -608,6 +669,12 @@ dependencies = [ "inout", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "combine" version = "4.6.7" @@ -1013,6 +1080,29 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2033,6 +2123,12 @@ dependencies = [ "libc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "isahc" version = "0.9.14" @@ -2100,6 +2196,30 @@ dependencies = [ "system-deps", ] +[[package]] +name = "jiff" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "jni" version = "0.21.1" @@ -3127,6 +3247,21 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3370,6 +3505,35 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4424,6 +4588,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "value-bag" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index 9a58e09..aa5f640 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,10 @@ dirs = "6.0.0" rand = "0.9.0" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0" -toml = "0.8.20" surf = { version = "2.3.2", features = ["hyper-client"] } base64 = "0.22.1" zip-extract = "0.2.1" -java-locator = "0.1.9" \ No newline at end of file +java-locator = "0.1.9" +log = "0.4.26" +env_logger = "0.11.7" +toml = "0.8.20" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..504d521 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# XCraft - A Modern Minecraft Launcher + +XCraft is a custom Minecraft launcher written in Rust, designed to provide enhanced flexibility and customization. It supports a custom online mode, skin and cape editing, and seamless integration with MultiMC instances. + +## Features + +- **Custom Online Mode**: Authenticate and play on your own custom Minecraft servers. +- **Skin & Cape Editing**: Easily customize your in-game appearance. +- **MultiMC Support**: Load and manage MultiMC instances directly from XCraft. +- **One-Click Mod Installation**: Install Forge, Fabric, and Omniarchive versions effortlessly. +- **Portable**: You can run launcher in portable mode from flash drive to play your lovely game everywhere you want. + +## Installation + +### Download Stable Build +You can download the latest stable build of XCraft from our Jenkins CI server: +[Download from Jenkins](https://jenkins.awain.net/job/XCraft/lastStableBuild/). + +### Build from Source + +```sh +# Clone the repository +git clone https://github.com/yourusername/XCraft.git +cd XCraft + +# Build the launcher +cargo build --release + +# Run the launcher +./target/release/xcraft +``` + +## Usage +1. Launch XCraft. +2. Configure your custom authentication settings. +3. Manage skins and capes within the built-in editor. +4. Load your MultiMC instances for easy access. +5. Enjoy a seamless Minecraft experience! + +## For Server Administrators +To make your Minecraft server compatible with XCraft, install the **XCraft-Auth** Spigot plugin. This plugin enables custom authentication and ensures seamless integration with XCraft's custom online mode. + +## Roadmap +- [ ] Add support for more mod loaders (Fabric, Forge, etc.) +- [ ] Enhance logging and debugging features +- [ ] Cross-platform support improvements + +## License +This project is licensed under the MIT License. \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 43a4978..ab5d288 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,14 +2,37 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize)] +pub struct LauncherCredentials { + pub uuid: String, + pub username: String, + pub password: String +} +#[derive(Serialize, Deserialize)] +pub struct LauncherServer { + pub domain: String, + pub port: u16, + pub session_server_port: u16, + pub credentials: LauncherCredentials +} -#[derive(Default, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] pub struct LauncherConfig { is_portable: bool, user_name: String, - pub user_secret: String, - pub java_path: String + pub java_path: String, + pub show_alpha: bool, + pub show_beta: bool, + pub show_snapshots: bool, + pub ram_amount: u32, + servers: Vec +} + +impl Default for LauncherConfig { + fn default() -> Self { + Self { is_portable: Default::default(), user_name: Default::default(), java_path: "java".to_string(), show_alpha: true, show_beta: true, show_snapshots: false, ram_amount: 1024, servers: Default::default() } + } } impl LauncherConfig { @@ -37,6 +60,10 @@ impl LauncherConfig { pub fn set_username(&mut self, user_name: String) { self.user_name = user_name; } + + pub fn add_server(&mut self, server: LauncherServer) { + self.servers.push(server); + } } pub fn get_relative_launcher_dir() -> PathBuf { diff --git a/src/launcher.rs b/src/launcher.rs index 798c9b8..28c48c7 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -1,11 +1,14 @@ use core::str; use std::io::Cursor; -use base64::{encode, Engine}; +use base64::Engine; use base64::prelude::BASE64_STANDARD; use tokio::fs::File; use tokio::process::Command; use tokio::sync::mpsc; use tokio::sync::mpsc::UnboundedSender; +use crate::config::{LauncherCredentials, LauncherServer}; +use crate::minecraft; +use crate::minecraft::session::SignUpResponse; use crate::minecraft::versions::Version; use crate::{config::LauncherConfig, minecraft::versions::VersionConfig, util}; @@ -56,10 +59,54 @@ impl Launcher { pub fn init_config(&mut self, user_name: String) { self.load_config(); self.config.set_username(user_name); - self.config.user_secret = crate::util::random_string(32); self.save_config(); } + fn save_server_info(&mut self, uuid: String, username: String, password: String, domain: String, session_server_port: u16, server_port: u16) -> (bool, &str) { + self.config.add_server(LauncherServer { + domain, + port: server_port, + session_server_port, + credentials: LauncherCredentials { + uuid, + username, + password + } + }); + self.save_config(); + (true, "You are successfully registered") + } + + pub async fn register_user_server(&mut self, server: String, username: String, password: String) -> (bool, &str) { + let mut session_server_port: u16 = 8999; + let mut server_port: u16 = 25565; + let mut domain = server.clone(); + if let Some(index) = server.find("#") { + let (a,b) = server.split_at(index+1); + session_server_port = b.parse().unwrap(); + domain = a[..a.len()-1].to_string(); + } + + if let Some(index) = domain.find(":") { + let dmc = domain.clone(); + let (a,b) = dmc.split_at(index+1); + domain = a[..a.len()-1].to_string(); + server_port = b.parse().unwrap(); + } + + println!("Server information: {}:{} session={}", domain, server_port, session_server_port); + + match minecraft::session::try_signup(domain.clone(), session_server_port, username.clone(), password.clone()).await { + Ok(status) => match status { + SignUpResponse::ServerError => (false, "Internal server error"), + SignUpResponse::BadCredentials => (false, "Username or password is not valid"), + SignUpResponse::UserAlreadyExists => (false, "User already exists"), + SignUpResponse::Registered(uuid) => self.save_server_info(uuid, username, password, domain, session_server_port, server_port) + } + Err(_e) => (false, "Internal server error") + } + } + pub fn get_instances_list(&self) -> Vec<(String, String, String)> { let mut v = Vec::new(); let mut instances = self.config.launcher_dir(); @@ -87,7 +134,7 @@ impl Launcher { v } - pub async fn launch_instance(&self, instance_name: String) { + pub async fn launch_instance(&self, instance_name: String, username: String, uuid: String, token: String) { let mut instances = self.config.launcher_dir(); instances.push("instances"); instances.push(&instance_name); @@ -145,7 +192,7 @@ impl Launcher { let mut assets_dir = self.config.launcher_dir(); assets_dir.push("assets"); - cmd.args(&["--username", self.config.user_name(), "--version", &instance_name, "--gameDir", game_dir.to_str().unwrap(), "--assetsDir", assets_dir.to_str().unwrap(), "--assetIndex", &config.assetIndex.id, "--uuid", "51820246d9fe372b81592602a5239ad9", "--accessToken", "51820246d9fe372b81592602a5239ad9", "--userProperties", "{}", "--userType", "mojang", "--width", "925", "--height", "530"]); + cmd.args(&["--username", &username, "--version", &instance_name, "--gameDir", game_dir.to_str().unwrap(), "--assetsDir", assets_dir.to_str().unwrap(), "--assetIndex", &config.assetIndex.id, "--uuid", &uuid, "--accessToken", &token, "--userProperties", "{}", "--userType", "mojang", "--width", "925", "--height", "530"]); cmd.spawn(); } } @@ -269,7 +316,7 @@ impl Launcher { pub fn init_dirs(&self) { let root = self.config.launcher_dir(); std::fs::create_dir_all(&root); - // instances assets libraries config.toml servers credentials + // instances assets libraries config.toml let mut instances = root.clone(); instances.push("instances"); @@ -279,16 +326,8 @@ impl Launcher { let mut libraries = root.clone(); libraries.push("libraries"); - let mut servers = root.clone(); - servers.push("servers"); - - let mut credentials = root.clone(); - credentials.push("credentials"); - std::fs::create_dir_all(&instances); std::fs::create_dir_all(&assets); std::fs::create_dir_all(&libraries); - std::fs::create_dir_all(&servers); - std::fs::create_dir_all(&credentials); } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3e89ef4..64b89c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,7 +111,16 @@ async fn main() { } "fetch_official_versions" => { if let Ok(versions) = crate::minecraft::versions::fetch_versions_list().await { - let versions: Vec = versions.versions.iter().map(|t| t.id.clone()).collect(); + let versions: Vec = versions.versions.iter().filter(|t| { + if !launcher.config.show_alpha && t.r#type == "old_alpha" { + return false; + } else if !launcher.config.show_beta && t.r#type == "old_beta" { + return false; + } else if !launcher.config.show_snapshots && t.r#type == "snapshot" { + return false; + } + return true; + }).map(|t| t.id.clone()).collect(); responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: [ vec!["set_downloadable_versions".to_string()], versions ].concat() }).unwrap())); } else { responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: Vec::new() }).unwrap())); @@ -159,7 +168,7 @@ async fn main() { } "run_instance" => { let instance_name = params.unwrap().params[0].clone(); - launcher.launch_instance(instance_name).await; + launcher.launch_instance(instance_name, launcher.config.user_name().to_string(), util::random_string(32), util::random_string(32)).await; } "locate_java" => { if let Ok(java_path) = java_locator::locate_file("java.exe") { @@ -169,6 +178,14 @@ async fn main() { // todo: implement error notifications } } + "add_server" => { + let params = ¶ms.unwrap().params; + let (status, msg) = launcher.register_user_server(params[0].clone(), params[1].clone(), params[2].clone()).await; + responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["add_server_response".to_string(), status.to_string(), msg.to_string()] }).unwrap())); + } + "fetch_settings" => { + responder.respond(Response::new(serde_json::to_vec(&UIMessage { params: vec!["fetch_settings_response".to_string(), launcher.config.show_alpha.to_string(), launcher.config.show_beta.to_string(), launcher.config.show_snapshots.to_string(), launcher.config.java_path.clone(), launcher.config.ram_amount.to_string()] }).unwrap())); + } _ => {} } } diff --git a/src/minecraft.rs b/src/minecraft.rs index fca9ea9..0f892ce 100644 --- a/src/minecraft.rs +++ b/src/minecraft.rs @@ -152,6 +152,49 @@ pub mod versions { } } +pub mod session { + use std::error::Error; + + use serde::{Deserialize, Serialize}; + + #[derive(Serialize)] + struct SignUpRequest { + username: String, + password: String, + } + + pub enum SignUpResponse { + Registered(String), + BadCredentials, + UserAlreadyExists, + ServerError + } + + #[derive(Deserialize)] + struct ResponseUUID { + uuid: String + } + + pub async fn try_signup(server_domain: String, port: u16, username: String, password: String) -> Result> { + let request = SignUpRequest { username, password }; + let mut r = surf::post(["http://".to_string(), server_domain, ":".to_string(), port.to_string(), "/api/register".to_string()].concat()) + .body_json(&request) + .unwrap() + .await?; + + let b= r.body_bytes().await.unwrap(); + match r.status() { + surf::StatusCode::BadRequest => Ok(SignUpResponse::BadCredentials), + surf::StatusCode::Conflict => Ok(SignUpResponse::UserAlreadyExists), + surf::StatusCode::Ok => { + let response: ResponseUUID = serde_json::from_slice(&b).unwrap(); + return Ok(SignUpResponse::Registered(response.uuid)) + }, + _ => Ok(SignUpResponse::ServerError) + } + } +} + pub mod assets { use std::{collections::HashMap, error::Error, path::PathBuf}; use serde::{Deserialize, Serialize}; diff --git a/src/www/portable.html b/src/www/portable.html index a5de571..066603b 100644 --- a/src/www/portable.html +++ b/src/www/portable.html @@ -128,19 +128,17 @@
- @@ -424,10 +442,49 @@ } else if( params[i] == "locate_java" ) { setJavaPath(params[i+1]); break; + } else if( params[i] == "add_server_response" ) { + addServerResponse(params[i+1], params[i+2]); + break; + } else if( params[i] == "fetch_settings_response" ) { + setSettings(params.slice(i+1)); + break; } } } + function setSettings(params) { + $("#show-alpha").prop('checked', (params[0] === 'true')); + $("#show-beta").prop('checked', (params[1] === 'true')); + $("#show-snapshots").prop('checked', (params[2] === 'true')); + $("#java-path").val(params[3]); + $("#ram-input").val(params[4]); + } + + function dismissPopup() { + $("#popup").addClass("hidden"); + } + + function showPopup(text) { + $("#popup_text").html(text); + $("#popup").removeClass("hidden"); + } + + function addServerResponse(status, msg) { + showPopup(msg); + } + + function addServerInstance() { + let server = $("#server_address").val(); + let username = $("#server_username").val(); + let password = $("#server_password").val(); + + $.post({url: "add_server", data: JSON.stringify({ params: [server, username, password] }) }, processParams); + } + + function addServer() { + showSection(undefined, "add-server"); + } + function setJavaPath(javaPath) { $("#java-path").val(javaPath); } @@ -517,7 +574,7 @@ $( document ).ready(async function() { $.get("check_installation", processParams); - + $.get("fetch_settings", processParams); setInterval(function() { if( !$("#loading-section").hasClass("hidden") ) { $.get("check_download_status", processParams);