1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
5pub struct BlockFacts {
6 pub id: &'static str,
7 pub properties: &'static [(&'static str, &'static [&'static str])],
8 pub default_state: &'static [(&'static str, &'static str)],
9 pub transparent: bool,
10 pub extras: Extras,
11}
12
13#[derive(Debug, Clone, Default)]
14pub struct Extras {
15 pub mock_data: Option<i32>,
17 pub color: Option<ColorData>,
18 pub bedrock: Option<BedrockData>,
19}
20
21#[derive(Debug, Clone, Copy)]
22pub struct BedrockData {
23 pub id: &'static str,
24 pub properties: &'static [(&'static str, &'static [&'static str])],
25 pub default_state: &'static [(&'static str, &'static str)],
26}
27
28#[derive(Debug, Clone, Copy)]
29pub struct ColorData {
30 pub rgb: [u8; 3],
31 pub oklab: [f32; 3],
32}
33
34impl ColorData {
35 pub fn to_extended(&self) -> color::ExtendedColorData {
37 color::ExtendedColorData::from_rgb(self.rgb[0], self.rgb[1], self.rgb[2])
38 }
39}
40
41impl From<color::ExtendedColorData> for ColorData {
42 fn from(extended: color::ExtendedColorData) -> Self {
43 ColorData {
44 rgb: extended.rgb,
45 oklab: extended.oklab,
46 }
47 }
48}
49
50impl Extras {
51 pub const fn new() -> Self {
52 Extras {
53 mock_data: None,
54 color: None,
55 bedrock: None,
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
61pub struct BlockState {
62 block_id: String,
63 properties: HashMap<String, String>,
64}
65
66impl BlockFacts {
67 pub fn id(&self) -> &str {
68 self.id
69 }
70
71 pub fn properties(&self) -> HashMap<String, Vec<String>> {
72 let mut map = HashMap::new();
73 for (key, values) in self.properties {
74 map.insert(
75 key.to_string(),
76 values.iter().map(|s| s.to_string()).collect(),
77 );
78 }
79 map
80 }
81
82 pub fn has_property(&self, property: &str) -> bool {
83 self.properties.iter().any(|(key, _)| *key == property)
84 }
85
86 pub fn get_property_values(&self, property: &str) -> Option<Vec<String>> {
87 self.properties
88 .iter()
89 .find(|(key, _)| *key == property)
90 .map(|(_, values)| values.iter().map(|s| s.to_string()).collect())
91 }
92
93 pub fn get_property(&self, property: &str) -> Option<&str> {
94 self.default_state
95 .iter()
96 .find(|(key, _)| *key == property)
97 .map(|(_, value)| *value)
98 }
99}
100
101impl BlockState {
102 pub fn id(&self) -> &str {
103 &self.block_id
104 }
105
106 pub fn get_property(&self, property: &str) -> Option<&str> {
107 self.properties.get(property).map(|s| s.as_str())
108 }
109
110 pub fn properties(&self) -> &HashMap<String, String> {
111 &self.properties
112 }
113
114 pub fn new(block_id: &str) -> Result<Self> {
115 errors::validation::validate_block_id(block_id)?;
117
118 if !BLOCKS.contains_key(block_id) {
120 return Err(BlockpediaError::block_not_found(block_id));
121 }
122
123 Ok(BlockState {
124 block_id: block_id.to_string(),
125 properties: HashMap::new(),
126 })
127 }
128
129 pub fn with(mut self, property: &str, value: &str) -> Result<Self> {
130 errors::validation::validate_property_name(property)?;
132
133 errors::validation::validate_property_value(value)?;
135
136 let block_facts = BLOCKS
138 .get(&self.block_id)
139 .ok_or_else(|| BlockpediaError::block_not_found(&self.block_id))?;
140
141 if !block_facts.has_property(property) {
143 return Err(BlockpediaError::property_not_found(
144 &self.block_id,
145 property,
146 ));
147 }
148
149 let valid_values = block_facts.get_property_values(property).ok_or_else(|| {
151 BlockpediaError::Property(errors::PropertyError::NoValues(property.to_string()))
152 })?;
153
154 if !valid_values.contains(&value.to_string()) {
155 return Err(BlockpediaError::invalid_property_value(
156 &self.block_id,
157 property,
158 value,
159 valid_values,
160 ));
161 }
162
163 self.properties
164 .insert(property.to_string(), value.to_string());
165 Ok(self)
166 }
167
168 pub fn from_default(block_facts: &BlockFacts) -> Result<Self> {
170 let mut state = BlockState {
171 block_id: block_facts.id().to_string(),
172 properties: HashMap::new(),
173 };
174
175 for (property, value) in block_facts.default_state {
177 state
178 .properties
179 .insert(property.to_string(), value.to_string());
180 }
181
182 Ok(state)
183 }
184
185 fn parse_unvalidated(blockstate_str: &str) -> Result<Self> {
187 if let Some(bracket_pos) = blockstate_str.find('[') {
188 let block_id = &blockstate_str[..bracket_pos];
189 let properties_str = &blockstate_str[bracket_pos + 1..];
190
191 if !properties_str.ends_with(']') {
192 return Err(BlockpediaError::parse_failed(
193 blockstate_str,
194 "missing closing bracket",
195 ));
196 }
197
198 let properties_str = &properties_str[..properties_str.len() - 1];
199 let mut properties = HashMap::new();
200
201 if !properties_str.is_empty() {
202 for prop_pair in properties_str.split(',') {
203 let parts: Vec<&str> = prop_pair.split('=').collect();
204 if parts.len() != 2 {
205 return Err(BlockpediaError::parse_failed(
206 blockstate_str,
207 &format!("invalid property format: {}", prop_pair),
208 ));
209 }
210 properties.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
211 }
212 }
213
214 Ok(BlockState {
215 block_id: block_id.to_string(),
216 properties,
217 })
218 } else {
219 Ok(BlockState {
220 block_id: blockstate_str.to_string(),
221 properties: HashMap::new(),
222 })
223 }
224 }
225
226 pub fn parse(blockstate_str: &str) -> Result<Self> {
228 if let Some(bracket_pos) = blockstate_str.find('[') {
229 let block_id = &blockstate_str[..bracket_pos];
231 let properties_str = &blockstate_str[bracket_pos + 1..];
232
233 if !properties_str.ends_with(']') {
234 return Err(BlockpediaError::parse_failed(
235 blockstate_str,
236 "missing closing bracket",
237 ));
238 }
239
240 let properties_str = &properties_str[..properties_str.len() - 1];
241 let mut state = BlockState::new(block_id)?;
242
243 if !properties_str.is_empty() {
244 for prop_pair in properties_str.split(',') {
245 let parts: Vec<&str> = prop_pair.split('=').collect();
246 if parts.len() != 2 {
247 return Err(BlockpediaError::parse_failed(
248 blockstate_str,
249 &format!("invalid property format: {}", prop_pair),
250 ));
251 }
252 state = state.with(parts[0].trim(), parts[1].trim())?;
253 }
254 }
255
256 Ok(state)
257 } else {
258 BlockState::new(blockstate_str)
260 }
261 }
262
263 pub fn to_bedrock(&self) -> Result<BlockState> {
265 let facts = BLOCKS
267 .get(&self.block_id)
268 .ok_or_else(|| BlockpediaError::block_not_found(&self.block_id))?;
269
270 let mut complete_properties = HashMap::new();
272
273 for (name, value) in facts.default_state {
275 complete_properties.insert(name.to_string(), value.to_string());
276 }
277
278 for (name, values) in facts.properties {
280 if !complete_properties.contains_key(*name) && !values.is_empty() {
281 complete_properties.insert(name.to_string(), values[0].to_string());
282 }
283 }
284
285 for (name, value) in &self.properties {
287 complete_properties.insert(name.clone(), value.clone());
288 }
289
290 let mut props = Vec::new();
292 for (key, value) in &complete_properties {
293 props.push(format!("{}={}", key, value));
294 }
295 props.sort(); let java_blockstate = if props.is_empty() {
297 format!("{}[]", self.block_id)
298 } else {
299 format!("{}[{}]", self.block_id, props.join(","))
300 };
301
302 if let Some(bedrock_blockstate) =
304 bedrock_mapping::BedrockBlockStateMapper::java_to_bedrock(&java_blockstate)
305 {
306 return Self::parse_unvalidated(bedrock_blockstate);
308 }
309
310 let default_props = facts.default_state;
314 let mut def_props_vec = Vec::new();
315 for (k, v) in default_props {
316 def_props_vec.push(format!("{}={}", k, v));
317 }
318 def_props_vec.sort();
319 let default_state_str = format!("{}[{}]", self.block_id, def_props_vec.join(","));
320
321 if let Some(bedrock_default) =
322 bedrock_mapping::BedrockBlockStateMapper::java_to_bedrock(&default_state_str)
323 {
324 let mut bedrock_state = Self::parse_unvalidated(bedrock_default)?;
328
329 for (key, value) in &self.properties {
331 match key.as_str() {
332 "facing" => {
333 bedrock_state.properties.insert("minecraft:cardinal_direction".to_string(), value.clone());
338 bedrock_state.properties.insert("direction".to_string(), match value.as_str() {
340 "down" => "0", "up" => "1", "north" => "2", "south" => "3", "west" => "4", "east" => "5",
341 _ => "0"
342 }.to_string());
343 },
344 "powered" => {
345 }
347 _ => {}
348 }
349 }
350 return Ok(bedrock_state);
351 }
352
353 Err(BlockpediaError::custom(format!(
354 "No Bedrock mapping found for Java blockstate: {}",
355 java_blockstate
356 )))
357 }
358
359 pub fn from_bedrock(bedrock_id: &str, properties: HashMap<String, String>) -> Result<Self> {
361 let mut props = Vec::new();
363 for (key, value) in &properties {
364 props.push(format!("{}={}", key, value));
365 }
366 props.sort(); let bedrock_blockstate = if props.is_empty() {
368 format!("{}[]", bedrock_id)
369 } else {
370 format!("{}[{}]", bedrock_id, props.join(","))
371 };
372
373 if let Some(java_blockstate) =
375 bedrock_mapping::BedrockBlockStateMapper::bedrock_to_java(&bedrock_blockstate)
376 {
377 return BlockState::parse(java_blockstate);
379 }
380
381 let java_id = if bedrock_id.starts_with("minecraft:") {
391 bedrock_id.to_string()
392 } else {
393 format!("minecraft:{}", bedrock_id)
394 };
395
396 if BLOCKS.contains_key(java_id.as_str()) {
398 let mut java_state = BlockState::new(&java_id)?;
399
400 for (key, value) in properties {
402 match key.as_str() {
403 "minecraft:cardinal_direction" | "direction" => {
404 let facing = match value.as_str() {
410 "0" => "down", "1" => "up", "2" => "north", "3" => "south", "4" => "west", "5" => "east",
411 _ => "north" };
413 if BLOCKS.get(&java_id).map(|b| b.has_property("facing")).unwrap_or(false) {
415 if let Some(valid) = BLOCKS.get(&java_id).and_then(|b| b.get_property_values("facing")) {
417 if valid.contains(&facing.to_string()) {
418 java_state = java_state.with("facing", facing)?;
419 }
420 }
421 }
422 },
423 "output_lit_bit" => {
424 if value == "1" {
425 if BLOCKS.get(&java_id).map(|b| b.has_property("powered")).unwrap_or(false) {
426 java_state = java_state.with("powered", "true")?;
427 }
428 }
429 },
430 _ => {}
432 }
433 }
434 return Ok(java_state);
435 }
436
437 Err(BlockpediaError::custom(format!(
438 "No Java mapping found for Bedrock blockstate: {}",
439 bedrock_blockstate
440 )))
441 }
442}
443
444impl std::fmt::Display for BlockState {
445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446 if self.properties.is_empty() {
447 write!(f, "{}", self.block_id)
448 } else {
449 let mut props = Vec::new();
450 for (key, value) in &self.properties {
451 props.push(format!("{}={}", key, value));
452 }
453 props.sort();
454 write!(f, "{}[{}]", self.block_id, props.join(","))
455 }
456 }
457}
458
459pub mod bedrock_mapping;
461
462include!(concat!(env!("OUT_DIR"), "/block_table.rs"));
464
465pub mod queries;
467pub use queries::*;
468
469pub mod fetchers;
471pub use fetchers::*;
472
473pub mod errors;
475pub use errors::{BlockpediaError, Result};
476
477pub mod data_sources;
479pub use data_sources::*;
480
481pub mod color;
483pub use color::ExtendedColorData;
484
485pub mod query_builder;
487pub use query_builder::{
488 AllBlocks, BlockQuery, ColorSamplingMethod, ColorSpace, EasingFunction, GradientConfig,
489};
490
491pub mod transforms;
493pub use transforms::{BlockShape, BlockTransforms, Direction, Rotation};
494
495pub fn get_block(id: &str) -> Option<&'static BlockFacts> {
497 BLOCKS.get(id).copied()
498}
499
500pub fn all_blocks() -> impl Iterator<Item = &'static BlockFacts> {
502 BLOCKS.values().copied()
503}
504
505#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
507mod wasm;
508#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
509pub use wasm::*;
510
511mod tests;
513
514pub mod block_entity;