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