use std::{collections::HashMap, iter::once, path::PathBuf};
use semver::Version;
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
use tokio::fs::{create_dir_all, read_to_string, remove_file, try_exists, write};
use tracing::debug;
use crate::{Artifact, Creeper, Id, Package, VersionRev, path::creeper_cache_dir};
#[serde_inline_default]
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Install {
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub java_lib_class: HashMap<PathBuf, Artifact>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub java_lib_mod: HashMap<PathBuf, Artifact>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub java_lib_file: HashMap<PathBuf, Artifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub java_agent: Vec<(Artifact, Option<String>)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub java_main_class: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub native: HashMap<PathBuf, Artifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub java_flag: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mc_jar: Option<Artifact>,
#[serde_inline_default(false)]
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub disable_mc_jar: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mc_flag: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub mc_asset: HashMap<PathBuf, Artifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mc_mod: Vec<Artifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resource_pack: Vec<Artifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shader_pack: Vec<Artifact>,
}
impl Install {
pub fn merge(self, next: Self) -> Self {
let mut new = self;
new.extend(Some(next));
new
}
pub fn simplify(&mut self) {
self.java_lib_file.retain(|k, _v| {
!self.java_lib_class.contains_key(k) && !self.java_lib_mod.contains_key(k)
});
}
}
impl Extend<Self> for Install {
fn extend<T: IntoIterator<Item = Self>>(&mut self, iter: T) {
for next in iter {
let Self {
java_lib_class,
java_lib_mod,
java_lib_file,
java_agent,
java_main_class,
native,
java_flag,
mc_jar,
disable_mc_jar,
mc_flag,
mc_asset,
mc_mod,
resource_pack,
shader_pack,
} = next;
self.java_lib_class.extend(java_lib_class);
self.java_lib_mod.extend(java_lib_mod);
self.java_lib_file.extend(java_lib_file);
self.java_agent.extend(java_agent);
self.java_main_class = java_main_class.or(self.java_main_class.take());
self.native.extend(native);
self.java_flag.extend(java_flag);
self.mc_jar = mc_jar.or(self.mc_jar.take());
self.disable_mc_jar = self.disable_mc_jar || disable_mc_jar;
self.mc_flag.extend(mc_flag);
self.mc_asset.extend(mc_asset);
self.mc_mod.extend(mc_mod);
self.resource_pack.extend(resource_pack);
self.shader_pack.extend(shader_pack);
}
self.simplify();
}
}
impl Creeper {
fn install_cache_path(&self, package: &Id, version: &VersionRev) -> anyhow::Result<PathBuf> {
let path = creeper_cache_dir()?
.join("install")
.join(package.indexed_path())
.join(version.to_string())
.with_added_extension("json");
Ok(path)
}
pub(crate) async fn get_install_cache(
&self,
package: &Id,
version: &VersionRev,
) -> anyhow::Result<Option<Install>> {
let cache = self.install_cache_path(package, version)?;
if !try_exists(&cache).await? {
return Ok(None);
}
let json = read_to_string(&cache).await?;
let install = serde_json::from_str(&json)?;
Ok(Some(install))
}
pub(crate) async fn set_install_cache(
&self,
package: &Id,
version: &VersionRev,
value: Option<&Install>,
) -> anyhow::Result<()> {
let cache = self.install_cache_path(package, version)?;
let install = if let Some(x) = value {
x
} else {
if try_exists(&cache).await? {
remove_file(&cache).await?;
}
return Ok(());
};
let json = serde_json::to_string(install)?;
create_dir_all(cache.parent().unwrap()).await?;
write(&cache, json).await?;
Ok(())
}
pub async fn install(
&self,
package: &Id,
version: &Version,
rev: u32,
) -> anyhow::Result<Install> {
if let Some(install) = self
.get_install_cache(package, &VersionRev::with_rev(version.clone(), rev))
.await?
{
debug!("using cached install {package}@{version}");
return Ok(install);
}
let install = if !package.is_regular() {
match package.as_str() {
"vanilla" => self.vanilla_install(version).await?,
"neoforge" => self.neoforge_install(version).await?,
"fabric" => self.fabric_install(version).await?,
"intermediary" => self.intermediary_install(version).await?,
_ => todo!(),
}
} else {
let package = self.query_registry(package, version, rev).await?;
package.install
};
self.set_install_cache(
package,
&VersionRev::with_rev(version.clone(), rev),
Some(&install),
)
.await?;
Ok(install)
}
pub async fn install_all(
&self,
packages: impl IntoIterator<Item = (Id, VersionRev)>,
) -> anyhow::Result<Install> {
let mut install = Install::default();
for (id, version) in packages {
let package = self.install(&id, &version.version, version.rev).await?;
install.extend(once(package));
}
Ok(install)
}
pub async fn recursive_install(&self, package: Package) -> anyhow::Result<Install> {
let dep = self.resolve(package.node.dep)?;
let sorted = self.sort_dependency(dep)?;
let mut install = Install::default();
for (id, version) in sorted {
let package = self.install(&id, &version.version, version.rev).await?;
install.extend(once(package));
}
install.extend(once(package.install));
Ok(install)
}
}