use proc_macro2::TokenStream; use quote::quote; use serde_json::Value; use std::fs; /// Reads `placed_feature.json` and emits the complete `build_placed_features()` function `TokenStream`. pub fn build() -> TokenStream { let json_content = fs::read_to_string("../assets/placed_feature.json") .expect("Failed to read placed_feature.json"); let json: Value = serde_json::from_str(&json_content).expect("Failed to parse placed_feature.json"); let entries: Vec = json .as_object() .unwrap() .iter() .map(|(name, value)| { let pf = value_to_placed_feature(value); quote! { map.insert(#name.to_string(), #pf); } }) .collect(); quote! { #[allow(clippy::all, unused_imports, dead_code)] fn build_placed_features() -> std::collections::HashMap { use crate::generation::block_predicate::{ AllOfBlockPredicate, AnyOfBlockPredicate, BlockPredicate, HasSturdyFacePredicate, InsideWorldBoundsBlockPredicate, MatchingBlockTagPredicate, MatchingBlocksBlockPredicate, MatchingBlocksWrapper, MatchingFluidsBlockPredicate, NotBlockPredicate, OffsetBlocksBlockPredicate, ReplaceableBlockPredicate, SolidBlockPredicate, WouldSurviveBlockPredicate, }; use crate::generation::height_provider::{ HeightProvider, TrapezoidHeightProvider, UniformHeightProvider, VeryBiasedToBottomHeightProvider, }; use pumpkin_util::y_offset::{AboveBottom, Absolute, BelowTop, YOffset}; use pumpkin_util::math::int_provider::{ BiasedToBottomIntProvider, ClampedIntProvider, ClampedNormalIntProvider, ConstantIntProvider, IntProvider, NormalIntProvider, UniformIntProvider, WeightedEntry, WeightedListIntProvider, }; use crate::block::BlockStateCodec; use pumpkin_data::{Block, BlockDirection}; use pumpkin_util::math::vector3::Vector3; use pumpkin_util::HeightMap; let mut map = std::collections::HashMap::new(); #(#entries)* map } } } /// Converts a single placed-feature JSON value into a `PlacedFeature` token stream. /// /// # Arguments /// – `v` – the JSON object for the entry, expected to contain `"feature"` and `"placement"` fields. fn value_to_placed_feature(v: &Value) -> TokenStream { let feature = value_to_feature(&v["feature"]); let placement_arr = v["placement"] .as_array() .map(|a| a.as_slice()) .unwrap_or(&[]); let placement: Vec = placement_arr .iter() .map(value_to_placement_modifier) .collect(); quote! { PlacedFeature { feature: #feature, placement: vec![#(#placement),*], } } } /// Converts a JSON `"feature"` value into a `Feature` token stream. /// /// # Arguments /// - `v` – Either a string (named reference) or object (inline configured feature). /// /// # Returns /// `Feature::Named(…)` for string values or `Feature::Inlined(…)` for object values. fn value_to_feature(v: &Value) -> TokenStream { match v { Value::String(s) => { let name = s.strip_prefix("minecraft:").unwrap_or(s); quote! { Feature::Named(#name.to_string()) } } Value::Object(_) => { let cf = value_to_inline_configured_feature(v); quote! { Feature::Inlined(Box::new(#cf)) } } _ => quote! { Feature::Named(String::new()) }, } } /// Converts a single placement-modifier JSON object into its corresponding `PlacementModifier` token stream. /// /// # Arguments /// - `v` – JSON object with a `"type"` discriminant and modifier-specific fields. /// /// # Returns /// A `PlacementModifier` variant token stream, or `compile_error!` for unknown types. fn value_to_placement_modifier(v: &Value) -> TokenStream { let type_str = v["type"].as_str().unwrap_or(""); match type_str { "minecraft:biome" => quote! { PlacementModifier::Biome(BiomePlacementModifier) }, "minecraft:in_square" => quote! { PlacementModifier::InSquare(SquarePlacementModifier) }, "minecraft:fixed_placement" => { let positions = v["positions"] .as_array() .map(|arr| { arr.iter().map(|p| { let coords = p.as_array().unwrap(); let x = coords[0].as_i64().unwrap_or(0) as i32; let y = coords[1].as_i64().unwrap_or(0) as i32; let z = coords[2].as_i64().unwrap_or(0) as i32; quote! { BlockPos::new(#x, #y, #z) } }).collect::>() }) .unwrap_or_default(); quote! { PlacementModifier::FixedPlacement(vec![#(#positions),*]) } }, "minecraft:heightmap" => { let heightmap = value_to_height_map(v["heightmap"].as_str().unwrap_or("MOTION_BLOCKING")); quote! { PlacementModifier::Heightmap(HeightmapPlacementModifier { heightmap: #heightmap, }) } } "minecraft:height_range" => { let height = value_to_height_provider(&v["height"]); quote! { PlacementModifier::HeightRange(HeightRangePlacementModifier { height: #height, }) } } "minecraft:count" => { let count = value_to_int_provider(&v["count"]); quote! { PlacementModifier::Count(CountPlacementModifier { count: #count, }) } } "minecraft:count_on_every_layer" => { let count = value_to_int_provider(&v["count"]); quote! { PlacementModifier::CountOnEveryLayer(CountOnEveryLayerPlacementModifier { count: #count, }) } } "minecraft:rarity_filter" => { let chance = v["chance"].as_u64().unwrap_or(1) as u32; quote! { PlacementModifier::RarityFilter(RarityFilterPlacementModifier { chance: #chance, }) } } "minecraft:block_predicate_filter" => { let predicate = value_to_block_predicate(&v["predicate"]); quote! { PlacementModifier::BlockPredicateFilter(BlockFilterPlacementModifier { predicate: #predicate, }) } } "minecraft:surface_relative_threshold_filter" => { let heightmap = value_to_height_map(v["heightmap"].as_str().unwrap_or("MOTION_BLOCKING")); let min_inc = value_to_option_i32(&v["min_inclusive"]); let max_inc = value_to_option_i32(&v["max_inclusive"]); quote! { PlacementModifier::SurfaceRelativeThresholdFilter(SurfaceThresholdFilterPlacementModifier { heightmap: #heightmap, min_inclusive: #min_inc, max_inclusive: #max_inc, }) } } "minecraft:surface_water_depth_filter" => { let depth = v["max_water_depth"].as_i64().unwrap_or(0) as i32; quote! { PlacementModifier::SurfaceWaterDepthFilter(SurfaceWaterDepthFilterPlacementModifier { max_water_depth: #depth, }) } } "minecraft:noise_based_count" => { let ratio = v["noise_to_count_ratio"].as_i64().unwrap_or(0) as i32; let factor = v["noise_factor"].as_f64().unwrap_or(1.0); let offset = v["noise_offset"].as_f64().unwrap_or(0.0); quote! { PlacementModifier::NoiseBasedCount(NoiseBasedCountPlacementModifier { noise_to_count_ratio: #ratio, noise_factor: #factor, noise_offset: #offset, }) } } "minecraft:noise_threshold_count" => { let level = v["noise_level"].as_f64().unwrap_or(0.0); let below = v["below_noise"].as_i64().unwrap_or(0) as i32; let above = v["above_noise"].as_i64().unwrap_or(0) as i32; quote! { PlacementModifier::NoiseThresholdCount(NoiseThresholdCountPlacementModifier { noise_level: #level, below_noise: #below, above_noise: #above, }) } } "minecraft:environment_scan" => { let dir = value_to_block_direction(v["direction_of_search"].as_str().unwrap_or("down")); let target = value_to_block_predicate(&v["target_condition"]); let allowed = if v["allowed_search_condition"].is_null() || v["allowed_search_condition"].is_object() { if v["allowed_search_condition"].is_object() { let p = value_to_block_predicate(&v["allowed_search_condition"]); quote! { Some(#p) } } else { quote! { None } } } else { quote! { None } }; let steps = v["max_steps"].as_i64().unwrap_or(1) as i32; quote! { PlacementModifier::EnvironmentScan(EnvironmentScanPlacementModifier { direction_of_search: #dir, target_condition: #target, allowed_search_condition: #allowed, max_steps: #steps, }) } } "minecraft:random_offset" => { let xz = value_to_int_provider(&v["xz_spread"]); let y = value_to_int_provider(&v["y_spread"]); quote! { PlacementModifier::RandomOffset(RandomOffsetPlacementModifier { xz_spread: #xz, y_spread: #y, }) } } other => { let msg = format!("unknown placement modifier: {other}"); quote! { compile_error!(#msg) } } } } /// Converts a block-predicate JSON object into its corresponding `BlockPredicate` token stream. /// /// # Arguments /// - `v` – JSON object with a `"type"` discriminant and predicate-specific fields. /// /// # Returns /// A `BlockPredicate` variant token stream, or `compile_error!` for unknown types. pub fn value_to_block_predicate(v: &Value) -> TokenStream { // Handle bare string values: "#minecraft:some_tag" is a block-tag predicate, // "true" (or any non-# string) is AlwaysTrue. if let Some(s) = v.as_str() { if let Some(tag) = s.strip_prefix('#') { return quote! { BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate { offset: OffsetBlocksBlockPredicate { offset: None }, tag: #tag.to_string(), }) }; } return quote! { BlockPredicate::AlwaysTrue }; } let type_str = v["type"].as_str().unwrap_or(""); match type_str { "minecraft:true" | "" => quote! { BlockPredicate::AlwaysTrue }, "minecraft:matching_blocks" => { let offset = value_to_offset_predicate(&v["offset"]); let blocks = value_to_matching_blocks_wrapper(&v["blocks"]); quote! { BlockPredicate::MatchingBlocks(MatchingBlocksBlockPredicate { offset: #offset, blocks: #blocks, }) } } "minecraft:matching_block_tag" => { let offset = value_to_offset_predicate(&v["offset"]); let tag = v["tag"].as_str().unwrap_or(""); quote! { BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate { offset: #offset, tag: #tag.to_string(), }) } } "minecraft:matching_fluids" => { let offset = value_to_offset_predicate(&v["offset"]); let fluids = value_to_matching_blocks_wrapper(&v["fluids"]); quote! { BlockPredicate::MatchingFluids(MatchingFluidsBlockPredicate { offset: #offset, fluids: #fluids, }) } } "minecraft:has_sturdy_face" => { let offset = value_to_offset_predicate(&v["offset"]); let dir = value_to_block_direction(v["direction"].as_str().unwrap_or("down")); quote! { BlockPredicate::HasSturdyFace(HasSturdyFacePredicate { offset: #offset, direction: #dir, }) } } "minecraft:solid" => { let offset = value_to_offset_predicate(&v["offset"]); quote! { BlockPredicate::Solid(SolidBlockPredicate { offset: #offset }) } } "minecraft:replaceable" => { let offset = value_to_offset_predicate(&v["offset"]); quote! { BlockPredicate::Replaceable(ReplaceableBlockPredicate { offset: #offset }) } } "minecraft:would_survive" => { let offset = value_to_offset_predicate(&v["offset"]); let state = value_to_block_state_codec(&v["state"]); quote! { BlockPredicate::WouldSurvive(WouldSurviveBlockPredicate { offset: #offset, state: #state, }) } } "minecraft:inside_world_bounds" => { let offset_v = &v["offset"]; let offset = if offset_v.is_array() { let x = offset_v[0].as_i64().unwrap_or(0) as i32; let y = offset_v[1].as_i64().unwrap_or(0) as i32; let z = offset_v[2].as_i64().unwrap_or(0) as i32; quote! { Vector3::new(#x, #y, #z) } } else { quote! { Vector3::new(0, 0, 0) } }; quote! { BlockPredicate::InsideWorldBounds(InsideWorldBoundsBlockPredicate { offset: #offset, }) } } "minecraft:any_of" => { let predicates: Vec = v["predicates"] .as_array() .map(|a| a.iter().map(value_to_block_predicate).collect()) .unwrap_or_default(); quote! { BlockPredicate::AnyOf(AnyOfBlockPredicate { predicates: vec![#(#predicates),*], }) } } "minecraft:all_of" => { let predicates: Vec = v["predicates"] .as_array() .map(|a| a.iter().map(value_to_block_predicate).collect()) .unwrap_or_default(); quote! { BlockPredicate::AllOf(AllOfBlockPredicate { predicates: vec![#(#predicates),*], }) } } "minecraft:not" => { let inner = value_to_block_predicate(&v["predicate"]); quote! { BlockPredicate::Not(NotBlockPredicate { predicate: Box::new(#inner), }) } } other => { let msg = format!("unknown block predicate: {other}"); quote! { compile_error!(#msg) } } } } /// Converts a height-provider JSON object into its corresponding `HeightProvider` token stream. /// /// # Arguments /// - `v` – JSON object with a `"type"` discriminant and provider-specific fields. /// /// # Returns /// A `HeightProvider` variant token stream, or `compile_error!` for unknown types. pub fn value_to_height_provider(v: &Value) -> TokenStream { let type_str = v["type"].as_str().unwrap_or(""); match type_str { "minecraft:uniform" => { let min = value_to_y_offset(&v["min_inclusive"]); let max = value_to_y_offset(&v["max_inclusive"]); quote! { HeightProvider::Uniform(UniformHeightProvider { min_inclusive: #min, max_inclusive: #max, }) } } "minecraft:trapezoid" => { let min = value_to_y_offset(&v["min_inclusive"]); let max = value_to_y_offset(&v["max_inclusive"]); let plateau = value_to_option_i32(&v["plateau"]); quote! { HeightProvider::Trapezoid(TrapezoidHeightProvider { min_inclusive: #min, max_inclusive: #max, plateau: #plateau, }) } } "minecraft:very_biased_to_bottom" => { let min = value_to_y_offset(&v["min_inclusive"]); let max = value_to_y_offset(&v["max_inclusive"]); let inner = if v["inner"].is_null() || !v["inner"].is_number() { quote! { None } } else { let n = v["inner"].as_u64().unwrap_or(1) as u32; quote! { std::num::NonZeroU32::new(#n) } }; quote! { HeightProvider::VeryBiasedToBottom(VeryBiasedToBottomHeightProvider { min_inclusive: #min, max_inclusive: #max, inner: #inner, }) } } "minecraft:constant" => { // constant height provider wraps a y_offset let y = value_to_y_offset(&v["value"]); // Map to uniform with same min/max quote! { HeightProvider::Uniform(UniformHeightProvider { min_inclusive: #y, max_inclusive: #y, }) } } other => { let msg = format!("unknown height provider: {other}"); quote! { compile_error!(#msg) } } } } /// Converts a Y-offset JSON object into its corresponding `YOffset` token stream. /// /// # Arguments /// - `v` – JSON object with one of the keys `"absolute"`, `"above_bottom"`, or `"below_top"`. /// /// # Returns /// A `YOffset` variant token stream; defaults to `YOffset::Absolute(0)` when unrecognized. pub fn value_to_y_offset(v: &Value) -> TokenStream { if let Some(abs) = v.get("absolute") { let val = abs.as_i64().unwrap_or(0) as i16; quote! { YOffset::Absolute(Absolute { absolute: #val }) } } else if let Some(ab) = v.get("above_bottom") { let val = ab.as_i64().unwrap_or(0) as i8; quote! { YOffset::AboveBottom(AboveBottom { above_bottom: #val }) } } else if let Some(bt) = v.get("below_top") { let val = bt.as_i64().unwrap_or(0) as i8; quote! { YOffset::BelowTop(BelowTop { below_top: #val }) } } else { quote! { YOffset::Absolute(Absolute { absolute: 0 }) } } } /// Converts an integer-provider JSON value into its corresponding `IntProvider` token stream. /// /// # Arguments /// - `v` – Either a bare JSON number (constant) or an object with a `"type"` discriminant. /// /// # Returns /// An `IntProvider` variant token stream; defaults to `IntProvider::Constant(0)` when unrecognized. pub fn value_to_int_provider(v: &Value) -> TokenStream { match v { Value::Number(n) => { let val = n.as_i64().unwrap_or(0) as i32; quote! { IntProvider::Constant(#val) } } Value::Object(_) => { let type_str = v["type"].as_str().unwrap_or(""); match type_str { "minecraft:constant" => { let val = v["value"].as_i64().unwrap_or(0) as i32; quote! { IntProvider::Object(NormalIntProvider::Constant(ConstantIntProvider { value: #val })) } } "minecraft:uniform" => { let min = v["min_inclusive"].as_i64().unwrap_or(0) as i32; let max = v["max_inclusive"].as_i64().unwrap_or(0) as i32; quote! { IntProvider::Object(NormalIntProvider::Uniform(UniformIntProvider { min_inclusive: #min, max_inclusive: #max })) } } "minecraft:biased_to_bottom" => { let min = v["min_inclusive"].as_i64().unwrap_or(0) as i32; let max = v["max_inclusive"].as_i64().unwrap_or(0) as i32; quote! { IntProvider::Object(NormalIntProvider::BiasedToBottom(BiasedToBottomIntProvider { min_inclusive: #min, max_inclusive: #max })) } } "minecraft:clamped" => { let min = v["min_inclusive"].as_i64().unwrap_or(0) as i32; let max = v["max_inclusive"].as_i64().unwrap_or(0) as i32; let src = value_to_int_provider(&v["source"]); quote! { IntProvider::Object(NormalIntProvider::Clamped(ClampedIntProvider { source: Box::new(#src), min_inclusive: #min, max_inclusive: #max })) } } "minecraft:clamped_normal" => { let mean = v["mean"].as_f64().unwrap_or(0.0) as f32; let dev = v["deviation"].as_f64().unwrap_or(1.0) as f32; let min = v["min_inclusive"].as_i64().unwrap_or(0) as i32; let max = v["max_inclusive"].as_i64().unwrap_or(0) as i32; quote! { IntProvider::Object(NormalIntProvider::ClampedNormal(ClampedNormalIntProvider { mean: #mean, deviation: #dev, min_inclusive: #min, max_inclusive: #max })) } } "minecraft:weighted_list" => { let entries: Vec = v["distribution"] .as_array() .map(|arr| { arr.iter() .map(|e| { let data = value_to_int_provider(&e["data"]); let weight = e["weight"].as_i64().unwrap_or(1) as i32; quote! { WeightedEntry { data: #data, weight: #weight } } }) .collect() }) .unwrap_or_default(); quote! { IntProvider::Object(NormalIntProvider::WeightedList(WeightedListIntProvider { distribution: vec![#(#entries),*] })) } } _ => { let val = v["value"].as_i64().unwrap_or(0) as i32; quote! { IntProvider::Constant(#val) } } } } _ => quote! { IntProvider::Constant(0) }, } } /// Converts a SCREAMING_SNAKE_CASE heightmap name into its `HeightMap` token stream. /// /// # Arguments /// - `s` – Heightmap name string (e.g. `"MOTION_BLOCKING"`). /// /// # Returns /// A `HeightMap` variant token stream; defaults to `HeightMap::MotionBlocking` when unrecognized. pub fn value_to_height_map(s: &str) -> TokenStream { match s { "WORLD_SURFACE_WG" => quote! { HeightMap::WorldSurfaceWg }, "WORLD_SURFACE" => quote! { HeightMap::WorldSurface }, "OCEAN_FLOOR_WG" => quote! { HeightMap::OceanFloorWg }, "OCEAN_FLOOR" => quote! { HeightMap::OceanFloor }, "MOTION_BLOCKING" => quote! { HeightMap::MotionBlocking }, "MOTION_BLOCKING_NO_LEAVES" => quote! { HeightMap::MotionBlockingNoLeaves }, _ => quote! { HeightMap::MotionBlocking }, } } /// Converts a lowercase direction string into its `BlockDirection` token stream. /// /// # Arguments /// - `s` – Direction name (e.g. `"down"`, `"up"`, `"north"`, etc.). /// /// # Returns /// A `BlockDirection` variant token stream; defaults to `BlockDirection::Down` when unrecognized. pub fn value_to_block_direction(s: &str) -> TokenStream { match s.to_lowercase().as_str() { "down" => quote! { BlockDirection::Down }, "up" => quote! { BlockDirection::Up }, "north" => quote! { BlockDirection::North }, "south" => quote! { BlockDirection::South }, "west" => quote! { BlockDirection::West }, "east" => quote! { BlockDirection::East }, _ => quote! { BlockDirection::Down }, } } /// Converts an optional offset JSON value into an `OffsetBlocksBlockPredicate` token stream. /// /// # Arguments /// - `v` – Either `null`/empty object (no offset) or a 3-element integer array `[x, y, z]`. /// /// # Returns /// An `OffsetBlocksBlockPredicate` with `offset: None` or `offset: Some(Vector3::new(x, y, z))`. fn value_to_offset_predicate(v: &Value) -> TokenStream { if v.is_null() || v.is_object() && v.as_object().is_none_or(|o| o.is_empty()) { quote! { OffsetBlocksBlockPredicate { offset: None } } } else if v.is_array() { let x = v[0].as_i64().unwrap_or(0) as i32; let y = v[1].as_i64().unwrap_or(0) as i32; let z = v[2].as_i64().unwrap_or(0) as i32; quote! { OffsetBlocksBlockPredicate { offset: Some(Vector3::new(#x, #y, #z)) } } } else { quote! { OffsetBlocksBlockPredicate { offset: None } } } } /// Converts a blocks/fluids JSON value into a `MatchingBlocksWrapper` token stream. /// /// # Arguments /// - `v` – Either a single string (one block/fluid name) or an array of strings. /// /// # Returns /// `MatchingBlocksWrapper::Single(…)` or `MatchingBlocksWrapper::Multiple(…)`. fn value_to_matching_blocks_wrapper(v: &Value) -> TokenStream { match v { Value::String(s) => { quote! { MatchingBlocksWrapper::Single(#s.to_string()) } } Value::Array(arr) => { let items: Vec = arr .iter() .filter_map(|s| s.as_str().map(|s| quote! { #s.to_string() })) .collect(); quote! { MatchingBlocksWrapper::Multiple(vec![#(#items),*]) } } _ => quote! { MatchingBlocksWrapper::Single(String::new()) }, } } /// Converts a block-state JSON object into a `BlockStateCodec` token stream. /// /// # Arguments /// - `v` – JSON object with a `"Name"` field and optional `"Properties"` map. /// /// # Returns /// A `BlockStateCodec` token stream referencing the corresponding `Block` constant and optional property map. pub fn value_to_block_state_codec(v: &Value) -> TokenStream { let name = v["Name"].as_str().unwrap_or("minecraft:air"); let name_stripped = name.strip_prefix("minecraft:").unwrap_or(name); let block_ident = quote::format_ident!("{}", name_stripped.to_uppercase().replace([':', '-'], "_")); if let Some(props) = v["Properties"].as_object() { let keys: Vec<&str> = props.keys().map(|k| k.as_str()).collect(); let vals: Vec<&str> = props.values().filter_map(|v| v.as_str()).collect(); quote! { { let mut props = std::collections::HashMap::new(); #(props.insert(#keys.to_string(), #vals.to_string());)* BlockStateCodec { name: &pumpkin_data::Block::#block_ident, properties: Some(props), } } } } else { quote! { BlockStateCodec { name: &pumpkin_data::Block::#block_ident, properties: None, } } } } pub fn value_to_block_state(v: &Value) -> TokenStream { let name = v["Name"].as_str().unwrap_or("minecraft:air"); let name_stripped = name.strip_prefix("minecraft:").unwrap_or(name); let block_ident = quote::format_ident!("{}", name_stripped.to_uppercase().replace([':', '-'], "_")); if let Some(props) = v["Properties"].as_object() { let keys: Vec<&str> = props.keys().map(|k| k.as_str()).collect(); let vals: Vec<&str> = props.values().filter_map(|v| v.as_str()).collect(); quote! { { let mut props = std::collections::HashMap::new(); #(props.insert(#keys.to_string(), #vals.to_string());)* BlockStateCodec { name: &pumpkin_data::Block::#block_ident, properties: Some(props), }.get_state() } } } else { quote! { pumpkin_data::Block::#block_ident.default_state } } } /// Converts a nullable JSON integer into an `Option` token stream. /// /// # Arguments /// - `v` – A JSON number or `null`. /// /// # Returns /// `Some(n)` if `v` is a valid integer, or `None` otherwise. fn value_to_option_i32(v: &Value) -> TokenStream { if v.is_null() { quote! { None } } else if let Some(n) = v.as_i64() { let val = n as i32; quote! { Some(#val) } } else { quote! { None } } } /// Stub for configured feature inline in placed feature fn value_to_inline_configured_feature(_v: &Value) -> TokenStream { quote! { crate::generation::feature::configured_features::ConfiguredFeature::NoOp } }