Vanilla works but fabric doesn't
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug
|
||||
target
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Generated by cargo mutants
|
||||
# Contains mutation testing data
|
||||
**/mutants.out*/
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
Generated
+1539
File diff suppressed because it is too large
Load Diff
+25
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "smallauncher_lib"
|
||||
version = "0.1.1"
|
||||
description = "smallauncher core library"
|
||||
edition = "2021"
|
||||
authors = ["Heráclito de Quadros Saldanha <[email protected]>"]
|
||||
license = "MIT OR Unlicense"
|
||||
keywords = ["smallauncher", "minecraft", "launcher"]
|
||||
homepage = "https://smallauncher.com/"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.115"
|
||||
ureq = { version = "2.9.6", features = ["json"] }
|
||||
log = "0.4.21"
|
||||
sha1 = "0.10.6"
|
||||
hex = "0.4"
|
||||
zip = "2.1.5"
|
||||
oauth2 = { version = "5.0.0", default-features = false }
|
||||
open = "5"
|
||||
uuid = "1.13.1"
|
||||
md5 = "0.7"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
@@ -0,0 +1,4 @@
|
||||
use_field_init_shorthand = true
|
||||
newline_style = "Unix"
|
||||
hard_tabs = true
|
||||
max_width = 150
|
||||
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Assets {
|
||||
pub objects: collections::HashMap<String, Asset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Asset {
|
||||
pub hash: String,
|
||||
pub size: usize,
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::collections;
|
||||
|
||||
use crate::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
impl JavaVersions {
|
||||
pub const DEFAULT_URL: &'static str =
|
||||
"https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct JavaVersions {
|
||||
pub linux: PlatformVersion,
|
||||
pub linux_i386: PlatformVersion,
|
||||
pub mac_os: PlatformVersion,
|
||||
pub mac_os_arm64: PlatformVersion,
|
||||
pub windows_arm64: PlatformVersion,
|
||||
pub windows_x64: PlatformVersion,
|
||||
pub windows_x86: PlatformVersion,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PlatformVersion {
|
||||
pub java_runtime_alpha: Vec<Version>,
|
||||
pub java_runtime_beta: Vec<Version>,
|
||||
pub java_runtime_delta: Vec<Version>,
|
||||
pub java_runtime_gamma: Vec<Version>,
|
||||
pub java_runtime_gamma_snapshot: Vec<Version>,
|
||||
pub jre_legacy: Vec<Version>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Version {
|
||||
pub manifest: api::meta::Download,
|
||||
pub version: VersionName,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VersionName {
|
||||
pub name: String,
|
||||
pub released: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
pub enum File {
|
||||
Directory,
|
||||
Link { target: String },
|
||||
File { executable: bool, downloads: Downloads },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct Files {
|
||||
pub files: collections::HashMap<String, File>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Downloads {
|
||||
pub raw: Download,
|
||||
pub lzma: Option<Download>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Download {
|
||||
pub url: String,
|
||||
pub size: usize,
|
||||
pub sha1: String,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
impl Manifest {
|
||||
pub const DEFAULT_URL: &'static str = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
|
||||
#[inline(always)]
|
||||
pub fn get_latest_snapshot(&self) -> Option<&Version> {
|
||||
self.get_version(&self.latest.snapshot)
|
||||
}
|
||||
#[inline(always)]
|
||||
pub fn get_latest_release(&self) -> Option<&Version> {
|
||||
self.get_version(&self.latest.release)
|
||||
}
|
||||
pub fn get_version(&self, name: &str) -> Option<&Version> {
|
||||
for version in &self.versions {
|
||||
if version.id == name {
|
||||
return Some(version);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Manifest {
|
||||
pub latest: Latest,
|
||||
pub versions: Vec<Version>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Latest {
|
||||
pub release: String,
|
||||
pub snapshot: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Version {
|
||||
pub id: String,
|
||||
pub r#type: String,
|
||||
pub url: String,
|
||||
pub time: String,
|
||||
pub release_time: String,
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Version {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub arguments: Arguments,
|
||||
pub asset_index: AssetIndex,
|
||||
pub downloads: Downloads,
|
||||
#[serde(default)]
|
||||
pub java_version: JavaVersion,
|
||||
pub libraries: Vec<Library>,
|
||||
pub main_class: String,
|
||||
pub r#type: VersionType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JavaVersion {
|
||||
pub component: String,
|
||||
pub major_version: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Arguments {
|
||||
pub game: Vec<Argument>,
|
||||
pub jvm: Vec<Argument>,
|
||||
}
|
||||
|
||||
impl Default for Arguments {
|
||||
fn default() -> Self {
|
||||
Arguments {
|
||||
game: vec![
|
||||
Argument::String("--username".to_string()),
|
||||
Argument::String("${auth_player_name}".to_string()),
|
||||
Argument::String("--version".to_string()),
|
||||
Argument::String("${version_name}".to_string()),
|
||||
Argument::String("--gameDir".to_string()),
|
||||
Argument::String("${game_directory}".to_string()),
|
||||
Argument::String("--assetsDir".to_string()),
|
||||
Argument::String("${assets_root}".to_string()),
|
||||
Argument::String("--assetIndex".to_string()),
|
||||
Argument::String("${assets_index_name}".to_string()),
|
||||
Argument::String("--uuid".to_string()),
|
||||
Argument::String("${auth_uuid}".to_string()),
|
||||
Argument::String("--accessToken".to_string()),
|
||||
Argument::String("${auth_access_token}".to_string()),
|
||||
Argument::String("--clientId".to_string()),
|
||||
Argument::String("${clientid}".to_string()),
|
||||
Argument::String("--xuid".to_string()),
|
||||
Argument::String("${auth_xuid}".to_string()),
|
||||
Argument::String("--userType".to_string()),
|
||||
Argument::String("${user_type}".to_string()),
|
||||
Argument::String("--versionType".to_string()),
|
||||
Argument::String("${version_type}".to_string()),
|
||||
],
|
||||
jvm: vec![
|
||||
Argument::String("-Djava.library.path=${natives_directory}".to_string()),
|
||||
Argument::String("-Djna.tmpdir=${natives_directory}".to_string()),
|
||||
Argument::String("-Dorg.lwjgl.system.SharedLibraryExtractPath=${natives_directory}".to_string()),
|
||||
Argument::String("-Dio.netty.native.workdir=${natives_directory}".to_string()),
|
||||
Argument::String("-Dminecraft.launcher.brand=${launcher_name}".to_string()),
|
||||
Argument::String("-Dminecraft.launcher.version=${launcher_version}".to_string()),
|
||||
Argument::String("-cp".to_string()),
|
||||
Argument::String("${classpath}".to_string()),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for JavaVersion {
|
||||
#[inline(always)]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: "jre-legacy".to_string(),
|
||||
major_version: 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionType {
|
||||
#[inline(always)]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Release => f.write_str("Release"),
|
||||
Self::Snapshot => f.write_str("Snapshot"),
|
||||
Self::OldBeta => f.write_str("OldBeta"),
|
||||
Self::OldAlpha => f.write_str("OldAlpha"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum Argument {
|
||||
String(String),
|
||||
Object(ArgumentRule),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ArgumentRule {
|
||||
pub rules: Vec<Rule>,
|
||||
pub value: ArgumentValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum ArgumentValue {
|
||||
String(String),
|
||||
List(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssetIndex {
|
||||
pub id: String,
|
||||
pub sha1: String,
|
||||
pub size: usize,
|
||||
pub total_size: usize,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Downloads {
|
||||
pub client: Download,
|
||||
pub server: Option<Download>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Download {
|
||||
pub url: String,
|
||||
pub size: usize,
|
||||
pub sha1: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VersionType {
|
||||
Release,
|
||||
Snapshot,
|
||||
OldBeta,
|
||||
OldAlpha,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Library {
|
||||
pub name: String,
|
||||
pub downloads: Option<LibraryDownload>,
|
||||
pub natives: Option<serde_json::Value>,
|
||||
pub rules: Option<Vec<Rule>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LibraryDownload {
|
||||
pub artifact: Option<Artifact>,
|
||||
pub classifiers: Option<Classifiers>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Classifiers {
|
||||
pub natives_linux: Option<Artifact>,
|
||||
pub natives_osx: Option<Artifact>,
|
||||
pub natives_windows: Option<Artifact>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Artifact {
|
||||
pub url: String,
|
||||
pub path: String,
|
||||
pub sha1: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Rule {
|
||||
pub action: Action,
|
||||
pub os: Option<Os>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Os {
|
||||
pub name: Option<OsName>,
|
||||
pub arch: Option<Arch>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Arch {
|
||||
X86,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum OsName {
|
||||
Windows,
|
||||
Osx,
|
||||
Linux,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Action {
|
||||
Allow,
|
||||
Disallow,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
pub mod assets;
|
||||
pub mod java;
|
||||
pub mod manifest;
|
||||
pub mod meta;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) fn get_from_url<T: DeserializeOwned>(url: &str) -> Result<T, error::Error> {
|
||||
Ok(ureq::get(url).call()?.into_json()?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn java_version_api() {
|
||||
let _java_versions: java::JavaVersions = get_from_url(java::JavaVersions::DEFAULT_URL).unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn manifest_api() {
|
||||
let _manifest: manifest::Manifest = get_from_url(manifest::Manifest::DEFAULT_URL).unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn all_versions_api() {
|
||||
let manifest: manifest::Manifest = get_from_url(manifest::Manifest::DEFAULT_URL).unwrap();
|
||||
for version in manifest.versions {
|
||||
let _version: meta::Version = get_from_url(&version.url).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
use std::io;
|
||||
use std::net;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub const CLIENT_ID: &'static str = match option_env!("CLIENT_ID") {
|
||||
Some(client_id) => client_id,
|
||||
None => "74909cec-49b6-4fee-aa60-1b2a57ef72e1",
|
||||
};
|
||||
pub const AUTH_URI: &'static str = "https://login.live.com/oauth20_authorize.srf";
|
||||
|
||||
const HTTP_OK: &'static [u8] = b"
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain
|
||||
Content-Length: 2
|
||||
|
||||
ok
|
||||
";
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Token {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct AuthResponse {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
pub expires_in: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct XboxAuthResponse {
|
||||
pub issue_instant: String,
|
||||
pub not_after: String,
|
||||
pub token: String,
|
||||
pub display_claims: DisplayClaims,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct DisplayClaims {
|
||||
pub xui: Vec<Xui>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Xui {
|
||||
pub uhs: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Profile {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub fn get_authorisation_code() -> error::Result<String> {
|
||||
let auth_uri = oauth2::AuthUrl::new(AUTH_URI.to_string()).unwrap();
|
||||
let redirect_uri = oauth2::RedirectUrl::new("http://localhost:6565".to_string()).unwrap();
|
||||
let client = oauth2::basic::BasicClient::new(oauth2::ClientId::new(CLIENT_ID.to_string()))
|
||||
.set_auth_uri(auth_uri)
|
||||
.set_redirect_uri(redirect_uri);
|
||||
|
||||
let (auth_url, _) = client
|
||||
.authorize_url(oauth2::CsrfToken::new_random)
|
||||
.add_scope(oauth2::Scope::new("XboxLive.signin".to_string()))
|
||||
.add_scope(oauth2::Scope::new("XboxLive.offline_access".to_string()))
|
||||
.set_response_type(&oauth2::ResponseType::new("code".to_string()))
|
||||
.url();
|
||||
|
||||
open::that_in_background(auth_url.as_str());
|
||||
|
||||
let listener = net::TcpListener::bind("127.0.0.1:6565")?;
|
||||
for stream in listener.incoming() {
|
||||
if let Ok(mut stream) = stream {
|
||||
let mut reader = io::BufReader::new(&mut stream);
|
||||
let mut request_line = String::new();
|
||||
|
||||
io::BufRead::read_line(&mut reader, &mut request_line)?;
|
||||
|
||||
let redirect_url = request_line.split_whitespace().nth(1).unwrap();
|
||||
let url = oauth2::url::Url::parse(&format!("http://localhost:6565{redirect_url}"))?;
|
||||
let mut pairs = url.query_pairs();
|
||||
let code = pairs.next().unwrap().1.to_string();
|
||||
io::Write::write(&mut stream, HTTP_OK)?;
|
||||
return Ok(code);
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn get_xbl_autentication(access_token: &str) -> error::Result<XboxAuthResponse> {
|
||||
let body = serde_json::json!({
|
||||
"Properties": {
|
||||
"AuthMethod": "RPS",
|
||||
"SiteName": "user.auth.xboxlive.com",
|
||||
"RpsTicket": format!("d={access_token}"),
|
||||
},
|
||||
"RelyingParty": "http://auth.xboxlive.com",
|
||||
"TokenType": "JWT"
|
||||
});
|
||||
Ok(ureq::post("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.send_json(body)?
|
||||
.into_json()?)
|
||||
}
|
||||
|
||||
pub fn get_xsts_autentication(xbl_token: &str) -> error::Result<XboxAuthResponse> {
|
||||
let body = serde_json::json!({
|
||||
"Properties": {
|
||||
"SandboxId": "RETAIL",
|
||||
"UserTokens": [xbl_token]
|
||||
},
|
||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
||||
"TokenType": "JWT",
|
||||
});
|
||||
Ok(ureq::post("https://xsts.auth.xboxlive.com/xsts/authorize").send_json(body)?.into_json()?)
|
||||
}
|
||||
|
||||
pub fn get_microsoft_token(authorisation_code: &str) -> error::Result<Token> {
|
||||
Ok(ureq::post("https://login.live.com/oauth20_token.srf")
|
||||
.send_form(&[
|
||||
("client_id", CLIENT_ID),
|
||||
("code", authorisation_code),
|
||||
("grant_type", "authorization_code"),
|
||||
("redirect_uri", "http://localhost:6565"),
|
||||
])?
|
||||
.into_json()?)
|
||||
}
|
||||
|
||||
pub fn get_minecraft_auth(xsts_auth: &XboxAuthResponse) -> error::Result<AuthResponse> {
|
||||
let user_hash = &xsts_auth.display_claims.xui.iter().next().unwrap().uhs;
|
||||
let token = &xsts_auth.token;
|
||||
let auth_body = serde_json::json!({
|
||||
"identityToken": format!("XBL3.0 x={};{}", user_hash, token)
|
||||
});
|
||||
Ok(ureq::post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||
.send_json(auth_body)?
|
||||
.into_json()?)
|
||||
}
|
||||
|
||||
pub fn get_user_profile(access_token: &str) -> error::Result<Profile> {
|
||||
Ok(ureq::get("https://api.minecraftservices.com/minecraft/profile")
|
||||
.set("Authorization", &format!("Bearer {access_token}"))
|
||||
.call()?
|
||||
.into_json()?)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use std::fs;
|
||||
use std::path;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub mod microsoft;
|
||||
pub mod offline;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Account {
|
||||
Microsoft { name: String, uuid: String, access_token: String },
|
||||
Offline { name: String, uuid: String },
|
||||
}
|
||||
|
||||
pub fn auth_microsoft() -> error::Result<Account> {
|
||||
let authorisation_code = microsoft::get_authorisation_code()?;
|
||||
let token = microsoft::get_microsoft_token(&authorisation_code)?;
|
||||
let xbl = microsoft::get_xbl_autentication(&token.access_token)?;
|
||||
let xsts = microsoft::get_xsts_autentication(&xbl.token)?;
|
||||
let auth = microsoft::get_minecraft_auth(&xsts)?;
|
||||
let profile = microsoft::get_user_profile(&auth.access_token)?;
|
||||
|
||||
Ok(Account::Microsoft {
|
||||
name: profile.name,
|
||||
uuid: profile.id,
|
||||
access_token: auth.access_token,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn auth_offline(name: &str) -> Account {
|
||||
let name = name.to_string();
|
||||
let uuid = offline::offline_uuid(&name);
|
||||
Account::Offline { name, uuid }
|
||||
}
|
||||
|
||||
pub fn save(auth_path: &path::Path, account: &Account) -> error::Result<()> {
|
||||
let data = serde_json::to_string_pretty(account)?;
|
||||
let name = match &account {
|
||||
Account::Microsoft { name, .. } => name,
|
||||
Account::Offline { name, .. } => name,
|
||||
};
|
||||
if !auth_path.exists() {
|
||||
fs::create_dir_all(auth_path)?;
|
||||
}
|
||||
let path = auth_path.join(format!("{name}.json"));
|
||||
fs::write(path, data.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(auth_path: &path::Path, name: &str) -> error::Result<Option<Account>> {
|
||||
if !auth_path.exists() {
|
||||
fs::create_dir_all(auth_path)?;
|
||||
}
|
||||
let path = auth_path.join(format!("{name}.json"));
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(serde_json::from_str(&fs::read_to_string(path)?)?))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub fn offline_uuid(username: &str) -> String {
|
||||
let mut hash = md5::compute(format!("OfflinePlayer:{}", username)).0;
|
||||
hash[6] = hash[6] & 0x0f | 0x30; // uuid version 3
|
||||
hash[8] = hash[8] & 0x3f | 0x80; // RFC4122 variant
|
||||
|
||||
uuid::Uuid::from_bytes(hash).to_string()
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
use crate::*;
|
||||
|
||||
use log::*;
|
||||
use std::io::Write;
|
||||
use std::{fs, io, path};
|
||||
|
||||
pub const RESOURCES_URL: &'static str = "https://resources.download.minecraft.net";
|
||||
|
||||
pub fn download<W: Write>(url: &str, writer: &mut W) -> Result<u64, error::Error> {
|
||||
let mut reader = ureq::get(url).call()?.into_reader();
|
||||
Ok(io::copy(&mut reader, writer)?)
|
||||
}
|
||||
|
||||
pub fn download_minecraft_version(minecraft_path: &path::Path, jre_path: &path::Path, name: &str) -> Result<(), error::Error> {
|
||||
let manifest: api::manifest::Manifest = api::get_from_url(api::manifest::Manifest::DEFAULT_URL)?;
|
||||
let Some(version) = manifest.get_version(name) else {
|
||||
return Err(error::Error::VersionNotFound);
|
||||
};
|
||||
let meta: api::meta::Version = api::get_from_url(&version.url)?;
|
||||
let assets: api::assets::Assets = api::get_from_url(&meta.asset_index.url)?;
|
||||
{
|
||||
let data_meta = serde_json::to_string_pretty(&meta)?;
|
||||
let data_assets = serde_json::to_string_pretty(&assets)?;
|
||||
|
||||
let path_client = path!(minecraft_path, "versions", &version.id, format!("{0}.jar", version.id));
|
||||
let path_meta = path!(minecraft_path, "versions", &version.id, format!("{0}.json", version.id));
|
||||
let path_assets = path!(minecraft_path, "assets", "indexes", format!("{0}.json", meta.asset_index.id));
|
||||
|
||||
let mut file_meta = file::create_or_open_file(&path_meta)?;
|
||||
let mut file_assets = file::create_or_open_file(&path_assets)?;
|
||||
|
||||
if !file::file_hash(&meta.downloads.client.sha1, &path_client).unwrap_or_default() {
|
||||
let mut file_client = file::create_or_open_file(&path_client)?;
|
||||
info!("Downloading client: {path_client:?}");
|
||||
download(&meta.downloads.client.url, &mut file_client)?;
|
||||
}
|
||||
|
||||
file_meta.write(data_meta.as_bytes())?;
|
||||
file_assets.write(data_assets.as_bytes())?;
|
||||
}
|
||||
{
|
||||
//this file not is used per smallauncher to launch the game, but some mod installers (forge) expect this file exists
|
||||
let profile_path = path!(minecraft_path, "launcher_profiles.json");
|
||||
if !profile_path.exists() {
|
||||
fs::write(profile_path, r#"{"profiles":{}}"#)?;
|
||||
}
|
||||
}
|
||||
{
|
||||
let java_versions: api::java::JavaVersions = api::get_from_url(api::java::JavaVersions::DEFAULT_URL)?;
|
||||
let java_version = meta.java_version;
|
||||
#[cfg(target_os = "linux")]
|
||||
let version = java_versions.linux;
|
||||
#[cfg(target_os = "windows")]
|
||||
let version = java_versions.windows_x64;
|
||||
#[cfg(target_os = "macos")]
|
||||
let version = java_versions.mac_os;
|
||||
let java_component = java_version.component;
|
||||
let list = match java_component.as_str() {
|
||||
"java-runtime-alpha" => version.java_runtime_alpha,
|
||||
"java-runtime-beta" => version.java_runtime_beta,
|
||||
"java-runtime-delta" => version.java_runtime_delta,
|
||||
"java-runtime-gamma" => version.java_runtime_gamma,
|
||||
"java-runtime-gamma-snapshot" => version.java_runtime_gamma_snapshot,
|
||||
"jre-legacy" => version.jre_legacy,
|
||||
_ => return Err(error::Error::JavaVersionNotSupported),
|
||||
};
|
||||
let Some(version) = list.get(0) else {
|
||||
return Err(error::Error::JavaVersionNotSupported);
|
||||
};
|
||||
let files: api::java::Files = api::get_from_url(&version.manifest.url)?;
|
||||
for (path, file) in files.files {
|
||||
let path = path!(jre_path, &java_component, path);
|
||||
match file {
|
||||
api::java::File::Directory => fs::create_dir_all(path)?,
|
||||
api::java::File::Link { .. } => {}
|
||||
#[cfg(target_family = "unix")]
|
||||
api::java::File::File { downloads, executable } => {
|
||||
let file_download = downloads.raw;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if !file::file_hash(&file_download.sha1, &path).unwrap_or_default() {
|
||||
let mut file = file::create_or_open_file(&path)?;
|
||||
info!("Downloading java file: {path:?}");
|
||||
download(&file_download.url, &mut file)?;
|
||||
if executable {
|
||||
let perm = fs::Permissions::from_mode(0o755);
|
||||
file.set_permissions(perm)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
api::java::File::File { downloads, .. } => {
|
||||
let file_download = downloads.raw;
|
||||
if !file::file_hash(&file_download.sha1, &path).unwrap_or_default() {
|
||||
let mut file = file::create_or_open_file(&path)?;
|
||||
info!("Downloading java file: {path:?}");
|
||||
download(&file_download.url, &mut file)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const OS_NAME: Option<api::meta::OsName> = Some(api::meta::OsName::Linux);
|
||||
#[cfg(target_os = "windows")]
|
||||
const OS_NAME: Option<api::meta::OsName> = Some(api::meta::OsName::Windows);
|
||||
#[cfg(target_arch = "x86")]
|
||||
const ARCH: Option<api::meta::Arch> = Some(api::meta::Arch::X86);
|
||||
#[cfg(not(target_arch = "x86"))]
|
||||
const ARCH: Option<api::meta::Arch> = None;
|
||||
|
||||
'l: for library in &meta.libraries {
|
||||
let Some(downloads) = &library.downloads else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(classifier) = &downloads.classifiers {
|
||||
#[cfg(target_os = "linux")]
|
||||
let Some(library_download) = &classifier.natives_linux
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let Some(library_download) = &classifier.natives_windows
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let Some(library_download) = &classifier.natives_osx
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let path = path!(minecraft_path, "libraries", &library_download.path);
|
||||
if !file::file_hash(&library_download.sha1, &path).unwrap_or_default() {
|
||||
let mut file = file::create_or_open_file(&path)?;
|
||||
info!("Downloading native library: {}", library_download.path);
|
||||
download(&library_download.url, &mut file)?;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(rules) = &library.rules {
|
||||
for api::meta::Rule { os, action } in rules {
|
||||
match (os, action) {
|
||||
(Some(api::meta::Os { name: OS_NAME, arch: ARCH }), api::meta::Action::Allow) => {}
|
||||
(Some(api::meta::Os { name: OS_NAME, arch: ARCH }), api::meta::Action::Disallow) => continue 'l,
|
||||
(None, api::meta::Action::Allow) => {}
|
||||
(None, api::meta::Action::Disallow) => continue 'l,
|
||||
(_, api::meta::Action::Disallow) => {}
|
||||
(_, api::meta::Action::Allow) => continue 'l,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(library_download) = &downloads.artifact {
|
||||
let path = path!(minecraft_path, "libraries", &library_download.path);
|
||||
if !file::file_hash(&library_download.sha1, &path).unwrap_or_default() {
|
||||
let mut file = file::create_or_open_file(&path)?;
|
||||
info!("Downloading library: {}", library_download.path);
|
||||
download(&library_download.url, &mut file)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
for (name, asset) in assets.objects {
|
||||
let prefix_hash = &asset.hash[0..2];
|
||||
let hash = &asset.hash;
|
||||
let path = path!(minecraft_path, "assets", "objects", prefix_hash, hash);
|
||||
let url = format!("{RESOURCES_URL}/{prefix_hash}/{hash}");
|
||||
if !file::file_hash(hash, &path).unwrap_or_default() {
|
||||
info!("Downloading asset: {name}");
|
||||
let mut file = file::create_or_open_file(&path)?;
|
||||
download(&url, &mut file)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
VersionNotFound,
|
||||
JavaVersionNotSupported,
|
||||
Serialize(serde_json::Error),
|
||||
Io(std::io::Error),
|
||||
Network(ureq::Error),
|
||||
OsString(std::ffi::OsString),
|
||||
Zip(zip::result::ZipError),
|
||||
ParseURL(oauth2::url::ParseError),
|
||||
}
|
||||
|
||||
impl From<ureq::Error> for Error {
|
||||
#[inline(always)]
|
||||
fn from(value: ureq::Error) -> Self {
|
||||
Self::Network(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
#[inline(always)]
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
#[inline(always)]
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
Self::Serialize(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::ffi::OsString> for Error {
|
||||
#[inline(always)]
|
||||
fn from(value: std::ffi::OsString) -> Self {
|
||||
Self::OsString(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<zip::result::ZipError> for Error {
|
||||
#[inline(always)]
|
||||
fn from(value: zip::result::ZipError) -> Self {
|
||||
Self::Zip(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<oauth2::url::ParseError> for Error {
|
||||
#[inline(always)]
|
||||
fn from(value: oauth2::url::ParseError) -> Self {
|
||||
Self::ParseURL(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
const META_SERVER: &str = "https://meta.fabricmc.net/";
|
||||
const MAVEN_SERVER: &str = "https://maven.fabricmc.net/";
|
||||
|
||||
fn query_meta_json(path: &str) -> Value {
|
||||
ureq::get(&format!("{META_SERVER}{path}")).call().unwrap().into_json().unwrap()
|
||||
}
|
||||
|
||||
fn download_maven(url: &str, path: &Path) {
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let mut file = File::create(path).unwrap();
|
||||
if url.starts_with(MAVEN_SERVER) {
|
||||
crate::download::download(&format!("{url}"), &mut file).unwrap();
|
||||
} else {
|
||||
crate::download::download(&format!("{META_SERVER}{url}"), &mut file).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn library_from_json(json: &Value) -> Option<(String, String)> {
|
||||
let name = json.get("name")?.as_str()?;
|
||||
let url = json.get("url")?.as_str()?;
|
||||
|
||||
// Split "group:artifact:version"
|
||||
let parts: Vec<&str> = name.splitn(3, ':').collect();
|
||||
if parts.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let group = parts[0].replace('.', "/");
|
||||
let artifact = parts[1];
|
||||
let version = parts[2];
|
||||
|
||||
// Build remote URL (like Java's getURL)
|
||||
let jar_name = format!("{artifact}-{version}.jar");
|
||||
let remote_path = format!("{}/{}/{}/{jar_name}", group, artifact, version);
|
||||
let full_url = format!("{}{}", url, remote_path);
|
||||
|
||||
// Build local path (like Java's getPath)
|
||||
let group_fs = parts[0].replace('.', &std::path::MAIN_SEPARATOR.to_string());
|
||||
let local_path = PathBuf::from(group_fs)
|
||||
.join(artifact)
|
||||
.join(version)
|
||||
.join(jar_name)
|
||||
.to_string_lossy()
|
||||
.replace(' ', "_");
|
||||
|
||||
Some((full_url, local_path))
|
||||
}
|
||||
|
||||
pub fn install_fabric(minecraft_path: &Path, minecraft_version: &str, loader_version: &str) -> Result<(), crate::error::Error> {
|
||||
let profile_name = format!("{minecraft_version}-fabric");
|
||||
let versions_dir = minecraft_path.join("versions");
|
||||
let profile_dir = versions_dir.join(&profile_name);
|
||||
let profile_json = profile_dir.join(format!("{profile_name}.json"));
|
||||
let libs_dir = minecraft_path.join("libraries");
|
||||
|
||||
let json = query_meta_json(&format!("v2/versions/loader/{minecraft_version}/{loader_version}/profile/json"));
|
||||
|
||||
fs::create_dir_all(&profile_dir).unwrap();
|
||||
|
||||
fs::write(&profile_json, json.to_string()).unwrap();
|
||||
|
||||
fs::copy(
|
||||
versions_dir.join(minecraft_version).join(format!("{minecraft_version}.jar")),
|
||||
profile_dir.join(profile_name + ".jar"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for lib_json in json.get("libraries").unwrap().as_array().unwrap() {
|
||||
if let Some((url, path)) = library_from_json(lib_json) {
|
||||
let lib_file = libs_dir.join(path);
|
||||
download_maven(&url, &lib_file);
|
||||
} else {
|
||||
eprintln!("Failed to get json from {lib_json}")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
use std::{ffi, fs, io, path};
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) fn create_or_open_file<S: AsRef<ffi::OsStr>>(path: &S) -> Result<fs::File, io::Error> {
|
||||
let path = path::Path::new(path);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
Ok(fs::File::create(&path)?)
|
||||
}
|
||||
|
||||
pub(crate) fn file_hash<P: AsRef<path::Path>>(hash: &str, path: &P) -> Result<bool, error::Error> {
|
||||
use sha1::Digest;
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut hasher = sha1::Sha1::new();
|
||||
io::copy(&mut file, &mut hasher)?;
|
||||
let reader_hash = hex::encode(hasher.finalize());
|
||||
Ok(reader_hash == hash)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn from_json_file<T: serde::de::DeserializeOwned, P: AsRef<path::Path>>(path: P) -> Result<T, error::Error> {
|
||||
Ok(serde_json::from_str(&fs::read_to_string(path)?)?)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! path {
|
||||
() => {
|
||||
std::path::PathBuf::new()
|
||||
};
|
||||
($($segment:expr),+ $(,)?) => {
|
||||
{
|
||||
let mut path = std::path::PathBuf::new();
|
||||
$(
|
||||
path.push($segment);
|
||||
)+
|
||||
path
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! osStr {
|
||||
() => {
|
||||
std::ffi::OsString::new()
|
||||
};
|
||||
($($segment:expr),+ $(,)?) => {
|
||||
{
|
||||
let mut string = std::ffi::OsString::new();
|
||||
$(
|
||||
string.push($segment);
|
||||
)+
|
||||
string
|
||||
}
|
||||
};
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
use crate::*;
|
||||
|
||||
use std::ffi;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path;
|
||||
use std::process;
|
||||
|
||||
const CLIENT_ID: &'static str = "42";
|
||||
const LAUNCHER_NAME: &'static str = "miniLauncher";
|
||||
const LAUNCHER_VERSION: &'static str = "42";
|
||||
|
||||
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
|
||||
fn merge(a: serde_json::Value, b: serde_json::Value) -> serde_json::Value {
|
||||
match (a, b) {
|
||||
(serde_json::Value::Object(mut a), serde_json::Value::Object(b)) => {
|
||||
for (k, v2) in b {
|
||||
if let Some(v1) = a.remove(&k) {
|
||||
a.insert(k, merge(v1, v2));
|
||||
} else {
|
||||
a.insert(k, v2);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(a)
|
||||
}
|
||||
(serde_json::Value::Array(mut a), serde_json::Value::Array(b)) => {
|
||||
a.extend(b);
|
||||
serde_json::Value::Array(a)
|
||||
}
|
||||
(_, b) => b,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch_minecraft_version(
|
||||
game_path: &path::Path,
|
||||
jre_path: &path::Path,
|
||||
version_name: &str,
|
||||
account: &auth::Account,
|
||||
) -> Result<(), error::Error> {
|
||||
let meta: api::meta::Version = {
|
||||
let meta_path = path!(game_path, "versions", &version_name, format!("{version_name}.json"));
|
||||
let mut map1: serde_json::Map<String, serde_json::Value> = file::from_json_file(&meta_path)?;
|
||||
match map1.remove("inheritsFrom") {
|
||||
Some(serde_json::Value::String(version_name)) => {
|
||||
let meta_path = path!(game_path, "versions", &version_name, format!("{version_name}.json"));
|
||||
let map2: serde_json::Map<String, serde_json::Value> = file::from_json_file(&meta_path)?;
|
||||
let v1 = serde_json::Value::Object(map1);
|
||||
let v2 = serde_json::Value::Object(map2);
|
||||
serde_json::from_value(merge(v2, v1))?
|
||||
}
|
||||
_ => serde_json::from_value(serde_json::Value::Object(map1))?,
|
||||
}
|
||||
};
|
||||
|
||||
extract_natives(&meta, &game_path)?;
|
||||
|
||||
let main_class = &meta.main_class;
|
||||
let java_component = &meta.java_version.component;
|
||||
let jvm_args = generate_jvm_args(&meta, &game_path);
|
||||
let game_args = generate_game_args(&meta, &game_path, account);
|
||||
#[cfg(target_family = "unix")]
|
||||
let jre_bin = path!(jre_path, java_component, "bin", "java");
|
||||
#[cfg(target_family = "windows")]
|
||||
let jre_bin = path!(jre_path, java_component, "bin", "java.exe");
|
||||
let mut command = process::Command::new(jre_bin);
|
||||
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
||||
std::os::windows::process::CommandExt::creation_flags(&mut command, DETACHED_PROCESS);
|
||||
command.current_dir(game_path).args(jvm_args).arg(main_class).args(game_args);
|
||||
|
||||
log::info!("Spawning command: {:?}", command);
|
||||
command.spawn()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_natives(meta: &api::meta::Version, game_path: &path::Path) -> Result<(), error::Error> {
|
||||
for library in &meta.libraries {
|
||||
let Some(downloads) = &library.downloads else {
|
||||
continue;
|
||||
};
|
||||
let Some(classifier) = &downloads.classifiers else {
|
||||
continue;
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
let Some(library_download) = &classifier.natives_linux
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let Some(library_download) = &classifier.natives_windows
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let Some(library_download) = &classifier.natives_osx
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = path!(game_path, "libraries", &library_download.path);
|
||||
let file = fs::File::open(&path)?;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
for i in 0..zip.len() {
|
||||
let mut zip_file = zip.by_index(i)?;
|
||||
let name = zip_file.name();
|
||||
if !(name.ends_with(".so") || name.ends_with(".dll") || name.ends_with(".dylib")) {
|
||||
continue;
|
||||
}
|
||||
let path = path!(game_path, "bin", name);
|
||||
log::info!("Extracting {path:?}");
|
||||
let mut file = file::create_or_open_file(&path)?;
|
||||
io::copy(&mut zip_file, &mut file)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_game_args(meta: &api::meta::Version, game_path: &path::Path, account: &auth::Account) -> Vec<ffi::OsString> {
|
||||
let mut args = Vec::new();
|
||||
let arguments = &meta.arguments;
|
||||
let assets_path = path!(game_path, "assets");
|
||||
for arg in &arguments.game {
|
||||
match arg {
|
||||
api::meta::Argument::String(str) => match str.as_str() {
|
||||
"${clientid}" => args.push(osStr!(CLIENT_ID)),
|
||||
"${auth_player_name}" => {
|
||||
match &account {
|
||||
auth::Account::Microsoft { name, .. } => args.push(osStr!(name)),
|
||||
auth::Account::Offline { name, .. } => args.push(osStr!(name)),
|
||||
};
|
||||
}
|
||||
"${auth_uuid}" => {
|
||||
match &account {
|
||||
auth::Account::Microsoft { uuid, .. } => args.push(osStr!(uuid)),
|
||||
auth::Account::Offline { uuid, .. } => args.push(osStr!(uuid)),
|
||||
};
|
||||
}
|
||||
"${user_type}" => {
|
||||
match &account {
|
||||
auth::Account::Microsoft { .. } => args.push(osStr!("msa")),
|
||||
auth::Account::Offline { .. } => args.push(osStr!()),
|
||||
};
|
||||
}
|
||||
"${auth_access_token}" => match &account {
|
||||
auth::Account::Microsoft { access_token, .. } => args.push(osStr!(access_token)),
|
||||
auth::Account::Offline { .. } => args.push(osStr!()),
|
||||
},
|
||||
"${game_directory}" => args.push(osStr!(game_path)),
|
||||
"${version_name}" => args.push(osStr!(&meta.id)),
|
||||
"${assets_root}" => args.push(osStr!(&assets_path)),
|
||||
"${assets_index_name}" => args.push(osStr!(&meta.asset_index.id)),
|
||||
"${version_type}" => args.push(osStr!(meta.r#type.to_string())),
|
||||
"--xuid" | "${auth_xuid}" => {}
|
||||
str => args.push(osStr!(str)),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
args.push(osStr!("--userProperties"));
|
||||
args.push(osStr!("{}"));
|
||||
args
|
||||
}
|
||||
|
||||
pub fn generate_jvm_args(meta: &api::meta::Version, game_path: &path::Path) -> Vec<ffi::OsString> {
|
||||
let mut args = Vec::new();
|
||||
let arguments = &meta.arguments;
|
||||
let natives_dir = path!(game_path, "bin");
|
||||
let class_paths = get_class_paths(meta, game_path);
|
||||
for arg in &arguments.jvm {
|
||||
match arg {
|
||||
api::meta::Argument::String(str) => match str.as_str() {
|
||||
"-Dminecraft.launcher.version=${launcher_version}" => {
|
||||
let value = osStr!("-Dminecraft.launcher.version=", LAUNCHER_VERSION);
|
||||
args.push(value);
|
||||
}
|
||||
"-Dminecraft.launcher.brand=${launcher_name}" => {
|
||||
let value = osStr!("-Dminecraft.launcher.brand=", LAUNCHER_NAME);
|
||||
args.push(value);
|
||||
}
|
||||
"-Djava.library.path=${natives_directory}" => {
|
||||
let value = osStr!("-Djava.library.path=", &natives_dir);
|
||||
args.push(value);
|
||||
}
|
||||
"-Djna.tmpdir=${natives_directory}" => {
|
||||
let value = osStr!("-Djna.tmpdir=", &natives_dir);
|
||||
args.push(value);
|
||||
}
|
||||
"-Dorg.lwjgl.system.SharedLibraryExtractPath=${natives_directory}" => {
|
||||
let value = osStr!("-Dorg.lwjgl.system.SharedLibraryExtractPath=", &natives_dir);
|
||||
args.push(value);
|
||||
}
|
||||
"-Dio.netty.native.workdir=${natives_directory}" => {
|
||||
let value = osStr!("-Dio.netty.native.workdir=", &natives_dir);
|
||||
args.push(value);
|
||||
}
|
||||
"${classpath}" => args.push(class_paths.clone()),
|
||||
_ => args.push(ffi::OsString::from(str)),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
pub fn get_class_paths(meta: &api::meta::Version, game_path: &path::Path) -> ffi::OsString {
|
||||
#[cfg(target_os = "linux")]
|
||||
const OS_NAME: Option<api::meta::OsName> = Some(api::meta::OsName::Linux);
|
||||
#[cfg(target_os = "windows")]
|
||||
const OS_NAME: Option<api::meta::OsName> = Some(api::meta::OsName::Windows);
|
||||
#[cfg(target_arch = "x86")]
|
||||
const ARCH: Option<api::meta::Arch> = Some(api::meta::Arch::X86);
|
||||
#[cfg(not(target_arch = "x86"))]
|
||||
const ARCH: Option<api::meta::Arch> = None;
|
||||
#[cfg(target_family = "unix")]
|
||||
const SEPARATOR: &'static str = ":";
|
||||
#[cfg(target_family = "windows")]
|
||||
const SEPARATOR: &'static str = ";";
|
||||
|
||||
let mut list = ffi::OsString::new();
|
||||
{
|
||||
let path = path!(game_path, "versions", &meta.id, format!("{}.jar", meta.id));
|
||||
list.push(path);
|
||||
}
|
||||
'l: for lib in &meta.libraries {
|
||||
match lib {
|
||||
api::meta::Library {
|
||||
downloads: Some(api::meta::LibraryDownload {
|
||||
artifact: Some(artifact), ..
|
||||
}),
|
||||
rules,
|
||||
..
|
||||
} => {
|
||||
if let Some(rules) = rules {
|
||||
for api::meta::Rule { os, action } in rules {
|
||||
match (os, action) {
|
||||
(Some(api::meta::Os { name: OS_NAME, arch: ARCH }), api::meta::Action::Allow) => {}
|
||||
(Some(api::meta::Os { name: OS_NAME, arch: ARCH }), api::meta::Action::Disallow) => continue 'l,
|
||||
(None, api::meta::Action::Allow) => {}
|
||||
(None, api::meta::Action::Disallow) => continue 'l,
|
||||
(_, api::meta::Action::Disallow) => {}
|
||||
(_, api::meta::Action::Allow) => continue 'l,
|
||||
}
|
||||
}
|
||||
}
|
||||
list.push(SEPARATOR);
|
||||
let path = path!(game_path, "libraries", &artifact.path);
|
||||
list.push(path);
|
||||
}
|
||||
lib => {
|
||||
let mut original_name: &str = &lib.name;
|
||||
let mut name = "";
|
||||
let mut version = "";
|
||||
let mut path = path!();
|
||||
|
||||
if let Some(i) = original_name.find(":") {
|
||||
let mut paths = &original_name[..i];
|
||||
original_name = &original_name[(i + 1)..];
|
||||
while let Some(i) = paths.find(".") {
|
||||
path = path!(path, &paths[..i]);
|
||||
paths = &paths[(i + 1)..];
|
||||
}
|
||||
path = path!(path, &paths);
|
||||
}
|
||||
if let Some(i) = original_name.find(":") {
|
||||
name = &original_name[..i];
|
||||
version = &original_name[(i + 1)..];
|
||||
}
|
||||
list.push(SEPARATOR);
|
||||
let path = path!(game_path, "libraries", &path, name, version, format!("{name}-{version}.jar"));
|
||||
list.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
list
|
||||
}
|
||||
|
||||
pub fn check_version_integrity(game_path: &path::Path, version_name: &str) -> bool {
|
||||
let path_version = path!(game_path, "versions", version_name, format!("{version_name}.json"));
|
||||
let path_client = path!(game_path, "versions", version_name, format!("{version_name}.jar"));
|
||||
let Ok(version) = file::from_json_file::<api::meta::Version, _>(&path_version) else {
|
||||
return false;
|
||||
};
|
||||
if !file::file_hash(&version.downloads.client.sha1, &path_client).unwrap_or_default() {
|
||||
return false;
|
||||
}
|
||||
let path_assets = path!(game_path, "assets", "indexes", format!("{}.json", version.asset_index.id));
|
||||
let Ok(assets) = file::from_json_file::<api::assets::Assets, _>(&path_assets) else {
|
||||
return false;
|
||||
};
|
||||
for (_, asset) in assets.objects {
|
||||
let prefix_hash = &asset.hash[0..2];
|
||||
let hash = &asset.hash;
|
||||
let path = path!(game_path, "assets", "objects", prefix_hash, hash);
|
||||
if !file::file_hash(hash, &path).unwrap_or_default() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn list_versions(game_path: &path::Path) -> Result<Vec<String>, error::Error> {
|
||||
let path = path!(game_path, "versions");
|
||||
fs::read_dir(path)?
|
||||
.map(|x| -> Result<String, error::Error> { Ok(x?.file_name().to_string_lossy().to_string()) })
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn list_all_versions() -> Result<api::manifest::Manifest, error::Error> {
|
||||
api::get_from_url(api::manifest::Manifest::DEFAULT_URL)
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod api;
|
||||
pub mod auth;
|
||||
pub mod download;
|
||||
pub mod error;
|
||||
pub mod fabric;
|
||||
mod file;
|
||||
pub mod launch;
|
||||
|
||||
pub fn download_version(version: &str) {
|
||||
let base_path = PathBuf::from(std::env::home_dir().unwrap()).join(".saturn-launcher");
|
||||
let jre_path = base_path.join("jre");
|
||||
let instances_path = base_path.join("instances");
|
||||
let instance = instances_path.join(version);
|
||||
|
||||
download::download_minecraft_version(&instance, &jre_path, version).unwrap()
|
||||
}
|
||||
|
||||
pub fn launch_version(version: &str) {
|
||||
let base_path = PathBuf::from(std::env::home_dir().unwrap()).join(".saturn-launcher");
|
||||
let jre_path = base_path.join("jre");
|
||||
let instances_path = base_path.join("instances");
|
||||
let instance = instances_path.join(version);
|
||||
|
||||
launch::launch_minecraft_version(
|
||||
&instance,
|
||||
&jre_path,
|
||||
&format!("{version}-fabric"),
|
||||
&auth::Account::Offline {
|
||||
name: "Daddy".into(),
|
||||
uuid: "Daddy".into(),
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
// download_version("1.21.4");
|
||||
smallauncher_lib::launch_version("1.21.4");
|
||||
// install_fabric("1.21.4", "0.17.2");
|
||||
}
|
||||
|
||||
fn install_fabric(version: &str, loader: &str) {
|
||||
let base_path = PathBuf::from(std::env::home_dir().unwrap()).join(".saturn-launcher");
|
||||
let instances_path = base_path.join("instances");
|
||||
let instance = instances_path.join(version);
|
||||
smallauncher_lib::fabric::install_fabric(&instance, version, loader).unwrap()
|
||||
}
|
||||
Reference in New Issue
Block a user