use log;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
mod cached_data;
mod data_source;
mod error;
mod features;
mod indexer;
mod loader;
mod paths;
mod structs;
mod version;
pub use cached_data::IndexedData;
pub use error::{Edition, McDataError};
pub use structs::*;
pub use version::Version;
static DATA_CACHE: Lazy<RwLock<HashMap<String, Arc<IndexedData>>>> = Lazy::new(Default::default);
pub fn mc_data(version_str: &str) -> Result<Arc<IndexedData>, McDataError> {
let version = version::resolve_version(version_str)?;
let cache_key = format!(
"{}_{}",
version.edition.path_prefix(),
version.minecraft_version
);
log::debug!("Requesting data for resolved version key: {}", cache_key);
{
let cache = DATA_CACHE
.read()
.map_err(|_| McDataError::Internal("Data cache read lock poisoned".to_string()))?;
if let Some(data) = cache.get(&cache_key) {
log::info!("Cache hit for version: {}", cache_key);
return Ok(data.clone()); }
}
log::info!("Cache miss for version: {}. Loading...", cache_key);
let loaded_data_result = IndexedData::load(version);
let loaded_data = match loaded_data_result {
Ok(data) => Arc::new(data),
Err(e) => {
log::error!("Failed to load data for {}: {}", cache_key, e);
return Err(e); }
};
{
let mut cache = DATA_CACHE
.write()
.map_err(|_| McDataError::Internal("Data cache write lock poisoned".to_string()))?;
if let Some(data) = cache.get(&cache_key) {
log::info!("Cache hit after load race for version: {}", cache_key);
return Ok(data.clone()); }
log::info!(
"Inserting loaded data into cache for version: {}",
cache_key
);
cache.insert(cache_key.clone(), loaded_data.clone());
}
Ok(loaded_data)
}
pub fn supported_versions(edition: Edition) -> Result<Vec<String>, McDataError> {
version::get_supported_versions(edition)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn setup() {
let _ = env_logger::builder().is_test(true).try_init();
}
fn get_test_cache_dir() -> Option<PathBuf> {
dirs_next::cache_dir().map(|p| p.join("mcdata-rs").join("minecraft-data"))
}
#[allow(dead_code)]
fn clear_test_cache() {
if let Some(cache_dir) = get_test_cache_dir() {
if cache_dir.exists() {
log::warn!("Clearing test cache directory: {}", cache_dir.display());
if let Err(e) = std::fs::remove_dir_all(&cache_dir) {
log::error!("Failed to clear test cache: {}", e);
}
}
}
}
#[test]
fn load_pc_1_18_2() {
setup();
let data = mc_data("1.18.2").expect("Failed to load 1.18.2 data");
assert_eq!(data.version.minecraft_version, "1.18.2");
assert_eq!(data.version.edition, Edition::Pc);
let stone = data
.blocks_by_name
.get("stone")
.expect("Stone block not found");
assert_eq!(stone.id, 1);
assert!(
data.items_by_name.contains_key("stick"),
"Stick item not found by name"
);
assert!(!data.biomes_array.is_empty(), "Biomes empty");
assert!(!data.entities_array.is_empty(), "Entities empty");
assert!(
data.block_collision_shapes_raw.is_some(),
"Collision shapes missing"
);
assert!(
!data.block_shapes_by_name.is_empty(),
"Indexed shapes empty"
);
}
#[test]
fn load_pc_major_version() {
setup();
let data = mc_data("1.19").expect("Failed to load 1.19 data");
assert!(data.version.minecraft_version.starts_with("1.19"));
assert_eq!(data.version.edition, Edition::Pc);
assert!(data.blocks_by_name.contains_key("mangrove_log"));
assert!(data.entities_by_name.contains_key("warden"));
}
#[test]
fn test_version_comparison() {
setup();
let data_1_18 = mc_data("1.18.2").unwrap();
let data_1_16 = mc_data("1.16.5").unwrap();
let data_1_20 = mc_data("1.20.1").unwrap();
assert!(data_1_18.is_newer_or_equal_to("1.16.5").unwrap());
assert!(data_1_18.is_newer_or_equal_to("1.18.2").unwrap());
assert!(!data_1_18.is_newer_or_equal_to("1.20.1").unwrap());
assert!(data_1_20.is_newer_or_equal_to("1.18.2").unwrap());
assert!(data_1_16.is_older_than("1.18.2").unwrap());
assert!(!data_1_16.is_older_than("1.16.5").unwrap());
assert!(!data_1_16.is_older_than("1.15.2").unwrap()); assert!(data_1_18.is_older_than("1.20.1").unwrap());
}
#[test]
fn test_feature_support() {
setup();
let data_1_18 = mc_data("1.18.2").unwrap();
let data_1_15 = mc_data("1.15.2").unwrap();
let dim_int_115 = data_1_15.support_feature("dimensionIsAnInt").unwrap();
assert_eq!(dim_int_115, serde_json::Value::Bool(true));
let dim_int_118 = data_1_18.support_feature("dimensionIsAnInt").unwrap();
assert_eq!(dim_int_118, serde_json::Value::Bool(false));
let meta_ix_118 = data_1_18.support_feature("metadataIxOfItem").unwrap();
assert_eq!(meta_ix_118, serde_json::Value::Number(8.into()));
let meta_ix_115 = data_1_15.support_feature("metadataIxOfItem").unwrap();
assert_eq!(meta_ix_115, serde_json::Value::Number(7.into()));
}
#[test]
fn test_cache() {
setup();
let version = "1.17.1";
log::info!("CACHE TEST: Loading {} for the first time", version);
let data1 = mc_data(version).expect("Load 1 failed");
log::info!("CACHE TEST: Loading {} for the second time", version);
let data2 = mc_data(version).expect("Load 2 failed");
assert!(
Arc::ptr_eq(&data1, &data2),
"Cache miss: Arcs point to different data for {}",
version
);
let prefixed_version = format!("pc_{}", version);
log::info!(
"CACHE TEST: Loading {} for the third time",
prefixed_version
);
let data3 = mc_data(&prefixed_version).expect("Load 3 failed");
assert!(
Arc::ptr_eq(&data1, &data3),
"Cache miss: Prefixed version {} loaded different data",
prefixed_version
);
}
#[test]
fn test_supported_versions() {
setup();
let versions =
supported_versions(Edition::Pc).expect("Failed to get supported PC versions");
assert!(!versions.is_empty());
assert!(versions.iter().any(|v| v == "1.8.8"));
assert!(versions.iter().any(|v| v == "1.16.5"));
assert!(versions.iter().any(|v| v == "1.18.2"));
assert!(versions.iter().any(|v| v == "1.20.1"));
let index_1_8 = versions.iter().position(|v| v == "1.8.8");
let index_1_16 = versions.iter().position(|v| v == "1.16.5");
assert!(index_1_8.is_some());
assert!(index_1_16.is_some());
assert!(
index_1_8 < index_1_16,
"Versions should be sorted oldest to newest"
);
}
#[test]
fn test_invalid_version() {
setup();
let result = mc_data("invalid_version_string_1.2.3");
assert!(result.is_err());
match result.err().unwrap() {
McDataError::InvalidVersion(s) => assert!(s.contains("invalid_version")),
e => panic!("Expected InvalidVersion error, got {:?}", e),
}
}
}