use crate::lock::Lock;
use crate::yml::{ModEntry, Snap};
use crate::{AvailableVersion, ResolveEnv};
use anyhow::Result;
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct ModCompatReport {
pub mod_id: String,
pub current_version: String,
pub status: CompatStatus,
}
#[derive(Debug, Clone)]
pub enum CompatStatus {
Compatible { suggested_version: String },
Incompatible,
Unknown,
Error(String),
}
#[derive(Debug, Clone)]
pub struct NewerVersionsReport {
pub mod_id: String,
pub current_version: String,
pub newer: Vec<String>,
}
fn mod_id(entry: &ModEntry) -> String {
match entry {
ModEntry::Registry { id, .. } => id.clone(),
ModEntry::Url { url, .. } => url.clone(),
}
}
fn current_version(entry: &ModEntry) -> String {
match entry {
ModEntry::Registry { version, .. } => version.clone(),
ModEntry::Url { .. } => "url".to_string(),
}
}
pub async fn check_against(snap: &Snap, target_mc: &str) -> Result<Vec<ModCompatReport>> {
let env = ResolveEnv {
minecraft: target_mc.to_string(),
loader_kind: snap.server.loader.kind.clone(),
loader_version: snap.server.loader.version.clone(),
};
let mut out = Vec::with_capacity(snap.mods.len());
for entry in &snap.mods {
let id = mod_id(entry);
let cur = current_version(entry);
let status = match entry {
ModEntry::Url { .. } => CompatStatus::Unknown,
ModEntry::Registry { .. } => {
match crate::providers::list_versions_for_entry(entry, &env).await {
Ok(versions) => {
let chosen = versions.into_iter().find(|v| {
v.game_versions.iter().any(|g| g == target_mc)
&& v.loaders.iter().any(|l| l == &env.loader_kind)
});
match chosen {
Some(v) => CompatStatus::Compatible {
suggested_version: v.version_number,
},
None => CompatStatus::Incompatible,
}
}
Err(e) => CompatStatus::Error(e.to_string()),
}
}
};
out.push(ModCompatReport {
mod_id: id,
current_version: cur,
status,
});
}
Ok(out)
}
pub async fn collect_supported_mc_versions(snap: &Snap) -> Result<Vec<(String, HashSet<String>)>> {
let env = ResolveEnv {
minecraft: String::new(),
loader_kind: snap.server.loader.kind.clone(),
loader_version: snap.server.loader.version.clone(),
};
let mut out = Vec::new();
for entry in &snap.mods {
if matches!(entry, ModEntry::Url { .. }) {
continue;
}
let id = mod_id(entry);
let versions = crate::providers::list_versions_for_entry(entry, &env).await?;
let supported: HashSet<String> = versions
.into_iter()
.filter(|v| v.loaders.iter().any(|l| l == &env.loader_kind))
.flat_map(|v| v.game_versions.into_iter())
.collect();
out.push((id, supported));
}
Ok(out)
}
pub fn newest_common_mc(per_mod: &[(String, HashSet<String>)], current_mc: &str) -> Option<String> {
if per_mod.is_empty() {
return None;
}
let mut intersection: HashSet<String> = per_mod[0].1.clone();
for (_, set) in &per_mod[1..] {
intersection.retain(|v| set.contains(v));
}
let mut candidates: Vec<String> = intersection.into_iter().collect();
candidates.sort_by(|a, b| compare_mc_version(b, a));
candidates
.into_iter()
.find(|v| compare_mc_version(v, current_mc) == std::cmp::Ordering::Greater)
}
pub async fn find_newer_versions(snap: &Snap, lock: &Lock) -> Result<Vec<NewerVersionsReport>> {
let env = ResolveEnv {
minecraft: snap.server.minecraft.clone(),
loader_kind: snap.server.loader.kind.clone(),
loader_version: snap.server.loader.version.clone(),
};
let mut out = Vec::new();
for entry in &snap.mods {
let ModEntry::Registry { id, .. } = entry else {
continue;
};
let locked = lock
.mods
.iter()
.find(|m| &m.id == id)
.map(|m| m.version.clone())
.unwrap_or_else(|| "?".to_string());
let versions = crate::providers::list_versions_for_entry(entry, &env).await?;
let newer = collect_newer(&versions, &locked);
out.push(NewerVersionsReport {
mod_id: id.clone(),
current_version: locked,
newer,
});
}
Ok(out)
}
fn collect_newer(versions: &[AvailableVersion], locked: &str) -> Vec<String> {
let mut out = Vec::new();
for v in versions {
if v.version_number == locked {
break;
}
out.push(v.version_number.clone());
}
out
}
pub fn compare_mc_version(a: &str, b: &str) -> std::cmp::Ordering {
use std::cmp::Ordering;
let (core_a, pre_a) = split_pre_release(a);
let (core_b, pre_b) = split_pre_release(b);
let parts_a: Vec<&str> = core_a.split('.').collect();
let parts_b: Vec<&str> = core_b.split('.').collect();
let n = parts_a.len().max(parts_b.len());
for i in 0..n {
let pa = parts_a.get(i).copied().unwrap_or("0");
let pb = parts_b.get(i).copied().unwrap_or("0");
match (pa.parse::<u64>(), pb.parse::<u64>()) {
(Ok(x), Ok(y)) => match x.cmp(&y) {
Ordering::Equal => continue,
o => return o,
},
_ => match pa.cmp(pb) {
Ordering::Equal => continue,
o => return o,
},
}
}
match (pre_a, pre_b) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(x), Some(y)) => x.cmp(&y),
}
}
fn split_pre_release(s: &str) -> (&str, Option<&str>) {
if let Some(idx) = s.find('-') {
(&s[..idx], Some(&s[idx + 1..]))
} else {
(s, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compares_versions_numerically() {
assert_eq!(
compare_mc_version("26.1.10", "26.1.2"),
std::cmp::Ordering::Greater
);
assert_eq!(
compare_mc_version("26.1.2", "26.1.2"),
std::cmp::Ordering::Equal
);
assert_eq!(
compare_mc_version("26.1", "26.1.2"),
std::cmp::Ordering::Less
);
}
#[test]
fn release_outranks_pre_release() {
assert_eq!(
compare_mc_version("26.1.0", "26.1.0-rc.1"),
std::cmp::Ordering::Greater
);
assert_eq!(
compare_mc_version("26.1.0-rc.1", "26.1.0"),
std::cmp::Ordering::Less
);
assert_eq!(
compare_mc_version("26.1.0-rc.1", "26.1.0-rc.2"),
std::cmp::Ordering::Less
);
}
#[test]
fn newest_common_picks_highest_in_intersection() {
let a: HashSet<String> = ["26.1.2", "26.1.3", "26.1.4"]
.iter()
.map(|s| s.to_string())
.collect();
let b: HashSet<String> = ["26.1.2", "26.1.3"].iter().map(|s| s.to_string()).collect();
let r = newest_common_mc(&[("a".into(), a), ("b".into(), b)], "26.1.2");
assert_eq!(r, Some("26.1.3".into()));
}
#[test]
fn collect_newer_stops_at_locked() {
let mk = |v: &str| AvailableVersion {
version_number: v.into(),
game_versions: vec![],
loaders: vec![],
date_published: None,
};
let versions = vec![mk("0.140.0"), mk("0.139.0"), mk("0.138.0")];
let newer = collect_newer(&versions, "0.139.0");
assert_eq!(newer, vec!["0.140.0"]);
}
}