use super::{palettes::GradientMethod, ExtendedColorData};
use crate::blockpedia::BlockFacts;
use crate::blockpedia::BLOCKS;
pub struct BlockPaletteGenerator;
#[derive(Debug, Clone)]
pub struct BlockPalette {
pub name: String,
pub description: String,
pub blocks: Vec<BlockRecommendation>,
pub theme: PaletteTheme,
}
#[derive(Debug, Clone)]
pub struct BlockRecommendation {
pub block: &'static BlockFacts,
pub color: ExtendedColorData,
pub role: BlockRole,
pub usage_notes: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PaletteTheme {
Monochrome,
Gradient,
Complementary,
Analogous,
Triadic,
Natural,
Architectural,
Seasonal,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BlockRole {
Primary, Secondary, Accent, Transition, Highlight, }
#[derive(Debug, Clone, Default)]
pub struct BlockFilter {
pub exclude_falling: bool,
pub exclude_tile_entities: bool,
pub full_blocks_only: bool,
pub exclude_needs_support: bool,
pub exclude_transparent: bool,
pub exclude_light_sources: bool,
pub survival_obtainable_only: bool,
pub exclude_patterns: Vec<String>,
pub include_patterns: Vec<String>,
pub required_tags: Vec<String>,
pub excluded_tags: Vec<String>,
pub kinds: Vec<String>,
}
fn normalize_mc_name(name: &str) -> String {
if name.contains(':') {
name.to_string()
} else {
format!("minecraft:{name}")
}
}
impl BlockFilter {
pub fn solid_blocks_only() -> Self {
BlockFilter {
exclude_falling: true,
exclude_tile_entities: true,
full_blocks_only: true,
exclude_needs_support: true,
exclude_transparent: true,
exclude_light_sources: false,
survival_obtainable_only: true,
exclude_patterns: vec![
"_slab".to_string(),
"_stairs".to_string(),
"_fence".to_string(),
"_gate".to_string(),
"_wall".to_string(),
"_button".to_string(),
"_pressure_plate".to_string(),
"_door".to_string(),
"_trapdoor".to_string(),
],
..Default::default()
}
}
pub fn decorative_blocks() -> Self {
BlockFilter {
exclude_falling: true,
exclude_tile_entities: true,
full_blocks_only: false,
exclude_needs_support: false,
exclude_transparent: false,
exclude_light_sources: false,
survival_obtainable_only: true,
..Default::default()
}
}
pub fn structural_blocks_only() -> Self {
BlockFilter {
exclude_falling: true,
exclude_tile_entities: true,
full_blocks_only: true,
exclude_needs_support: true,
exclude_transparent: true,
exclude_light_sources: true,
survival_obtainable_only: true,
exclude_patterns: vec![
"_slab".to_string(),
"_stairs".to_string(),
"_fence".to_string(),
"_gate".to_string(),
"_wall".to_string(),
"_button".to_string(),
"_pressure_plate".to_string(),
"_door".to_string(),
"_trapdoor".to_string(),
"glass".to_string(),
"water".to_string(),
"lava".to_string(),
"air".to_string(),
],
..Default::default()
}
}
pub fn allows_block(&self, block: &BlockFacts) -> bool {
let id = block.id().to_lowercase();
if !self.include_patterns.is_empty() {
let included = self
.include_patterns
.iter()
.any(|pattern| id.contains(&pattern.to_lowercase()));
if !included {
return false;
}
}
if self
.exclude_patterns
.iter()
.any(|pattern| id.contains(&pattern.to_lowercase()))
{
return false;
}
if self.exclude_falling && Self::is_falling_block(&id) {
return false;
}
if self.exclude_tile_entities && Self::is_tile_entity(&id) {
return false;
}
if self.full_blocks_only && !block.is_full_cube() {
return false;
}
if self.exclude_needs_support && Self::needs_support(&id) {
return false;
}
if self.exclude_transparent && block.transparent {
return false;
}
if self.exclude_light_sources && Self::is_light_source(&id) {
return false;
}
if self.survival_obtainable_only && !Self::is_survival_obtainable(&id) {
return false;
}
if !self.required_tags.is_empty() && !self.required_tags.iter().all(|t| block.has_tag(t)) {
return false;
}
if self.excluded_tags.iter().any(|t| block.has_tag(t)) {
return false;
}
if !self.kinds.is_empty()
&& !self
.kinds
.iter()
.any(|k| block.kind() == normalize_mc_name(k))
{
return false;
}
true
}
fn is_falling_block(id: &str) -> bool {
matches!(id,
id if id.contains("sand") ||
id.contains("gravel") ||
id.contains("anvil") ||
id.contains("concrete_powder")
)
}
fn is_tile_entity(id: &str) -> bool {
matches!(id,
id if id.contains("chest") ||
id.contains("furnace") ||
id.contains("dispenser") ||
id.contains("dropper") ||
id.contains("hopper") ||
id.contains("beacon") ||
id.contains("brewing_stand") ||
id.contains("enchanting_table") ||
id.contains("ender_chest") ||
id.contains("shulker_box") ||
id.contains("barrel") ||
id.contains("smoker") ||
id.contains("blast_furnace") ||
id.contains("campfire") ||
id.contains("lectern") ||
id.contains("jukebox")
)
}
fn needs_support(id: &str) -> bool {
matches!(id,
id if id.contains("torch") ||
id.contains("flower") ||
id.contains("grass") && !id.contains("grass_block") ||
id.contains("fern") ||
id.contains("sapling") ||
id.contains("mushroom") && !id.contains("mushroom_block") ||
id.contains("wheat") ||
id.contains("carrot") ||
id.contains("potato") ||
id.contains("beetroot") ||
id.contains("sugar_cane") ||
id.contains("cactus") ||
id.contains("bamboo") ||
id.contains("vine") ||
id.contains("lily_pad") ||
id.contains("seagrass") ||
id.contains("kelp") ||
id.contains("coral") && !id.contains("coral_block") ||
id.contains("button") ||
id.contains("lever") ||
id.contains("sign") ||
id.contains("banner") ||
id.contains("painting")
)
}
fn is_light_source(id: &str) -> bool {
matches!(id,
id if id.contains("torch") ||
id.contains("lantern") ||
id.contains("glowstone") ||
id.contains("sea_lantern") ||
id.contains("beacon") ||
id.contains("campfire") ||
id.contains("fire") ||
id.contains("lava") ||
id.contains("magma_block") ||
id.contains("jack_o_lantern") ||
id.contains("redstone_lamp") ||
id.contains("shroomlight") ||
id.contains("crying_obsidian") ||
id.contains("respawn_anchor") ||
id.contains("candle") ||
id.contains("glow_lichen") ||
id.contains("amethyst_cluster")
)
}
fn is_survival_obtainable(id: &str) -> bool {
!matches!(id,
id if id.contains("barrier") ||
id.contains("structure_void") ||
id.contains("structure_block") ||
id.contains("command_block") ||
id.contains("chain_command_block") ||
id.contains("repeating_command_block") ||
id.contains("jigsaw") ||
id.contains("debug_stick") ||
id.contains("knowledge_book") ||
id.contains("spawn_egg")
)
}
}
#[allow(dead_code, clippy::needless_borrow, clippy::explicit_auto_deref)] impl BlockPaletteGenerator {
pub fn generate_block_gradient(
start_block: &'static BlockFacts,
end_block: &'static BlockFacts,
steps: usize,
) -> Option<BlockPalette> {
Self::generate_block_gradient_filtered(
start_block,
end_block,
steps,
&BlockFilter::default(),
)
}
pub fn generate_block_gradient_filtered(
start_block: &'static BlockFacts,
end_block: &'static BlockFacts,
steps: usize,
filter: &BlockFilter,
) -> Option<BlockPalette> {
let start_color = start_block.extras.color?.to_extended();
let end_color = end_block.extras.color?.to_extended();
let color_gradient = super::palettes::PaletteGenerator::generate_gradient_palette(
start_color,
end_color,
steps,
GradientMethod::LinearOklab,
);
let mut blocks = Vec::new();
for (i, target_color) in color_gradient.iter().enumerate() {
if let Some(block) = Self::find_closest_block_to_color_filtered(*target_color, filter) {
let role = match i {
0 => BlockRole::Primary,
i if i == steps - 1 => BlockRole::Accent,
i if i == steps / 2 => BlockRole::Secondary,
_ => BlockRole::Transition,
};
let usage_notes = Self::generate_usage_notes(&block, &role);
blocks.push(BlockRecommendation {
block,
color: block.extras.color?.to_extended(),
role,
usage_notes,
});
}
}
Some(BlockPalette {
name: format!(
"{} to {} Gradient",
Self::block_display_name(start_block),
Self::block_display_name(end_block)
),
description: format!(
"A smooth gradient from {} to {} using {} blocks for natural color flow",
Self::block_display_name(start_block),
Self::block_display_name(end_block),
blocks.len()
),
blocks,
theme: PaletteTheme::Gradient,
})
}
pub fn generate_monochrome_palette(
base_block: &'static BlockFacts,
range: usize,
) -> Option<BlockPalette> {
let base_color = base_block.extras.color?.to_extended();
let mono_colors =
super::palettes::PaletteGenerator::generate_monochrome_palette(base_color, range);
let mut blocks = Vec::new();
for (i, target_color) in mono_colors.iter().enumerate() {
if let Some(block) = Self::find_closest_block_to_color(*target_color) {
let role = match i {
0 => BlockRole::Accent, i if i == range / 2 => BlockRole::Primary, i if i == range - 1 => BlockRole::Highlight, _ => BlockRole::Secondary,
};
let usage_notes = Self::generate_usage_notes(&block, &role);
blocks.push(BlockRecommendation {
block,
color: block.extras.color?.to_extended(),
role,
usage_notes,
});
}
}
Some(BlockPalette {
name: format!("{} Monochrome", Self::block_display_name(base_block)),
description: format!(
"A monochrome palette based on {} with {} tonal variations from dark to light",
Self::block_display_name(base_block),
blocks.len()
),
blocks,
theme: PaletteTheme::Monochrome,
})
}
pub fn generate_complementary_palette(base_block: &'static BlockFacts) -> Option<BlockPalette> {
let base_color = base_block.extras.color?.to_extended();
let comp_colors =
super::palettes::PaletteGenerator::generate_complementary_palette(&base_color);
let mut blocks = Vec::new();
for (i, target_color) in comp_colors.iter().enumerate() {
if let Some(block) = Self::find_closest_block_to_color(*target_color) {
let role = match i {
0 => BlockRole::Primary,
1 => BlockRole::Accent,
_ => BlockRole::Secondary,
};
let usage_notes = Self::generate_usage_notes(&block, &role);
blocks.push(BlockRecommendation {
block,
color: block.extras.color?.to_extended(),
role,
usage_notes,
});
}
}
Some(BlockPalette {
name: format!("{} Complementary", Self::block_display_name(base_block)),
description: format!(
"A complementary color scheme based on {} with high contrast blocks",
Self::block_display_name(base_block)
),
blocks,
theme: PaletteTheme::Complementary,
})
}
pub fn generate_natural_palette(theme: &str) -> Option<BlockPalette> {
Self::generate_natural_palette_filtered(theme, &BlockFilter::default())
}
pub fn generate_natural_palette_filtered(
theme: &str,
filter: &BlockFilter,
) -> Option<BlockPalette> {
match theme.to_lowercase().as_str() {
"forest" | "woods" => Self::generate_forest_palette_filtered(filter),
"desert" | "sand" => Self::generate_desert_palette_filtered(filter),
"ocean" | "water" => Self::generate_ocean_palette_filtered(filter),
"mountain" | "stone" => Self::generate_mountain_palette_filtered(filter),
"nether" => Self::generate_nether_palette_filtered(filter),
"end" => Self::generate_end_palette_filtered(filter),
_ => None,
}
}
pub fn generate_architectural_palette(style: &str) -> Option<BlockPalette> {
Self::generate_architectural_palette_filtered(style, &BlockFilter::default())
}
pub fn generate_architectural_palette_filtered(
style: &str,
filter: &BlockFilter,
) -> Option<BlockPalette> {
match style.to_lowercase().as_str() {
"medieval" => Self::generate_medieval_palette_filtered(filter),
"modern" => Self::generate_modern_palette_filtered(filter),
"rustic" => Self::generate_rustic_palette_filtered(filter),
"industrial" => Self::generate_industrial_palette_filtered(filter),
_ => None,
}
}
fn find_closest_block_to_color(target_color: ExtendedColorData) -> Option<&'static BlockFacts> {
let mut best_block = None;
let mut best_distance = f32::INFINITY;
for block in BLOCKS.values() {
if let Some(block_color) = block.extras.color {
let distance = block_color.to_extended().distance_oklab(&target_color);
if distance < best_distance {
best_distance = distance;
best_block = Some(*block);
}
}
}
best_block
}
fn find_closest_block_to_color_filtered(
target_color: ExtendedColorData,
filter: &BlockFilter,
) -> Option<&'static BlockFacts> {
let mut best_block = None;
let mut best_distance = f32::INFINITY;
for block in BLOCKS.values() {
if !filter.allows_block(block) {
continue;
}
if let Some(block_color) = block.extras.color {
let distance = block_color.to_extended().distance_oklab(&target_color);
if distance < best_distance {
best_distance = distance;
best_block = Some(*block);
}
}
}
best_block
}
fn generate_usage_notes(block: &BlockFacts, role: &BlockRole) -> String {
let block_type = Self::categorize_block(block);
match (role, block_type.as_str()) {
(BlockRole::Primary, "stone") => {
"Excellent for foundations, walls, and main structures".to_string()
}
(BlockRole::Primary, "wood") => {
"Great for frames, floors, and warm architectural elements".to_string()
}
(BlockRole::Primary, "concrete") => {
"Perfect for modern builds and large surfaces".to_string()
}
(BlockRole::Secondary, "stone") => {
"Use for detailing, trim, and structural accents".to_string()
}
(BlockRole::Secondary, "wood") => {
"Ideal for stairs, slabs, and secondary features".to_string()
}
(BlockRole::Secondary, _) => {
"Good for supporting elements and medium-scale features".to_string()
}
(BlockRole::Accent, _) => {
"Use sparingly for highlights, borders, and eye-catching details".to_string()
}
(BlockRole::Transition, _) => {
"Perfect for gradual color changes and smooth blending".to_string()
}
(BlockRole::Highlight, _) => {
"Excellent for focal points, lighting accents, and key features".to_string()
}
_ => "Versatile block suitable for various building applications".to_string(),
}
}
fn categorize_block(block: &BlockFacts) -> String {
let id = block.id().to_lowercase();
if id.contains("stone") || id.contains("cobblestone") || id.contains("brick") {
"stone".to_string()
} else if id.contains("wood") || id.contains("plank") || id.contains("log") {
"wood".to_string()
} else if id.contains("concrete") || id.contains("terracotta") {
"concrete".to_string()
} else if id.contains("wool") || id.contains("carpet") {
"fabric".to_string()
} else if id.contains("glass") {
"glass".to_string()
} else if id.contains("metal") || id.contains("iron") || id.contains("gold") {
"metal".to_string()
} else {
"other".to_string()
}
}
fn block_display_name(block: &BlockFacts) -> String {
block
.id()
.strip_prefix("minecraft:")
.unwrap_or(block.id())
.replace('_', " ")
.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn generate_forest_palette() -> Option<BlockPalette> {
Self::generate_forest_palette_filtered(&BlockFilter::default())
}
fn generate_forest_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let forest_blocks = [
"minecraft:oak_log",
"minecraft:oak_leaves",
"minecraft:grass_block",
"minecraft:coarse_dirt",
"minecraft:moss_block",
"minecraft:fern",
];
Self::create_themed_palette_filtered(
"Forest Biome",
"Natural forest colors with browns, greens, and earth tones",
&forest_blocks,
PaletteTheme::Natural,
filter,
)
}
fn generate_desert_palette() -> Option<BlockPalette> {
Self::generate_desert_palette_filtered(&BlockFilter::default())
}
fn generate_desert_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let desert_blocks = [
"minecraft:sand",
"minecraft:sandstone",
"minecraft:smooth_sandstone",
"minecraft:cut_sandstone",
"minecraft:red_sand",
"minecraft:terracotta",
];
Self::create_themed_palette_filtered(
"Desert Biome",
"Warm sandy colors and sun-baked earth tones",
&desert_blocks,
PaletteTheme::Natural,
filter,
)
}
fn generate_ocean_palette() -> Option<BlockPalette> {
Self::generate_ocean_palette_filtered(&BlockFilter::default())
}
fn generate_ocean_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let ocean_blocks = [
"minecraft:water",
"minecraft:prismarine",
"minecraft:dark_prismarine",
"minecraft:sea_lantern",
"minecraft:kelp",
"minecraft:sand",
];
Self::create_themed_palette_filtered(
"Ocean Biome",
"Cool blues and aquatic colors for underwater builds",
&ocean_blocks,
PaletteTheme::Natural,
filter,
)
}
fn generate_mountain_palette() -> Option<BlockPalette> {
Self::generate_mountain_palette_filtered(&BlockFilter::default())
}
fn generate_mountain_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let mountain_blocks = [
"minecraft:stone",
"minecraft:cobblestone",
"minecraft:andesite",
"minecraft:granite",
"minecraft:diorite",
"minecraft:gravel",
];
Self::create_themed_palette_filtered(
"Mountain Biome",
"Rocky grays and mineral tones for mountainous terrain",
&mountain_blocks,
PaletteTheme::Natural,
filter,
)
}
fn generate_nether_palette() -> Option<BlockPalette> {
Self::generate_nether_palette_filtered(&BlockFilter::default())
}
fn generate_nether_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let nether_blocks = [
"minecraft:netherrack",
"minecraft:nether_bricks",
"minecraft:blackstone",
"minecraft:crimson_planks",
"minecraft:warped_planks",
"minecraft:soul_sand",
];
Self::create_themed_palette_filtered(
"Nether Dimension",
"Dark reds, blacks, and otherworldly colors",
&nether_blocks,
PaletteTheme::Natural,
filter,
)
}
fn generate_end_palette() -> Option<BlockPalette> {
Self::generate_end_palette_filtered(&BlockFilter::default())
}
fn generate_end_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let end_blocks = [
"minecraft:end_stone",
"minecraft:purpur_block",
"minecraft:end_stone_bricks",
"minecraft:obsidian",
"minecraft:chorus_flower",
"minecraft:chorus_plant",
];
Self::create_themed_palette_filtered(
"End Dimension",
"Pale yellows, purples, and ethereal tones",
&end_blocks,
PaletteTheme::Natural,
filter,
)
}
fn generate_medieval_palette() -> Option<BlockPalette> {
Self::generate_medieval_palette_filtered(&BlockFilter::default())
}
fn generate_medieval_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let medieval_blocks = [
"minecraft:cobblestone",
"minecraft:oak_planks",
"minecraft:stone_bricks",
"minecraft:dark_oak_planks",
"minecraft:mossy_cobblestone",
"minecraft:oak_log",
];
Self::create_themed_palette_filtered(
"Medieval Architecture",
"Traditional building materials for castles and medieval structures",
&medieval_blocks,
PaletteTheme::Architectural,
filter,
)
}
fn generate_modern_palette() -> Option<BlockPalette> {
Self::generate_modern_palette_filtered(&BlockFilter::default())
}
fn generate_modern_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let modern_blocks = [
"minecraft:white_concrete",
"minecraft:light_gray_concrete",
"minecraft:glass",
"minecraft:iron_block",
"minecraft:quartz_block",
"minecraft:black_concrete",
];
Self::create_themed_palette_filtered(
"Modern Architecture",
"Clean lines and contemporary materials for modern builds",
&modern_blocks,
PaletteTheme::Architectural,
filter,
)
}
fn generate_rustic_palette() -> Option<BlockPalette> {
Self::generate_rustic_palette_filtered(&BlockFilter::default())
}
fn generate_rustic_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let rustic_blocks = [
"minecraft:stripped_oak_log",
"minecraft:cobblestone",
"minecraft:coarse_dirt",
"minecraft:hay_bale",
"minecraft:oak_fence",
"minecraft:stone",
];
Self::create_themed_palette_filtered(
"Rustic Style",
"Natural materials for farmhouses and country builds",
&rustic_blocks,
PaletteTheme::Architectural,
filter,
)
}
fn generate_industrial_palette() -> Option<BlockPalette> {
Self::generate_industrial_palette_filtered(&BlockFilter::default())
}
fn generate_industrial_palette_filtered(filter: &BlockFilter) -> Option<BlockPalette> {
let industrial_blocks = [
"minecraft:iron_block",
"minecraft:gray_concrete",
"minecraft:observer",
"minecraft:anvil",
"minecraft:cauldron",
"minecraft:redstone_block",
];
Self::create_themed_palette_filtered(
"Industrial Style",
"Metallic and mechanical blocks for factories and tech builds",
&industrial_blocks,
PaletteTheme::Architectural,
filter,
)
}
fn create_themed_palette(
name: &str,
description: &str,
block_ids: &[&str],
theme: PaletteTheme,
) -> Option<BlockPalette> {
Self::create_themed_palette_filtered(
name,
description,
block_ids,
theme,
&BlockFilter::default(),
)
}
fn create_themed_palette_filtered(
name: &str,
description: &str,
block_ids: &[&str],
theme: PaletteTheme,
filter: &BlockFilter,
) -> Option<BlockPalette> {
let mut blocks = Vec::new();
for (i, block_id) in block_ids.iter().enumerate() {
if let Some(block) = BLOCKS.get(*block_id) {
if !filter.allows_block(block) {
continue;
}
let role = match i {
0 => BlockRole::Primary,
1 => BlockRole::Secondary,
_ if i == block_ids.len() - 1 => BlockRole::Accent,
_ => BlockRole::Transition,
};
let color = block
.extras
.color
.map(|c| c.to_extended())
.unwrap_or_else(|| ExtendedColorData::from_rgb(128, 128, 128));
let usage_notes = Self::generate_usage_notes(block, &role);
blocks.push(BlockRecommendation {
block: *block,
color,
role,
usage_notes,
});
}
}
if blocks.is_empty() {
return None;
}
Some(BlockPalette {
name: name.to_string(),
description: description.to_string(),
blocks,
theme,
})
}
pub fn find_blocks_by_color_range(
target_color: ExtendedColorData,
tolerance: f32,
max_blocks: usize,
) -> Vec<&'static BlockFacts> {
let mut candidates: Vec<_> = BLOCKS
.values()
.filter_map(|block| {
block.extras.color.map(|color| {
let distance = color.to_extended().distance_oklab(&target_color);
(*block, distance)
})
})
.filter(|(_, distance)| *distance <= tolerance)
.collect();
candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
candidates
.into_iter()
.take(max_blocks)
.map(|(block, _)| block)
.collect()
}
pub fn get_natural_themes() -> Vec<&'static str> {
vec!["forest", "desert", "ocean", "mountain", "nether", "end"]
}
pub fn get_architectural_styles() -> Vec<&'static str> {
vec!["medieval", "modern", "rustic", "industrial"]
}
}
impl BlockPalette {
pub fn to_text_list(&self) -> String {
let mut output = String::new();
output.push_str(&format!("# {}\n", self.name));
output.push_str(&format!("{}\n\n", self.description));
for recommendation in &self.blocks {
output.push_str(&format!(
"- {} ({}): {}\n",
Self::format_block_name(recommendation.block.id()),
recommendation.color.hex_string(),
recommendation.usage_notes
));
}
output
}
pub fn to_json(&self) -> String {
serde_json::json!({
"name": self.name,
"description": self.description,
"theme": format!("{:?}", self.theme),
"blocks": self.blocks.iter().map(|rec| {
serde_json::json!({
"id": rec.block.id(),
"name": Self::format_block_name(rec.block.id()),
"color": rec.color.hex_string(),
"role": format!("{:?}", rec.role),
"usage": rec.usage_notes
})
}).collect::<Vec<_>>()
})
.to_string()
}
fn format_block_name(id: &str) -> String {
id.strip_prefix("minecraft:")
.unwrap_or(id)
.replace('_', " ")
.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
}