use std::io::{Cursor, Read};
use anyhow::{bail, Context};
use mcvm_shared::output::{MCVMOutput, MessageContents, MessageLevel};
use mcvm_shared::util::DeserListOrSingle;
use mcvm_shared::{translate, UpdateDepth};
use reqwest::Client;
use serde::Deserialize;
use zip::ZipArchive;
use crate::io::files::{self, paths::Paths};
use crate::io::java::JavaMajorVersion;
use crate::io::json_from_file;
use crate::io::update::UpdateManager;
use crate::net::download::ProgressiveDownload;
use super::version_manifest::VersionManifest;
#[derive(Deserialize, Debug, Clone)]
pub struct ClientMeta {
#[serde(alias = "minecraftArguments")]
pub arguments: args::Arguments,
#[serde(rename = "assetIndex")]
pub asset_index: AssetIndexInfo,
#[serde(rename = "assets")]
pub assets_version: String,
pub downloads: Downloads,
#[serde(rename = "javaVersion")]
pub java_info: JavaInfo,
pub libraries: Vec<libraries::Library>,
#[serde(rename = "mainClass")]
pub main_class: String,
pub logging: 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,
}
#[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(alias = "osx")]
MacOS,
Linux,
}
impl Display for OSName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Windows => "windows",
Self::MacOS => "macos",
Self::Linux => "linux",
}
)
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum OSArch {
X86,
X86_64,
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 MCVMOutput,
) -> 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 client_meta_name: String = version_string.clone() + ".json";
let version_dir = paths.internal.join("versions").join(version_string);
files::create_dir(&version_dir).context("Failed to create versions directory")?;
let path = version_dir.join(client_meta_name);
let meta = if manager.update_depth < UpdateDepth::Force && 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
))),
),
MessageLevel::Important,
);
}
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)
}