use crate::data_source;
use crate::error::McDataError;
use crate::loader::load_data_from_path;
use crate::structs::ProtocolVersionInfo;
use once_cell::sync::OnceCell;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Edition {
Pc,
Bedrock,
}
impl Edition {
pub fn path_prefix(&self) -> &'static str {
match self {
Edition::Pc => "pc",
Edition::Bedrock => "bedrock",
}
}
}
impl std::fmt::Display for Edition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path_prefix())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Version {
pub minecraft_version: String,
pub major_version: String,
pub version: i32,
pub data_version: i32,
pub edition: Edition,
pub release_type: String,
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.edition != other.edition {
None } else {
self.data_version.partial_cmp(&other.data_version)
}
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap_or_else(|| {
log::warn!(
"Comparing Version structs from different editions ({:?} vs {:?})",
self.edition,
other.edition
);
Ordering::Equal
})
}
}
static LOADED_VERSIONS: OnceCell<HashMap<Edition, Result<Arc<VersionData>, McDataError>>> =
OnceCell::new();
#[derive(Debug, Clone)]
pub struct VersionData {
pub by_minecraft_version: HashMap<String, Version>,
pub by_major_version: HashMap<String, Vec<Version>>,
pub by_protocol_version: HashMap<i32, Vec<Version>>,
}
fn load_and_index_versions(edition: Edition) -> Result<Arc<VersionData>, McDataError> {
log::debug!(
"Attempting to load protocolVersions.json for {:?}...",
edition
);
let data_root = data_source::get_data_root()?;
let path_str = format!("{}/common/protocolVersions.json", edition.path_prefix());
let path = data_root.join(path_str);
let mut raw_versions: Vec<ProtocolVersionInfo> = load_data_from_path(&path)?;
raw_versions.sort_by(|a, b| b.version.cmp(&a.version));
for (i, v) in raw_versions.iter_mut().enumerate() {
if v.data_version.is_none() {
v.data_version = Some(-(i as i32));
log::trace!(
"Assigned synthetic data_version {} to {}",
v.data_version.unwrap(),
v.minecraft_version
);
}
}
let mut by_mc_ver = HashMap::new();
let mut by_major = HashMap::<String, Vec<Version>>::new();
let mut by_proto = HashMap::<i32, Vec<Version>>::new();
for raw in raw_versions {
let data_version = raw.data_version.ok_or_else(|| {
McDataError::Internal(format!("Missing dataVersion for {}", raw.minecraft_version))
})?;
let v = Version {
minecraft_version: raw.minecraft_version.clone(),
major_version: raw.major_version.clone(),
version: raw.version,
data_version,
edition,
release_type: raw.release_type.clone(),
};
by_mc_ver.insert(raw.minecraft_version.clone(), v.clone());
by_mc_ver
.entry(raw.major_version.clone())
.and_modify(|existing| {
if (v.data_version > existing.data_version && v.release_type == "release")
|| (v.data_version > existing.data_version
&& existing.release_type != "release")
{
*existing = v.clone();
}
})
.or_insert_with(|| v.clone());
by_major
.entry(raw.major_version)
.or_default()
.push(v.clone());
by_proto.entry(raw.version).or_default().push(v);
}
for versions in by_major.values_mut() {
versions.sort_unstable_by(|a, b| b.cmp(a));
}
for versions in by_proto.values_mut() {
versions.sort_unstable_by(|a, b| b.cmp(a));
}
Ok(Arc::new(VersionData {
by_minecraft_version: by_mc_ver,
by_major_version: by_major,
by_protocol_version: by_proto,
}))
}
pub fn get_version_data(edition: Edition) -> Result<Arc<VersionData>, McDataError> {
let cache = LOADED_VERSIONS.get_or_init(|| {
log::debug!("Initializing version cache map");
let mut map = HashMap::new();
map.insert(Edition::Pc, load_and_index_versions(Edition::Pc));
map.insert(Edition::Bedrock, load_and_index_versions(Edition::Bedrock));
map
});
match cache.get(&edition) {
Some(Ok(arc_data)) => Ok(arc_data.clone()),
Some(Err(original_error)) => Err(McDataError::Internal(format!(
"Failed to load protocol versions for {:?} during initialization: {}",
edition, original_error
))),
None => Err(McDataError::Internal(format!(
"Version data for edition {:?} unexpectedly missing from cache.",
edition
))),
}
}
pub fn resolve_version(version_str: &str) -> Result<Version, McDataError> {
log::debug!("Resolving version string: '{}'", version_str);
let (edition, version_part) = parse_version_string(version_str)?;
let version_data = get_version_data(edition)?;
if let Some(version) = version_data.by_minecraft_version.get(version_part) {
if version.minecraft_version == version_part || version.major_version == version_part {
log::trace!(
"Resolved '{}' via direct/major Minecraft version lookup to {}",
version_str,
version.minecraft_version
);
return Ok(version.clone());
}
}
if let Ok(protocol_num) = version_part.parse::<i32>() {
if let Some(versions) = version_data.by_protocol_version.get(&protocol_num) {
if let Some(best_match) = versions
.iter()
.find(|v| v.release_type == "release") .or_else(|| versions.first())
{
log::trace!(
"Resolved '{}' via protocol number {} lookup to {}",
version_str,
protocol_num,
best_match.minecraft_version
);
return Ok(best_match.clone());
}
}
}
if let Some(versions) = version_data.by_major_version.get(version_part) {
if let Some(newest) = versions.first() {
log::trace!(
"Resolved '{}' via by_major_version map (newest is {})",
version_str,
newest.minecraft_version
);
return Ok(newest.clone());
}
}
log::warn!(
"Failed to resolve version string '{}' for edition {:?}",
version_str,
edition
);
Err(McDataError::InvalidVersion(version_str.to_string()))
}
fn parse_version_string(version_str: &str) -> Result<(Edition, &str), McDataError> {
if let Some(stripped) = version_str.strip_prefix("pc_") {
Ok((Edition::Pc, stripped))
} else if let Some(stripped) = version_str.strip_prefix("bedrock_") {
Ok((Edition::Bedrock, stripped))
} else {
log::trace!(
"Assuming PC edition for version string '{}' (no prefix found)",
version_str
);
Ok((Edition::Pc, version_str))
}
}
pub fn get_supported_versions(edition: Edition) -> Result<Vec<String>, McDataError> {
let version_data = get_version_data(edition)?;
let mut versions: Vec<_> = version_data
.by_minecraft_version
.values()
.filter(|v| v.minecraft_version.contains('.'))
.map(|v| v.minecraft_version.clone())
.collect();
versions.sort_by(|a, b| {
let parts_a: Vec<Option<u32>> = a.split('.').map(|s| s.parse().ok()).collect();
let parts_b: Vec<Option<u32>> = b.split('.').map(|s| s.parse().ok()).collect();
let len = std::cmp::max(parts_a.len(), parts_b.len());
for i in 0..len {
let val_a = parts_a.get(i).cloned().flatten().unwrap_or(0);
let val_b = parts_b.get(i).cloned().flatten().unwrap_or(0);
match val_a.cmp(&val_b) {
Ordering::Equal => continue, other => return other, }
}
Ordering::Equal });
Ok(versions)
}