use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct BlockFacts {
pub id: &'static str,
pub properties: &'static [(&'static str, &'static [&'static str])],
pub default_state: &'static [(&'static str, &'static str)],
pub transparent: bool,
pub emit_light: u8,
pub kind: &'static str,
pub base_block: Option<&'static str>,
pub tags: &'static [&'static str],
pub full_cube: bool,
pub has_block_entity: bool,
pub extras: Extras,
}
#[derive(Debug, Clone, Default)]
pub struct Extras {
pub mock_data: Option<i32>,
pub color: Option<ColorData>,
pub bedrock: Option<BedrockData>,
}
#[derive(Debug, Clone, Copy)]
pub struct BedrockData {
pub id: &'static str,
pub properties: &'static [(&'static str, &'static [&'static str])],
pub default_state: &'static [(&'static str, &'static str)],
}
#[derive(Debug, Clone, Copy)]
pub struct ColorData {
pub rgb: [u8; 3],
pub oklab: [f32; 3],
}
impl ColorData {
pub fn to_extended(&self) -> color::ExtendedColorData {
color::ExtendedColorData::from_rgb(self.rgb[0], self.rgb[1], self.rgb[2])
}
}
impl From<color::ExtendedColorData> for ColorData {
fn from(extended: color::ExtendedColorData) -> Self {
ColorData {
rgb: extended.rgb,
oklab: extended.oklab,
}
}
}
impl Extras {
pub const fn new() -> Self {
Extras {
mock_data: None,
color: None,
bedrock: None,
}
}
}
#[derive(Debug, Clone)]
pub struct BlockState {
block_id: String,
properties: HashMap<String, String>,
}
impl BlockFacts {
pub fn id(&self) -> &str {
self.id
}
pub fn kind(&self) -> &'static str {
self.kind
}
pub fn base_block(&self) -> Option<&'static str> {
self.base_block
}
pub fn has_tag(&self, tag: &str) -> bool {
if tag.contains(':') {
self.tags.iter().any(|t| *t == tag)
} else {
self.tags
.iter()
.any(|t| t.strip_prefix("minecraft:") == Some(tag))
}
}
pub fn is_full_cube(&self) -> bool {
self.full_cube
}
pub fn has_block_entity(&self) -> bool {
self.has_block_entity
}
pub fn is_light_source(&self) -> bool {
self.emit_light > 0
}
pub fn properties(&self) -> HashMap<String, Vec<String>> {
let mut map = HashMap::new();
for (key, values) in self.properties {
map.insert(
key.to_string(),
values.iter().map(|s| s.to_string()).collect(),
);
}
map
}
pub fn has_property(&self, property: &str) -> bool {
self.properties.iter().any(|(key, _)| *key == property)
}
pub fn get_property_values(&self, property: &str) -> Option<Vec<String>> {
self.properties
.iter()
.find(|(key, _)| *key == property)
.map(|(_, values)| values.iter().map(|s| s.to_string()).collect())
}
pub fn get_property(&self, property: &str) -> Option<&str> {
self.default_state
.iter()
.find(|(key, _)| *key == property)
.map(|(_, value)| *value)
}
}
impl BlockState {
pub fn id(&self) -> &str {
&self.block_id
}
pub fn get_property(&self, property: &str) -> Option<&str> {
self.properties.get(property).map(|s| s.as_str())
}
pub fn properties(&self) -> &HashMap<String, String> {
&self.properties
}
pub fn new(block_id: &str) -> Result<Self> {
errors::validation::validate_block_id(block_id)?;
if !BLOCKS.contains_key(block_id) {
return Err(BlockpediaError::block_not_found(block_id));
}
Ok(BlockState {
block_id: block_id.to_string(),
properties: HashMap::new(),
})
}
pub fn with(mut self, property: &str, value: &str) -> Result<Self> {
errors::validation::validate_property_name(property)?;
errors::validation::validate_property_value(value)?;
let block_facts = BLOCKS
.get(&self.block_id)
.ok_or_else(|| BlockpediaError::block_not_found(&self.block_id))?;
if !block_facts.has_property(property) {
return Err(BlockpediaError::property_not_found(
&self.block_id,
property,
));
}
let valid_values = block_facts.get_property_values(property).ok_or_else(|| {
BlockpediaError::Property(errors::PropertyError::NoValues(property.to_string()))
})?;
if !valid_values.contains(&value.to_string()) {
return Err(BlockpediaError::invalid_property_value(
&self.block_id,
property,
value,
valid_values,
));
}
self.properties
.insert(property.to_string(), value.to_string());
Ok(self)
}
pub fn from_default(block_facts: &BlockFacts) -> Result<Self> {
let mut state = BlockState {
block_id: block_facts.id().to_string(),
properties: HashMap::new(),
};
for (property, value) in block_facts.default_state {
state
.properties
.insert(property.to_string(), value.to_string());
}
Ok(state)
}
fn parse_unvalidated(blockstate_str: &str) -> Result<Self> {
if let Some(bracket_pos) = blockstate_str.find('[') {
let block_id = &blockstate_str[..bracket_pos];
let properties_str = &blockstate_str[bracket_pos + 1..];
if !properties_str.ends_with(']') {
return Err(BlockpediaError::parse_failed(
blockstate_str,
"missing closing bracket",
));
}
let properties_str = &properties_str[..properties_str.len() - 1];
let mut properties = HashMap::new();
if !properties_str.is_empty() {
for prop_pair in properties_str.split(',') {
let parts: Vec<&str> = prop_pair.split('=').collect();
if parts.len() != 2 {
return Err(BlockpediaError::parse_failed(
blockstate_str,
&format!("invalid property format: {}", prop_pair),
));
}
properties.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
}
}
Ok(BlockState {
block_id: block_id.to_string(),
properties,
})
} else {
Ok(BlockState {
block_id: blockstate_str.to_string(),
properties: HashMap::new(),
})
}
}
pub fn parse(blockstate_str: &str) -> Result<Self> {
if let Some(bracket_pos) = blockstate_str.find('[') {
let block_id = &blockstate_str[..bracket_pos];
let properties_str = &blockstate_str[bracket_pos + 1..];
if !properties_str.ends_with(']') {
return Err(BlockpediaError::parse_failed(
blockstate_str,
"missing closing bracket",
));
}
let properties_str = &properties_str[..properties_str.len() - 1];
let mut state = BlockState::new(block_id)?;
if !properties_str.is_empty() {
for prop_pair in properties_str.split(',') {
let parts: Vec<&str> = prop_pair.split('=').collect();
if parts.len() != 2 {
return Err(BlockpediaError::parse_failed(
blockstate_str,
&format!("invalid property format: {}", prop_pair),
));
}
state = state.with(parts[0].trim(), parts[1].trim())?;
}
}
Ok(state)
} else {
BlockState::new(blockstate_str)
}
}
pub fn to_bedrock(&self) -> Result<BlockState> {
let facts = BLOCKS
.get(&self.block_id)
.ok_or_else(|| BlockpediaError::block_not_found(&self.block_id))?;
let mut complete_properties = HashMap::new();
for (name, value) in facts.default_state {
complete_properties.insert(name.to_string(), value.to_string());
}
for (name, values) in facts.properties {
if !complete_properties.contains_key(*name) && !values.is_empty() {
complete_properties.insert(name.to_string(), values[0].to_string());
}
}
for (name, value) in &self.properties {
complete_properties.insert(name.clone(), value.clone());
}
let mut props = Vec::new();
for (key, value) in &complete_properties {
props.push(format!("{}={}", key, value));
}
props.sort(); let java_blockstate = if props.is_empty() {
format!("{}[]", self.block_id)
} else {
format!("{}[{}]", self.block_id, props.join(","))
};
if let Some(bedrock_blockstate) =
bedrock_mapping::BedrockBlockStateMapper::java_to_bedrock(&java_blockstate)
{
return Self::parse_unvalidated(bedrock_blockstate);
}
let default_props = facts.default_state;
let mut def_props_vec = Vec::new();
for (k, v) in default_props {
def_props_vec.push(format!("{}={}", k, v));
}
def_props_vec.sort();
let default_state_str = format!("{}[{}]", self.block_id, def_props_vec.join(","));
if let Some(bedrock_default) =
bedrock_mapping::BedrockBlockStateMapper::java_to_bedrock(&default_state_str)
{
let mut bedrock_state = Self::parse_unvalidated(bedrock_default)?;
for (key, value) in &self.properties {
match key.as_str() {
"facing" => {
bedrock_state.properties.insert("minecraft:cardinal_direction".to_string(), value.clone());
bedrock_state.properties.insert("direction".to_string(), match value.as_str() {
"down" => "0", "up" => "1", "north" => "2", "south" => "3", "west" => "4", "east" => "5",
_ => "0"
}.to_string());
},
"powered" => {
}
_ => {}
}
}
return Ok(bedrock_state);
}
Err(BlockpediaError::custom(format!(
"No Bedrock mapping found for Java blockstate: {}",
java_blockstate
)))
}
pub fn from_bedrock(bedrock_id: &str, properties: HashMap<String, String>) -> Result<Self> {
let mut props = Vec::new();
for (key, value) in &properties {
props.push(format!("{}={}", key, value));
}
props.sort(); let bedrock_blockstate = if props.is_empty() {
format!("{}[]", bedrock_id)
} else {
format!("{}[{}]", bedrock_id, props.join(","))
};
if let Some(java_blockstate) =
bedrock_mapping::BedrockBlockStateMapper::bedrock_to_java(&bedrock_blockstate)
{
return BlockState::parse(java_blockstate);
}
let java_id = if bedrock_id.starts_with("minecraft:") {
bedrock_id.to_string()
} else {
format!("minecraft:{}", bedrock_id)
};
if BLOCKS.contains_key(java_id.as_str()) {
let mut java_state = BlockState::new(&java_id)?;
for (key, value) in properties {
match key.as_str() {
"minecraft:cardinal_direction" | "direction" => {
let facing = match value.as_str() {
"0" => "down", "1" => "up", "2" => "north", "3" => "south", "4" => "west", "5" => "east",
_ => "north" };
if BLOCKS.get(&java_id).map(|b| b.has_property("facing")).unwrap_or(false) {
if let Some(valid) = BLOCKS.get(&java_id).and_then(|b| b.get_property_values("facing")) {
if valid.contains(&facing.to_string()) {
java_state = java_state.with("facing", facing)?;
}
}
}
},
"output_lit_bit" => {
if value == "1" {
if BLOCKS.get(&java_id).map(|b| b.has_property("powered")).unwrap_or(false) {
java_state = java_state.with("powered", "true")?;
}
}
},
_ => {}
}
}
return Ok(java_state);
}
Err(BlockpediaError::custom(format!(
"No Java mapping found for Bedrock blockstate: {}",
bedrock_blockstate
)))
}
}
impl std::fmt::Display for BlockState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.properties.is_empty() {
write!(f, "{}", self.block_id)
} else {
let mut props = Vec::new();
for (key, value) in &self.properties {
props.push(format!("{}={}", key, value));
}
props.sort();
write!(f, "{}[{}]", self.block_id, props.join(","))
}
}
}
pub mod bedrock_mapping;
include!(concat!(env!("OUT_DIR"), "/block_table.rs"));
pub mod queries;
pub use queries::*;
pub mod fetchers;
pub use fetchers::*;
pub mod errors;
pub use errors::{BlockpediaError, Result};
pub mod data_sources;
pub use data_sources::*;
pub mod color;
pub use color::ExtendedColorData;
pub mod query_builder;
pub use query_builder::{
AllBlocks, BlockQuery, ColorSamplingMethod, ColorSpace, EasingFunction, GradientConfig,
};
pub mod facts_json;
pub mod transforms;
pub use transforms::{BlockShape, BlockTransforms, Direction, Rotation};
pub fn get_block(id: &str) -> Option<&'static BlockFacts> {
BLOCKS.get(id).copied()
}
pub fn all_blocks() -> impl Iterator<Item = &'static BlockFacts> {
BLOCKS.values().copied()
}
pub fn blocks_by_tag(tag: &str) -> impl Iterator<Item = &'static BlockFacts> {
const EMPTY: &[&str] = &[];
let key = if tag.contains(':') {
tag.to_string()
} else {
format!("minecraft:{tag}")
};
BLOCK_TAGS
.get(key.as_str())
.copied()
.unwrap_or(EMPTY)
.iter()
.filter_map(|id| get_block(id))
}
pub fn variants_of(base_id: &str) -> Vec<&'static BlockFacts> {
let key = if base_id.contains(':') {
base_id.to_string()
} else {
format!("minecraft:{base_id}")
};
let mut variants: Vec<&'static BlockFacts> = all_blocks()
.filter(|b| b.base_block == Some(key.as_str()))
.collect();
variants.sort_by_key(|b| b.id);
variants
}
pub mod block_entity;