use crate::error::{Result, LateJavaCoreError};
use crate::utils::get_file_from_archive;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
const MOJANG_LIB: &[(&str, &str)] = &[
("windows", "windows"),
("macos", "osx"),
("linux", "linux"),
];
const ARCH: &[(&str, &str)] = &[
("x86", "32"),
("x86_64", "64"),
("aarch64", "64"),
("arm", "32"),
];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinecraftLibrary {
pub name: Option<String>,
pub rules: Option<Vec<Rule>>,
pub natives: Option<HashMap<String, String>>,
pub downloads: LibraryDownloads,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
pub os: Option<OsRule>,
pub action: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsRule {
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LibraryDownloads {
pub artifact: Option<Artifact>,
pub classifiers: Option<HashMap<String, Artifact>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub sha1: Option<String>,
pub size: Option<u64>,
pub path: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinecraftVersionJson {
pub id: String,
pub libraries: Vec<MinecraftLibrary>,
pub downloads: VersionDownloads,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionDownloads {
pub client: ClientDownload,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientDownload {
pub sha1: String,
pub size: u64,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomAssetItem {
pub path: String,
pub hash: String,
pub size: u64,
pub url: String,
}
#[derive(Debug, Clone)]
pub struct LibrariesOptions {
pub path: String,
pub instance: Option<String>,
pub headers: Option<HashMap<String, String>>,
pub token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LibraryDownload {
pub sha1: Option<String>,
pub size: Option<u64>,
pub path: String,
pub r#type: String,
pub url: Option<String>,
pub content: Option<String>,
}
pub struct Libraries {
json: Option<MinecraftVersionJson>,
options: LibrariesOptions,
}
impl Libraries {
pub fn new(options: LibrariesOptions) -> Self {
Self {
json: None,
options,
}
}
pub async fn get_libraries(&mut self, json: MinecraftVersionJson) -> Result<Vec<LibraryDownload>> {
self.json = Some(json.clone());
let mut libraries = Vec::new();
for lib in &json.libraries {
let mut artifact: Option<Artifact> = None;
let mut lib_type = "Libraries".to_string();
if let Some(natives) = &lib.natives {
let current_os = std::env::consts::OS;
let mojang_os = MOJANG_LIB.iter()
.find(|(node_os, _)| *node_os == current_os)
.map(|(_, mojang_os)| *mojang_os)
.unwrap_or(current_os);
if let Some(native) = natives.get(mojang_os) {
lib_type = "Native".to_string();
let current_arch = std::env::consts::ARCH;
let arch_replacement = ARCH.iter()
.find(|(arch, _)| *arch == current_arch)
.map(|(_, replacement)| *replacement)
.unwrap_or("64");
let arch_replaced = native.replace("${arch}", arch_replacement);
if let Some(classifiers) = &lib.downloads.classifiers {
artifact = classifiers.get(&arch_replaced).cloned();
}
} else {
continue;
}
} else {
if let Some(rules) = &lib.rules {
if let Some(first_rule) = rules.first() {
if let Some(os_rule) = &first_rule.os {
if let Some(os_name) = &os_rule.name {
let current_os = std::env::consts::OS;
let mojang_os = MOJANG_LIB.iter()
.find(|(node_os, _)| *node_os == current_os)
.map(|(_, mojang_os)| *mojang_os)
.unwrap_or(current_os);
if os_name != mojang_os {
continue;
}
}
}
}
}
artifact = lib.downloads.artifact.clone();
}
if let Some(artifact) = artifact {
libraries.push(LibraryDownload {
sha1: artifact.sha1,
size: artifact.size,
path: format!("libraries/{}", artifact.path.unwrap_or_default()),
r#type: lib_type,
url: artifact.url,
content: None,
});
}
}
libraries.push(LibraryDownload {
sha1: Some(json.downloads.client.sha1),
size: Some(json.downloads.client.size),
path: format!("versions/{}/{}.jar", json.id, json.id),
r#type: "Libraries".to_string(),
url: Some(json.downloads.client.url),
content: None,
});
libraries.push(LibraryDownload {
path: format!("versions/{}/{}.json", json.id, json.id),
r#type: "CFILE".to_string(),
content: Some(serde_json::to_string(&json)?),
sha1: None,
size: None,
url: None,
});
Ok(libraries)
}
pub async fn get_assets_others(&self, url: Option<&str>) -> Result<Vec<LibraryDownload>> {
if url.is_none() {
return Ok(Vec::new());
}
let url = url.unwrap();
let mut headers = self.options.headers.clone();
if self.options.token.is_some() && headers.is_none() {
let mut auth_headers = HashMap::new();
auth_headers.insert("Authorization".to_string(), self.options.token.as_ref().unwrap().clone());
headers = Some(auth_headers);
}
let mut request = reqwest::Client::new().get(url);
if let Some(headers) = headers {
for (key, value) in headers {
request = request.header(&key, &value);
}
}
let response = request.send().await?;
let data: Vec<CustomAssetItem> = response.json().await?;
let mut assets = Vec::new();
for asset in data {
if asset.path.is_empty() {
continue;
}
let file_type = asset.path.split('/').next().unwrap_or("unknown");
assets.push(LibraryDownload {
sha1: Some(asset.hash),
size: Some(asset.size),
r#type: file_type.to_string(),
path: if let Some(instance) = &self.options.instance {
format!("instances/{}/{}", instance, asset.path)
} else {
asset.path
},
url: Some(asset.url),
content: None,
});
}
Ok(assets)
}
pub async fn natives(&self, bundle: &[LibraryDownload]) -> Result<Vec<String>> {
let json = self.json.as_ref()
.ok_or_else(|| LateJavaCoreError::Minecraft("No version JSON loaded".to_string()))?;
let natives: Vec<String> = bundle
.iter()
.filter(|item| item.r#type == "Native")
.map(|item| item.path.clone())
.collect();
if natives.is_empty() {
return Ok(Vec::new());
}
let natives_folder = format!("{}/versions/{}/natives", self.options.path, json.id);
std::fs::create_dir_all(&natives_folder)?;
for native in natives {
let full_path = format!("{}/{}", self.options.path, native);
let entries = get_file_from_archive(&full_path, None, None, true).await?;
for entry in entries {
if entry.name.starts_with("META-INF") {
continue;
}
let entry_path = format!("{}/{}", natives_folder, entry.name);
if entry.is_directory {
std::fs::create_dir_all(&entry_path)?;
} else {
std::fs::write(&entry_path, entry.data)?;
}
}
}
Ok(natives)
}
}