1use log;
2use once_cell::sync::Lazy;
3use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6mod cached_data;
8mod data_source;
9mod error;
10mod features;
11mod indexer;
12mod loader;
13mod paths;
14mod structs;
15mod version;
16
17pub use cached_data::IndexedData;
19pub use error::{Edition, McDataError};
20pub use structs::*;
21pub use version::Version; static DATA_CACHE: Lazy<RwLock<HashMap<String, Arc<IndexedData>>>> = Lazy::new(Default::default);
26
27pub fn mc_data(version_str: &str) -> Result<Arc<IndexedData>, McDataError> {
44 let version = version::resolve_version(version_str)?;
47 let cache_key = format!(
48 "{}_{}",
49 version.edition.path_prefix(),
50 version.minecraft_version
51 );
52 log::debug!("Requesting data for resolved version key: {}", cache_key);
53
54 {
56 let cache = DATA_CACHE
57 .read()
58 .map_err(|_| McDataError::Internal("Data cache read lock poisoned".to_string()))?;
59 if let Some(data) = cache.get(&cache_key) {
60 log::info!("Cache hit for version: {}", cache_key);
61 return Ok(data.clone()); }
63 } log::info!("Cache miss for version: {}. Loading...", cache_key);
69 let loaded_data_result = IndexedData::load(version); let loaded_data = match loaded_data_result {
73 Ok(data) => Arc::new(data),
74 Err(e) => {
75 log::error!("Failed to load data for {}: {}", cache_key, e);
76 return Err(e); }
78 };
79
80 {
82 let mut cache = DATA_CACHE
83 .write()
84 .map_err(|_| McDataError::Internal("Data cache write lock poisoned".to_string()))?;
85 if let Some(data) = cache.get(&cache_key) {
88 log::info!("Cache hit after load race for version: {}", cache_key);
89 return Ok(data.clone()); }
91 log::info!(
93 "Inserting loaded data into cache for version: {}",
94 cache_key
95 );
96 cache.insert(cache_key.clone(), loaded_data.clone());
97 } Ok(loaded_data)
100}
101
102pub fn supported_versions(edition: Edition) -> Result<Vec<String>, McDataError> {
110 version::get_supported_versions(edition)
111}
112
113#[cfg(test)]
115mod tests {
116 use super::*;
117 use std::path::PathBuf;
118
119 fn setup() {
121 let _ = env_logger::builder().is_test(true).try_init();
123 }
124
125 fn get_test_cache_dir() -> Option<PathBuf> {
127 dirs_next::cache_dir().map(|p| p.join("mcdata-rs").join("minecraft-data"))
128 }
129
130 #[allow(dead_code)]
132 fn clear_test_cache() {
133 if let Some(cache_dir) = get_test_cache_dir() {
134 if cache_dir.exists() {
135 log::warn!("Clearing test cache directory: {}", cache_dir.display());
136 if let Err(e) = std::fs::remove_dir_all(&cache_dir) {
137 log::error!("Failed to clear test cache: {}", e);
138 }
139 }
140 }
141 }
142
143 #[test]
144 fn load_pc_1_18_2() {
145 setup();
146 let data = mc_data("1.18.2").expect("Failed to load 1.18.2 data");
147 assert_eq!(data.version.minecraft_version, "1.18.2");
148 assert_eq!(data.version.edition, Edition::Pc);
149 let stone = data
150 .blocks_by_name
151 .get("stone")
152 .expect("Stone block not found");
153 assert_eq!(stone.id, 1);
154 assert!(
155 data.items_by_name.contains_key("stick"),
156 "Stick item not found by name"
157 );
158 assert!(!data.biomes_array.is_empty(), "Biomes empty");
159 assert!(!data.entities_array.is_empty(), "Entities empty");
160 assert!(
161 data.block_collision_shapes_raw.is_some(),
162 "Collision shapes missing"
163 );
164 assert!(
165 !data.block_shapes_by_name.is_empty(),
166 "Indexed shapes empty"
167 );
168 }
169
170 #[test]
171 fn load_pc_major_version() {
172 setup();
173 let data = mc_data("1.19").expect("Failed to load 1.19 data");
175 assert!(data.version.minecraft_version.starts_with("1.19"));
176 assert_eq!(data.version.edition, Edition::Pc);
177 assert!(data.blocks_by_name.contains_key("mangrove_log"));
178 assert!(data.entities_by_name.contains_key("warden"));
179 }
180
181 #[test]
182 fn test_version_comparison() {
183 setup();
184 let data_1_18 = mc_data("1.18.2").unwrap();
185 let data_1_16 = mc_data("1.16.5").unwrap();
186 let data_1_20 = mc_data("1.20.1").unwrap(); assert!(data_1_18.is_newer_or_equal_to("1.16.5").unwrap());
189 assert!(data_1_18.is_newer_or_equal_to("1.18.2").unwrap());
190 assert!(!data_1_18.is_newer_or_equal_to("1.20.1").unwrap());
191 assert!(data_1_20.is_newer_or_equal_to("1.18.2").unwrap());
192
193 assert!(data_1_16.is_older_than("1.18.2").unwrap());
194 assert!(!data_1_16.is_older_than("1.16.5").unwrap());
195 assert!(!data_1_16.is_older_than("1.15.2").unwrap()); assert!(data_1_18.is_older_than("1.20.1").unwrap());
197 }
198
199 #[test]
200 fn test_feature_support() {
201 setup();
202 let data_1_18 = mc_data("1.18.2").unwrap();
203 let data_1_15 = mc_data("1.15.2").unwrap();
204
205 let dim_int_115 = data_1_15.support_feature("dimensionIsAnInt").unwrap();
207 assert_eq!(dim_int_115, serde_json::Value::Bool(true));
208
209 let dim_int_118 = data_1_18.support_feature("dimensionIsAnInt").unwrap();
210 assert_eq!(dim_int_118, serde_json::Value::Bool(false));
211
212 let meta_ix_118 = data_1_18.support_feature("metadataIxOfItem").unwrap();
214 assert_eq!(meta_ix_118, serde_json::Value::Number(8.into()));
215
216 let meta_ix_115 = data_1_15.support_feature("metadataIxOfItem").unwrap();
217 assert_eq!(meta_ix_115, serde_json::Value::Number(7.into()));
218 }
219
220 #[test]
221 fn test_cache() {
222 setup();
223 let version = "1.17.1";
224 log::info!("CACHE TEST: Loading {} for the first time", version);
225 let data1 = mc_data(version).expect("Load 1 failed");
226 log::info!("CACHE TEST: Loading {} for the second time", version);
227 let data2 = mc_data(version).expect("Load 2 failed");
228 assert!(
230 Arc::ptr_eq(&data1, &data2),
231 "Cache miss: Arcs point to different data for {}",
232 version
233 );
234
235 let prefixed_version = format!("pc_{}", version);
237 log::info!(
238 "CACHE TEST: Loading {} for the third time",
239 prefixed_version
240 );
241 let data3 = mc_data(&prefixed_version).expect("Load 3 failed");
242 assert!(
243 Arc::ptr_eq(&data1, &data3),
244 "Cache miss: Prefixed version {} loaded different data",
245 prefixed_version
246 );
247 }
248
249 #[test]
250 fn test_supported_versions() {
251 setup();
252 let versions =
253 supported_versions(Edition::Pc).expect("Failed to get supported PC versions");
254 assert!(!versions.is_empty());
255 assert!(versions.iter().any(|v| v == "1.8.8"));
257 assert!(versions.iter().any(|v| v == "1.16.5"));
258 assert!(versions.iter().any(|v| v == "1.18.2"));
259 assert!(versions.iter().any(|v| v == "1.20.1"));
260
261 let index_1_8 = versions.iter().position(|v| v == "1.8.8");
263 let index_1_16 = versions.iter().position(|v| v == "1.16.5");
264 assert!(index_1_8.is_some());
265 assert!(index_1_16.is_some());
266 assert!(
267 index_1_8 < index_1_16,
268 "Versions should be sorted oldest to newest"
269 );
270 }
271
272 #[test]
273 fn test_invalid_version() {
274 setup();
275 let result = mc_data("invalid_version_string_1.2.3");
276 assert!(result.is_err());
277 match result.err().unwrap() {
278 McDataError::InvalidVersion(s) => assert!(s.contains("invalid_version")),
279 e => panic!("Expected InvalidVersion error, got {:?}", e),
280 }
281 }
282
283 }