mod fmt;
use std::{collections::HashMap, iter::once, path::PathBuf, str::FromStr, time::Duration};
use anyhow::anyhow;
use creeper_maven_coord::MavenCoord;
use neoforge::NfInstallProfile;
use reqwest::Client;
use semver::Version;
use serde::{Deserialize, Serialize};
use strfmt::Format;
use tokio::process::Command;
use tracing::{debug, error, info, trace};
use walkdir::WalkDir;
use crate::{
Artifact, Checksum, Creeper, Id, Install, McVersionExt,
builtin::{SyncBuiltinIndex, UpdateIndex},
index::{Index, VersionRev},
neoforge::fmt::maven_coord_format,
pack::PackNode,
path::creeper_cache_dir,
util::JarManifest,
zip::{extract_zip, extract_zip_to},
};
fn cache_path() -> anyhow::Result<PathBuf> {
let path = creeper_cache_dir()?.join("builtin").join("neoforge");
Ok(path)
}
const VERSIONS_URL: &str =
"https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge";
pub struct NeoforgeManager {
http: Client,
}
impl NeoforgeManager {
pub fn new(http: Client) -> Self {
Self { http }
}
}
impl SyncBuiltinIndex for NeoforgeManager {
fn package(&self) -> Id {
Id::neoforge()
}
async fn sync_index(&self) -> anyhow::Result<Index> {
info!("updating NeoForge metadata");
let req = self.http.get(VERSIONS_URL).build()?;
let res = self.http.execute(req).await?;
#[derive(Clone, Debug, Serialize, Deserialize)]
struct Versions {
#[serde(rename = "isSnapshot")]
is_snapshot: bool,
versions: Vec<String>,
}
let versions = res.json::<Versions>().await?;
let count = versions.versions.len();
let versions = versions
.versions
.into_iter()
.filter_map(|s| parse_neoforge_version(&s));
let index = neoforge_index(versions);
debug!(
"retrieved {count} NeoForge versions, of which {} valid",
index.len()
);
Ok(index)
}
fn cache_expiry(&self) -> std::time::Duration {
Duration::from_hours(72)
}
}
impl Creeper {
pub async fn update_neoforge(&self) -> anyhow::Result<()> {
if self.args.offline {
info!("skipping neoforge update because offline mode enabled");
return Ok(());
}
self.neoforge.update_index().await
}
async fn neoforge_installer_jar(&self, version: &Version) -> anyhow::Result<Artifact> {
let nf_version = decode_neoforge_version(version);
let url = if self.config.use_bmclapi {
format!(
"https://bmclapi2.bangbang93.com/neoforge/version/{nf_version}/download/installer.jar"
)
} else {
format!(
"https://maven.neoforged.net/releases/net/neoforged/neoforge/{nf_version}/neoforge-{nf_version}-installer.jar"
)
};
let sha1_url = format!(
"https://maven.neoforged.net/releases/net/neoforged/neoforge/{nf_version}/neoforge-{nf_version}-installer.jar.sha1"
);
let req = self.http.get(sha1_url).build()?;
let res = self.http.execute(req).await?;
let sha1 = res.text().await?.trim().to_string();
let name = format!("neoforge-{nf_version}-installer.jar");
let installer = self
.download(name, url, None, once(Checksum::sha1(sha1)))
.await?;
Ok(installer)
}
pub(crate) async fn neoforge_install(&self, version: &Version) -> anyhow::Result<Install> {
let installer = self.neoforge_installer_jar(version).await?;
let installer = self.retrieve_artifact(&installer).await?;
let mc_version = extract_zip(&installer, "version.json").await?;
let mc_version = serde_json::from_str::<McVersionExt>(&mc_version)?;
let mut install = self.vanilla_version_install(mc_version).await?;
let tmp_dir = cache_path()?.join("tmp").join(version.to_string());
let tmp_lib_dir = tmp_dir.join("lib");
let install_profile = extract_zip(&installer, "install_profile.json").await?;
let install_profile = serde_json::from_str::<NfInstallProfile>(&install_profile)?;
let mut java_lib_file = HashMap::new();
java_lib_file.extend(self.vanilla_lib(install_profile.libraries).await?);
info!("preparing neoforge install environment");
for (path, art) in &java_lib_file {
let path = tmp_lib_dir.join(path);
self.retrieve_artifact_to(art, path).await?;
}
let vanilla_install = {
let version = nf_required_mc_version(version);
if let Some(install) = self
.get_install_cache(&Id::vanilla(), &version.clone().into())
.await?
{
install
} else {
let install = self.vanilla_install(&version).await?;
self.set_install_cache(&Id::vanilla(), &version.into(), Some(&install))
.await?;
install
}
};
let mc_jar = vanilla_install
.mc_jar
.ok_or(anyhow!("missing minecraft jar in vanilla install"))?;
let mc_jar = self.retrieve_artifact(&mc_jar).await?;
let mut vars = install_profile
.data
.into_iter()
.map(|(k, v)| (k, v.client))
.chain(once(("SIDE".into(), "client".into())))
.chain(once(("MINECRAFT_JAR".into(), mc_jar.display().to_string())))
.collect::<HashMap<_, _>>();
let binpatch = tmp_dir.join("installer").join("data").join("client.lzma");
extract_zip_to(&installer, "data/client.lzma", &binpatch).await?;
vars.insert("BINPATCH".into(), binpatch.display().to_string());
info!("running neoforge install processors");
for proc in install_profile.processors {
if !proc
.sides
.as_ref()
.is_none_or(|x| x.contains(&"client".into()))
{
debug!("skipping a processor because side mismatch: {proc}");
continue;
}
info!("running processor: {proc}");
let jar = java_lib_file
.get(&proc.jar.parse::<MavenCoord>()?.path())
.ok_or(anyhow!(
"processor runs {} but the jar file not found",
proc.jar
))?;
let jar = self.retrieve_artifact(jar).await?;
let manifest = extract_zip(&jar, "META-INF/MANIFEST.MF")
.await?
.parse::<JarManifest>()?;
let main_class = manifest
.main_class
.ok_or(anyhow!("processor missing java main class"))?;
let mut cp = vec![jar.display().to_string()];
for c in proc.classpath {
let coord = c.parse::<MavenCoord>()?;
let jar = java_lib_file.get(&coord.path()).ok_or(anyhow!(
"processor classpath {} not found in java libraries",
c
))?;
let jar = self.retrieve_artifact(jar).await?;
cp.push(jar.display().to_string());
}
let mut cmd = Command::new("java");
cmd.arg("--class-path").arg(cp.join(":"));
cmd.arg(main_class);
for arg in proc.args {
let arg = arg.format(&vars)?;
let arg = maven_coord_format(&arg, &tmp_lib_dir)?;
cmd.arg(arg);
}
debug!("running command {cmd:?}");
let mut proc = cmd.spawn()?;
let exit = proc.wait().await;
if let Err(e) = exit {
error!("a processor failed: {e}");
}
}
info!("collecting neoforge install result");
for i in WalkDir::new(&tmp_lib_dir) {
let entry = i?;
let file = entry.path();
let relative = file.strip_prefix(&tmp_lib_dir).unwrap();
if file.is_dir() {
continue;
}
if java_lib_file.contains_key(relative)
|| install.java_lib_class.contains_key(relative)
|| install.java_lib_mod.contains_key(relative)
|| install.java_lib_file.contains_key(relative)
|| vanilla_install.java_lib_class.contains_key(relative)
|| vanilla_install.java_lib_mod.contains_key(relative)
|| vanilla_install.java_lib_file.contains_key(relative)
{
continue;
}
trace!("found file {}", file.display());
let art = self.store_artifact(file).await?;
java_lib_file.insert(relative.to_path_buf(), art);
}
install.extend(once(Install {
java_lib_file,
..Default::default()
}));
install.simplify();
install.disable_mc_jar = true;
Ok(install)
}
}
pub fn parse_neoforge_version(version: &str) -> Option<Version> {
if let Ok(version) = version.parse() {
return Some(version);
}
let (major, rest) = version.split_once('.')?;
let rest = Version::from_str(rest).ok()?;
let minor = rest.major;
let (high, low) = (rest.minor, rest.patch);
if high > u32::MAX as u64 || low > u32::MAX as u64 {
return None;
}
let patch = (high << 32) | low;
let mut version = rest.clone();
version.major = major.parse().ok()?;
version.minor = minor;
version.patch = patch;
Some(version)
}
pub fn decode_neoforge_version(version: &Version) -> String {
if version.major < 26 {
return version.to_string();
}
let high = version.patch >> 32;
let low = version.patch & 0xFFFFFFFF;
let pre = if version.pre.is_empty() {
"".to_string()
} else {
format!("-{}", version.pre)
};
let build = if version.build.is_empty() {
"".to_string()
} else {
format!("+{}", version.build)
};
let version = format!("{}.{}.{}.{}", version.major, version.minor, high, low);
let version = format!("{}{}{}", version, pre, build);
version
}
fn nf_required_mc_version(version: &Version) -> Version {
if version.major >= 26 {
let high = version.patch >> 32;
Version::new(version.major, version.minor, high)
} else {
Version::new(1, version.major, version.minor)
}
}
fn neoforge_index(versions: impl IntoIterator<Item = Version>) -> Index {
versions
.into_iter()
.map(|version| {
let req = nf_required_mc_version(&version);
let req = format!("={}", req).parse().unwrap();
let dep = Some((Id::vanilla(), req)).into_iter().collect();
let node = PackNode {
dep,
..Default::default()
};
(VersionRev::new(version), node)
})
.collect()
}