use crate::data_source;
use crate::error::{Edition, McDataError};
use crate::loader::load_data_from_path;
use crate::structs::Feature;
use crate::version::{self, Version};
use once_cell::sync::{Lazy, OnceCell};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
static LOADED_FEATURES: OnceCell<HashMap<Edition, Result<Arc<Vec<Feature>>, McDataError>>> =
OnceCell::new();
fn load_features_for_edition(edition: Edition) -> Result<Arc<Vec<Feature>>, McDataError> {
log::debug!("Attempting to load features.json for {:?}...", edition);
let data_root = data_source::get_data_root()?;
let path_str = format!("{}/common/features.json", edition.path_prefix());
let path = data_root.join(path_str);
load_data_from_path(&path).map(Arc::new)
}
fn get_features(edition: Edition) -> Result<Arc<Vec<Feature>>, McDataError> {
let cache = LOADED_FEATURES.get_or_init(|| {
log::debug!("Initializing features cache map");
let mut map = HashMap::new();
map.insert(Edition::Pc, load_features_for_edition(Edition::Pc));
map.insert(
Edition::Bedrock,
load_features_for_edition(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 features.json for {:?} during initialization: {}",
edition, original_error
))),
None => Err(McDataError::Internal(format!(
"Features data for edition {:?} unexpectedly missing from cache.",
edition
))),
}
}
static RESOLVED_VERSION_CACHE: Lazy<RwLock<HashMap<(Edition, String), Version>>> =
Lazy::new(Default::default);
fn resolve_cached_version(edition: Edition, version_str: &str) -> Result<Version, McDataError> {
let cache_key = (edition, version_str.to_string());
{
let cache = RESOLVED_VERSION_CACHE
.read()
.map_err(|_| McDataError::Internal("Version cache read lock poisoned".to_string()))?;
if let Some(cached_version) = cache.get(&cache_key) {
log::trace!("Cache hit for resolved version: {:?}", cache_key);
return Ok(cached_version.clone());
}
}
let resolved_result = crate::version::resolve_version(version_str);
if let Ok(ref version) = resolved_result {
if version.edition == edition {
let mut cache = RESOLVED_VERSION_CACHE.write().map_err(|_| {
McDataError::Internal("Version cache write lock poisoned".to_string())
})?;
log::trace!("Cache miss, inserting resolved version: {:?}", cache_key);
cache.entry(cache_key).or_insert_with(|| version.clone());
} else {
log::warn!(
"Resolved version {} has edition {:?}, but expected {:?}",
version_str,
version.edition,
edition
);
return Err(McDataError::Internal(format!(
"Resolved version {} edition mismatch (got {:?}, expected {:?})",
version_str, version.edition, edition
)));
}
}
resolved_result }
fn is_version_in_range(
target_version: &Version,
min_ver_str: &str,
max_ver_str: &str,
) -> Result<bool, McDataError> {
let edition = target_version.edition;
log::trace!(
"Checking if {:?} {} is in range [{}, {}]",
edition,
target_version.minecraft_version,
min_ver_str,
max_ver_str
);
let min_ver = if let Some(base_major) = min_ver_str.strip_suffix("_major") {
log::trace!("Resolving min_ver {}_major", base_major);
let version_data = version::get_version_data(edition)?;
version_data
.by_major_version
.get(base_major)
.and_then(|versions| versions.last()) .cloned()
.ok_or_else(|| {
McDataError::InvalidVersion(format!(
"Could not find oldest version for major '{}_{}'",
edition.path_prefix(),
base_major
))
})?
} else {
log::trace!("Resolving min_ver {}", min_ver_str);
resolve_cached_version(edition, min_ver_str)?
};
let max_ver = if max_ver_str == "latest" {
log::trace!("Resolving max_ver 'latest'");
let version_data = version::get_version_data(edition)?;
version_data
.by_minecraft_version .values()
.max() .cloned()
.ok_or_else(|| {
McDataError::Internal(format!(
"Could not determine latest version for {:?}",
edition
))
})?
} else if let Some(base_major) = max_ver_str.strip_suffix("_major") {
log::trace!("Resolving max_ver {}_major", base_major);
let version_data = version::get_version_data(edition)?;
version_data
.by_major_version
.get(base_major)
.and_then(|versions| versions.first()) .cloned()
.ok_or_else(|| {
McDataError::InvalidVersion(format!(
"Could not find newest version for major '{}_{}'",
edition.path_prefix(),
base_major
))
})?
} else {
log::trace!("Resolving max_ver {}", max_ver_str);
resolve_cached_version(edition, max_ver_str)?
};
let result = target_version >= &min_ver && target_version <= &max_ver;
log::trace!(
"Range check: {} >= {} && {} <= {} -> {}",
target_version.data_version,
min_ver.data_version,
target_version.data_version,
max_ver.data_version,
result
);
Ok(result)
}
pub fn get_feature_support(
target_version: &Version,
feature_name: &str,
) -> Result<Value, McDataError> {
log::debug!(
"Checking feature support for '{}' in version {}",
feature_name,
target_version.minecraft_version
);
let features = get_features(target_version.edition)?;
if let Some(feature) = features.iter().rev().find(|f| f.name == feature_name) {
log::trace!("Found feature entry: {:?}", feature);
if !feature.values.is_empty() {
log::trace!(
"Checking feature.values array ({} entries)",
feature.values.len()
);
for fv in feature.values.iter().rev() {
let in_range = if let Some(v_str) = &fv.version {
is_version_in_range(target_version, v_str, v_str)?
} else if fv.versions.len() == 2 {
is_version_in_range(target_version, &fv.versions[0], &fv.versions[1])?
} else {
log::warn!(
"Invalid version range definition in feature '{}' value: {:?}",
feature_name,
fv
);
false };
if in_range {
log::debug!(
"Feature '{}' supported via values array, value: {}",
feature_name,
fv.value
);
return Ok(fv.value.clone());
}
}
log::trace!("No matching range found in feature.values");
}
else if let Some(v_str) = &feature.version {
log::trace!("Checking feature.version string: {}", v_str);
if is_version_in_range(target_version, v_str, v_str)? {
log::debug!(
"Feature '{}' supported via version string (implicit true)",
feature_name
);
return Ok(Value::Bool(true)); }
}
else if feature.versions.len() == 2 {
log::trace!(
"Checking feature.versions array: [{}, {}]",
feature.versions[0],
feature.versions[1]
);
if is_version_in_range(target_version, &feature.versions[0], &feature.versions[1])? {
log::debug!(
"Feature '{}' supported via versions array (implicit true)",
feature_name
);
return Ok(Value::Bool(true)); }
} else {
log::trace!(
"Feature '{}' found but has no version/versions/values definition, assuming false",
feature_name
);
}
} else {
log::trace!("Feature '{}' not found in features.json", feature_name);
}
log::debug!(
"Feature '{}' determined to be unsupported (defaulting to false)",
feature_name
);
Ok(Value::Bool(false))
}