use std::io::{Cursor, Read};
use std::path::PathBuf;
use anyhow::{Context, bail};
use nitro_shared::minecraft::VersionManifest;
use nitro_shared::output::{MessageContents, NitroOutput};
use nitro_shared::util::DeserListOrSingle;
use nitro_shared::{UpdateDepth, translate};
use reqwest::Client;
use serde::Deserialize;
use zip::ZipArchive;
use crate::io::files::create_leading_dirs;
use crate::io::files::paths::Paths;
use crate::io::java::JavaMajorVersion;
use crate::io::json_from_file;
use crate::io::update::UpdateManager;
use crate::net::download::ProgressiveDownload;
#[derive(Deserialize, Debug, Clone)]
pub struct ClientMeta {
#[serde(alias = "minecraftArguments")]
pub arguments: args::Arguments,
#[serde(rename = "assetIndex")]
#[serde(default)]
pub asset_index: Option<AssetIndexInfo>,
#[serde(rename = "assets")]
#[serde(default)]
pub assets_version: Option<String>,
#[serde(default)]
pub downloads: Option<Downloads>,
#[serde(rename = "javaVersion")]
#[serde(default)]
pub java_info: Option<JavaInfo>,
pub libraries: Vec<libraries::Library>,
#[serde(rename = "mainClass")]
pub main_class: String,
#[serde(default)]
pub logging: Option<LogInfo>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct AssetIndexInfo {
pub url: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Downloads {
pub client: DownloadInfo,
pub server: DownloadInfo,
}
#[derive(Deserialize, Debug, Clone)]
pub struct DownloadInfo {
pub url: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct JavaInfo {
#[serde(rename = "majorVersion")]
pub major_version: JavaMajorVersion,
}
#[derive(Deserialize, Debug, Clone)]
pub struct LogInfo {
pub client: ClientLogInfo,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ClientLogInfo {
pub argument: String,
pub file: DownloadInfo,
}
pub mod args {
use super::*;
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Arguments {
New(NewArguments),
Old(String),
}
#[derive(Deserialize, Debug, Clone)]
pub struct NewArguments {
pub jvm: Vec<ArgumentItem>,
pub game: Vec<ArgumentItem>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum ArgumentItem {
Simple(String),
Conditional(ConditionalArguments),
}
#[derive(Deserialize, Debug, Clone)]
pub struct ConditionalArguments {
pub rules: Vec<conditions::Rule>,
pub value: DeserListOrSingle<String>,
}
}
pub mod libraries {
use std::collections::HashMap;
use super::*;
#[derive(Deserialize, Debug, Clone)]
pub struct Library {
#[serde(default)]
pub downloads: Downloads,
pub name: String,
#[serde(default)]
pub natives: HashMap<String, String>,
#[serde(default)]
pub rules: Vec<conditions::Rule>,
#[serde(default)]
pub extract: ExtractionRules,
#[serde(default)]
pub url: Option<String>,
}
#[derive(Deserialize, Debug, Default, Clone)]
pub struct Downloads {
pub artifact: Option<Artifact>,
#[serde(rename = "classifiers")]
#[serde(default)]
pub native_classifiers: HashMap<String, Artifact>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Artifact {
pub path: String,
pub url: String,
}
#[derive(Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct ExtractionRules {
pub exclude: Vec<String>,
}
}
pub mod conditions {
use std::fmt::Display;
use super::*;
#[derive(Deserialize, Debug, Clone)]
pub struct Rule {
pub action: RuleAction,
#[serde(default)]
pub features: RuleFeatures,
#[serde(default)]
pub os: OSConditions,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum RuleAction {
Allow,
Disallow,
}
impl RuleAction {
pub fn is_allowed(&self) -> bool {
matches!(&self, Self::Allow)
}
pub fn is_allowed_with_condition(&self, condition: bool) -> bool {
self.is_allowed() == condition
}
}
#[derive(Deserialize, Debug, Default, Clone)]
pub struct RuleFeatures {
pub is_demo_user: Option<bool>,
pub has_custom_resolution: Option<bool>,
#[serde(alias = "has_quick_plays_support")]
pub has_quick_play_support: Option<bool>,
pub is_quick_play_singleplayer: Option<bool>,
pub is_quick_play_multiplayer: Option<bool>,
pub is_quick_play_realms: Option<bool>,
}
#[derive(Deserialize, Debug, Default, Clone)]
pub struct OSConditions {
pub name: Option<OSName>,
pub arch: Option<OSArch>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum OSName {
Windows,
#[serde(rename = "windows-arm64")]
WindowsArm64,
#[serde(alias = "osx")]
#[serde(rename = "macos")]
MacOS,
#[serde(rename = "osx-arm64")]
MacOSArm64,
Linux,
#[serde(rename = "linux-arm64")]
LinuxArm64,
#[serde(untagged)]
Other(String),
}
impl Display for OSName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Windows => "windows",
Self::WindowsArm64 => "windows_arm64",
Self::MacOS => "macos",
Self::MacOSArm64 => "macos_arm64",
Self::Linux => "linux",
Self::LinuxArm64 => "linux_arm64",
Self::Other(other) => &other,
}
)
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum OSArch {
X86,
X86_64,
#[serde(alias = "aarch64")]
Arm,
}
impl Display for OSArch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::X86 => "x86",
Self::X86_64 => "x86_64",
Self::Arm => "arm",
}
)
}
}
}
pub async fn get(
version: &str,
version_manifest: &VersionManifest,
paths: &Paths,
manager: &UpdateManager,
client: &Client,
o: &mut impl NitroOutput,
) -> anyhow::Result<ClientMeta> {
let version_string = version.to_owned();
let entry = version_manifest
.versions
.iter()
.find(|x| x.id == version_string);
let Some(entry) = entry else {
bail!("Minecraft version does not exist or was not found in the manifest");
};
let path = get_path(&version_string, paths);
create_leading_dirs(&path)?;
let meta = if manager.update_depth < UpdateDepth::Full && path.exists() {
json_from_file(path).context("Failed to read client meta contents from file")?
} else {
let mut download = ProgressiveDownload::bytes(&entry.url, client).await?;
while !download.is_finished() {
download.poll_download().await?;
o.display(MessageContents::Associated(
Box::new(download.get_progress()),
Box::new(MessageContents::Simple(translate!(
o,
DownloadingClientMeta
))),
));
}
let mut bytes = download.finish();
if entry.is_zipped {
let mut zip =
ZipArchive::new(Cursor::new(&bytes)).context("Failed to open zip archive")?;
if !zip.is_empty() {
let mut out = None;
for i in 0..zip.len() {
let mut file = zip.by_index(i).expect("Index should exist");
if file.is_file() {
let mut buf = Vec::with_capacity(
file.size().try_into().expect("Stop using 32 pointer width"),
);
file.read_to_end(&mut buf)
.context("Failed to read zip file")?;
out = Some(buf);
}
}
if let Some(out) = out {
bytes = out;
} else {
bail!("No files found for use in zip file");
}
} else {
bail!("Zipped client meta has no files inside")
}
}
std::fs::write(path, &bytes).context("Failed to write client meta to a file")?;
simd_json::from_slice(&mut bytes).context("Failed to parse client meta")?
};
Ok(meta)
}
pub fn get_path(version: &str, paths: &Paths) -> PathBuf {
paths
.internal
.join("versions")
.join(format!("{version}.json"))
}