use anyhow::{Context, Result};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct UnifiedBlockData {
pub id: String,
pub properties: HashMap<String, Vec<String>>,
pub default_state: HashMap<String, String>,
pub transparent: bool,
pub extra_properties: HashMap<String, Value>, pub bedrock_id: Option<String>,
pub bedrock_properties: Option<HashMap<String, Vec<String>>>,
pub bedrock_default_state: Option<HashMap<String, String>>,
}
pub trait DataSourceAdapter {
fn name(&self) -> &'static str;
fn fetch_url(&self) -> &'static str;
fn parse_data(&self, json_data: &str) -> Result<Vec<UnifiedBlockData>>;
fn validate_structure(&self, json: &Value) -> Result<()>;
}
pub struct PrismarineAdapter;
impl DataSourceAdapter for PrismarineAdapter {
fn name(&self) -> &'static str {
"PrismarineJS"
}
fn fetch_url(&self) -> &'static str {
"https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.20.4/blocks.json"
}
fn parse_data(&self, json_data: &str) -> Result<Vec<UnifiedBlockData>> {
let parsed: Value =
serde_json::from_str(json_data).context("Failed to parse PrismarineJS JSON")?;
let blocks_array = parsed
.as_array()
.context("PrismarineJS JSON is not an array")?;
let mut unified_blocks = Vec::new();
for block in blocks_array {
let block_obj = block.as_object().context("Block is not an object")?;
let name = block_obj
.get("name")
.and_then(|n| n.as_str())
.context("Block missing name field")?;
let id = format!("minecraft:{}", name);
let mut properties = HashMap::new();
if let Some(states) = block_obj.get("states").and_then(|s| s.as_array()) {
for state in states {
if let Some(state_obj) = state.as_object() {
if let (Some(prop_name), Some(prop_type), Some(num_values)) = (
state_obj.get("name").and_then(|n| n.as_str()),
state_obj.get("type").and_then(|t| t.as_str()),
state_obj.get("num_values").and_then(|n| n.as_u64()),
) {
let values = match prop_type {
"bool" => vec!["false".to_string(), "true".to_string()],
"int" => (0..num_values).map(|i| i.to_string()).collect(),
"enum" => {
if let Some(values_array) =
state_obj.get("values").and_then(|v| v.as_array())
{
values_array
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
} else {
(0..num_values).map(|i| format!("value_{}", i)).collect()
}
}
_ => vec!["unknown".to_string()],
};
properties.insert(prop_name.to_string(), values);
}
}
}
}
let mut extra_properties = HashMap::new();
if let Some(hardness) = block_obj.get("hardness") {
extra_properties.insert("hardness".to_string(), hardness.clone());
}
if let Some(resistance) = block_obj.get("resistance") {
extra_properties.insert("resistance".to_string(), resistance.clone());
}
unified_blocks.push(UnifiedBlockData {
id,
properties,
default_state: HashMap::new(), transparent: false, extra_properties,
bedrock_id: None,
bedrock_properties: None,
bedrock_default_state: None,
});
}
Ok(unified_blocks)
}
fn validate_structure(&self, json: &Value) -> Result<()> {
let blocks_array = json
.as_array()
.context("PrismarineJS JSON is not a valid array")?;
if blocks_array.is_empty() {
anyhow::bail!("No blocks found in PrismarineJS data");
}
for (i, block_data) in blocks_array.iter().take(3).enumerate() {
let block_obj = block_data
.as_object()
.with_context(|| format!("Block at index {} is not an object", i))?;
if !block_obj.contains_key("name") {
anyhow::bail!("Block at index {} missing 'name' field", i);
}
}
Ok(())
}
}
pub struct MCPropertyEncyclopediaAdapter;
impl DataSourceAdapter for MCPropertyEncyclopediaAdapter {
fn name(&self) -> &'static str {
"MCPropertyEncyclopedia"
}
fn fetch_url(&self) -> &'static str {
"https://raw.githubusercontent.com/JoakimThorsen/MCPropertyEncyclopedia/main/data/block_data.json"
}
fn parse_data(&self, json_data: &str) -> Result<Vec<UnifiedBlockData>> {
let parsed: Value = serde_json::from_str(json_data)
.context("Failed to parse MCPropertyEncyclopedia JSON")?;
let key_list = parsed
.get("key_list")
.and_then(|k| k.as_array())
.context("Missing or invalid key_list")?;
let properties_obj = parsed
.get("properties")
.and_then(|p| p.as_object())
.context("Missing or invalid properties")?;
let mut unified_blocks = Vec::new();
for block_name in key_list {
let block_name_str = block_name.as_str().context("Block name is not a string")?;
let id = format!(
"minecraft:{}",
block_name_str
.to_lowercase()
.replace(" ", "_")
.replace("(", "")
.replace(")", "")
.replace("-", "_")
);
let mut extra_properties = HashMap::new();
for (prop_name, prop_data) in properties_obj {
if let Some(entries) = prop_data.get("entries").and_then(|e| e.as_object()) {
if let Some(value) = entries.get(block_name_str) {
extra_properties.insert(prop_name.clone(), value.clone());
}
}
}
unified_blocks.push(UnifiedBlockData {
id,
properties: HashMap::new(), default_state: HashMap::new(),
transparent: false,
extra_properties,
bedrock_id: None,
bedrock_properties: None,
bedrock_default_state: None,
});
}
Ok(unified_blocks)
}
fn validate_structure(&self, json: &Value) -> Result<()> {
let _key_list = json
.get("key_list")
.and_then(|k| k.as_array())
.context("Missing or invalid key_list")?;
let _properties = json
.get("properties")
.and_then(|p| p.as_object())
.context("Missing or invalid properties")?;
Ok(())
}
}
pub struct BedrockDataAdapter;
impl DataSourceAdapter for BedrockDataAdapter {
fn name(&self) -> &'static str {
"BedrockData"
}
fn fetch_url(&self) -> &'static str {
"https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/bedrock/1.21.0/blocks.json"
}
fn parse_data(&self, json_data: &str) -> Result<Vec<UnifiedBlockData>> {
let parsed: Value =
serde_json::from_str(json_data).context("Failed to parse Bedrock data JSON")?;
let blocks_array = parsed
.as_array()
.context("Bedrock data JSON is not an array")?;
let mut unified_blocks = Vec::new();
for block in blocks_array {
let block_obj = block.as_object().context("Block is not an object")?;
let name = block_obj
.get("name")
.and_then(|n| n.as_str())
.context("Block missing name field")?;
let id = format!("minecraft:{}", name);
let mut properties = HashMap::new();
if let Some(states) = block_obj.get("states").and_then(|s| s.as_array()) {
for state in states {
if let Some(state_obj) = state.as_object() {
if let (Some(prop_name), Some(prop_type), Some(num_values)) = (
state_obj.get("name").and_then(|n| n.as_str()),
state_obj.get("type").and_then(|t| t.as_str()),
state_obj.get("num_values").and_then(|n| n.as_u64()),
) {
let values = match prop_type {
"bool" => vec!["false".to_string(), "true".to_string()],
"int" => (0..num_values).map(|i| i.to_string()).collect(),
"enum" => {
if let Some(values_array) =
state_obj.get("values").and_then(|v| v.as_array())
{
values_array
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
} else {
(0..num_values).map(|i| format!("value_{}", i)).collect()
}
}
_ => vec!["unknown".to_string()],
};
properties.insert(prop_name.to_string(), values);
}
}
}
}
let transparent = block_obj
.get("transparent")
.and_then(|t| t.as_bool())
.unwrap_or(false);
let mut extra_properties = HashMap::new();
if let Some(hardness) = block_obj.get("hardness") {
extra_properties.insert("hardness".to_string(), hardness.clone());
}
unified_blocks.push(UnifiedBlockData {
id: id.clone(),
properties: properties.clone(),
default_state: HashMap::new(), transparent,
extra_properties,
bedrock_id: Some(id),
bedrock_properties: Some(properties),
bedrock_default_state: Some(HashMap::new()),
});
}
Ok(unified_blocks)
}
fn validate_structure(&self, json: &Value) -> Result<()> {
let blocks_array = json
.as_array()
.context("Bedrock data JSON is not a valid array")?;
if blocks_array.is_empty() {
anyhow::bail!("No blocks found in Bedrock data");
}
Ok(())
}
}
pub struct DataSourceRegistry {
sources: Vec<Box<dyn DataSourceAdapter>>,
primary_source: Option<usize>,
}
impl DataSourceRegistry {
pub fn new() -> Self {
Self {
sources: Vec::new(),
primary_source: None,
}
}
pub fn register_source(&mut self, source: Box<dyn DataSourceAdapter>) {
self.sources.push(source);
if self.primary_source.is_none() {
self.primary_source = Some(0);
}
}
pub fn set_primary_source(&mut self, name: &str) -> Result<()> {
for (i, source) in self.sources.iter().enumerate() {
if source.name() == name {
self.primary_source = Some(i);
return Ok(());
}
}
anyhow::bail!("Data source '{}' not found", name);
}
pub fn get_primary_source(&self) -> Result<&dyn DataSourceAdapter> {
let index = self.primary_source.context("No primary data source set")?;
Ok(self.sources[index].as_ref())
}
pub fn list_sources(&self) -> Vec<&str> {
self.sources.iter().map(|s| s.name()).collect()
}
#[allow(clippy::ptr_arg)] pub fn merge_data_sources(&self, _unified_blocks: &mut Vec<UnifiedBlockData>) -> Result<()> {
Ok(())
}
}
impl Default for DataSourceRegistry {
fn default() -> Self {
let mut registry = Self::new();
registry.register_source(Box::new(PrismarineAdapter));
registry.register_source(Box::new(MCPropertyEncyclopediaAdapter));
registry.register_source(Box::new(BedrockDataAdapter));
registry
}
}