blockpedia/
lib.rs

1use std::collections::HashMap;
2
3// Core data structures
4#[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    // Future extension point for fetcher data
16    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    /// Convert to ExtendedColorData for palette operations
36    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        // Validate block ID format first
116        errors::validation::validate_block_id(block_id)?;
117
118        // Validate block ID exists in our data
119        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        // Validate property name format
131        errors::validation::validate_property_name(property)?;
132
133        // Validate property value format
134        errors::validation::validate_property_value(value)?;
135
136        // Get the block data to validate property and value
137        let block_facts = BLOCKS
138            .get(&self.block_id)
139            .ok_or_else(|| BlockpediaError::block_not_found(&self.block_id))?;
140
141        // Check if the property exists for this block
142        if !block_facts.has_property(property) {
143            return Err(BlockpediaError::property_not_found(
144                &self.block_id,
145                property,
146            ));
147        }
148
149        // Check if the value is valid for this property
150        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    /// Create a BlockState from the default state of a block
169    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        // Set all default properties
176        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    /// Parse a blockstate string without validation (for Bedrock blockstates)
186    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    /// Parse a blockstate string like "minecraft:repeater[delay=3,facing=north]"
227    pub fn parse(blockstate_str: &str) -> Result<Self> {
228        if let Some(bracket_pos) = blockstate_str.find('[') {
229            // Block with properties
230            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            // Simple block without properties
259            BlockState::new(blockstate_str)
260        }
261    }
262
263    /// Convert this Java BlockState to a Bedrock BlockState using dynamic mappings
264    pub fn to_bedrock(&self) -> Result<BlockState> {
265        // Get the block facts to fill in default properties
266        let facts = BLOCKS
267            .get(&self.block_id)
268            .ok_or_else(|| BlockpediaError::block_not_found(&self.block_id))?;
269
270        // Build a complete blockstate with all properties (including defaults)
271        let mut complete_properties = HashMap::new();
272
273        // First, add all default properties from default_state
274        for (name, value) in facts.default_state {
275            complete_properties.insert(name.to_string(), value.to_string());
276        }
277
278        // For any properties that don't have defaults, use the first allowed value
279        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        // Then, override with any explicitly set properties
286        for (name, value) in &self.properties {
287            complete_properties.insert(name.clone(), value.clone());
288        }
289
290        // Build the Java blockstate string with all properties
291        let mut props = Vec::new();
292        for (key, value) in &complete_properties {
293            props.push(format!("{}={}", key, value));
294        }
295        props.sort(); // Normalize property order
296        let java_blockstate = if props.is_empty() {
297            format!("{}[]", self.block_id)
298        } else {
299            format!("{}[{}]", self.block_id, props.join(","))
300        };
301
302        // Look up the Bedrock blockstate string in the mapping
303        if let Some(bedrock_blockstate) =
304            bedrock_mapping::BedrockBlockStateMapper::java_to_bedrock(&java_blockstate)
305        {
306            // Parse the Bedrock blockstate string (without validation since it's Bedrock format)
307            return Self::parse_unvalidated(bedrock_blockstate);
308        }
309
310        // If no direct mapping found, try fallback procedural mapping
311        // This is where Section 1.A "Procedural Property Mapping" logic goes
312        // For now, try to find a mapping for the default state if the exact state fails
313        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             // We found the default state mapping. Now try to apply the differences.
325             // This is a naive heuristic but better than nothing.
326             // Parse the bedrock default state
327             let mut bedrock_state = Self::parse_unvalidated(bedrock_default)?;
328             
329             // Apply common property remappings
330             for (key, value) in &self.properties {
331                 match key.as_str() {
332                     "facing" => {
333                         // Map facing to minecraft:cardinal_direction or direction
334                         // This requires knowing the specific bedrock property name, which varies.
335                         // But we can guess or use a look-up if we had one.
336                         // For many blocks it is minecraft:cardinal_direction
337                         bedrock_state.properties.insert("minecraft:cardinal_direction".to_string(), value.clone());
338                         // Sometimes it's just "direction"
339                         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                         // Map powered=true/false to some bit property if we knew it.
346                     }
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    /// Create a Java BlockState from a Bedrock BlockState using dynamic mappings
360    pub fn from_bedrock(bedrock_id: &str, properties: HashMap<String, String>) -> Result<Self> {
361        // Build the Bedrock blockstate string
362        let mut props = Vec::new();
363        for (key, value) in &properties {
364            props.push(format!("{}={}", key, value));
365        }
366        props.sort(); // Normalize property order
367        let bedrock_blockstate = if props.is_empty() {
368            format!("{}[]", bedrock_id)
369        } else {
370            format!("{}[{}]", bedrock_id, props.join(","))
371        };
372
373        // Look up the Java blockstate string in the mapping
374        if let Some(java_blockstate) =
375            bedrock_mapping::BedrockBlockStateMapper::bedrock_to_java(&bedrock_blockstate)
376        {
377            // Parse the Java blockstate string (with validation since it's Java format)
378            return BlockState::parse(java_blockstate);
379        }
380
381        // Fallback: Try to map based on rules if exact match fails
382        // Section 1.A: Procedural Property Mapping logic
383        
384        // 1. Identify the likely Java block ID
385        // Bedrock "minecraft:stone" -> Java "minecraft:stone"
386        // But Bedrock "minecraft:wool" [color=14] -> Java "minecraft:red_wool"
387        // We can try to use a "base" mapping if available, or just guess the ID.
388        
389        // Try stripping the namespace and seeing if it matches a Java block
390        let java_id = if bedrock_id.starts_with("minecraft:") {
391            bedrock_id.to_string()
392        } else {
393            format!("minecraft:{}", bedrock_id)
394        };
395        
396        // Check if this simple ID exists in Java blocks
397        if BLOCKS.contains_key(java_id.as_str()) {
398            let mut java_state = BlockState::new(&java_id)?;
399            
400            // Try to map properties
401            for (key, value) in properties {
402                match key.as_str() {
403                    "minecraft:cardinal_direction" | "direction" => {
404                        // Map to "facing"
405                        // Value mapping: 0,1,2,3... -> down, up, north, south, west, east?
406                        // This depends heavily on the block type.
407                        // Standard 6-way: 0=down, 1=up, 2=north, 3=south, 4=west, 5=east
408                        // Standard 4-way (horizontal): 2=north, 3=south, 4=west, 5=east
409                        let facing = match value.as_str() {
410                            "0" => "down", "1" => "up", "2" => "north", "3" => "south", "4" => "west", "5" => "east",
411                            _ => "north" // default
412                        };
413                        // Only set if the Java block has this property
414                        if BLOCKS.get(&java_id).map(|b| b.has_property("facing")).unwrap_or(false) {
415                             // Check valid values
416                             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                    // Add more procedural rules here
431                    _ => {}
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
459// Bedrock mapping module
460pub mod bedrock_mapping;
461
462// Include the generated block table
463include!(concat!(env!("OUT_DIR"), "/block_table.rs"));
464
465// Query utilities module
466pub mod queries;
467pub use queries::*;
468
469// Fetcher framework module
470pub mod fetchers;
471pub use fetchers::*;
472
473// Error handling module
474pub mod errors;
475pub use errors::{BlockpediaError, Result};
476
477// Data sources module for multi-source support
478pub mod data_sources;
479pub use data_sources::*;
480
481// Color processing module
482pub mod color;
483pub use color::ExtendedColorData;
484
485// Query builder module for chained filtering
486pub mod query_builder;
487pub use query_builder::{
488    AllBlocks, BlockQuery, ColorSamplingMethod, ColorSpace, EasingFunction, GradientConfig,
489};
490
491// Block transformation module for rotation and variants
492pub mod transforms;
493pub use transforms::{BlockShape, BlockTransforms, Direction, Rotation};
494
495/// Get a block by its string ID
496pub fn get_block(id: &str) -> Option<&'static BlockFacts> {
497    BLOCKS.get(id).copied()
498}
499
500/// Get all blocks as an iterator
501pub fn all_blocks() -> impl Iterator<Item = &'static BlockFacts> {
502    BLOCKS.values().copied()
503}
504
505// WASM bindings
506#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
507mod wasm;
508#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
509pub use wasm::*;
510
511// Include tests
512mod tests;
513
514// Block Entity translation
515pub mod block_entity;