From 7d2758a5d51a0394b1796a7cfb0e0c895b2cd04d Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:52:30 +0200 Subject: [PATCH 01/54] add infinite height --- steel-worldgen/src/noise/noise_chunk.rs | 26 ++++++------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/steel-worldgen/src/noise/noise_chunk.rs b/steel-worldgen/src/noise/noise_chunk.rs index ff33202e0399..34d4148e9c0b 100644 --- a/steel-worldgen/src/noise/noise_chunk.rs +++ b/steel-worldgen/src/noise/noise_chunk.rs @@ -21,10 +21,6 @@ use crate::noise::Beardifier; /// Overworld uses 8 (1 terrain + 4 noodle caves + 3 vein channels), nether/end use 1. const MAX_INTERP: usize = 16; -/// Maximum slice length (`z_corners` * `corners_y`) across all dimensions. -/// Overworld: (16/4+1) * (384/8+1) = 5 * 49 = 245. Rounded up for headroom. -const MAX_SLICE_LEN: usize = 256; - /// Stores density values at cell corners for a single chunk and provides /// trilinear interpolation between corners for block-level resolution. /// @@ -47,7 +43,7 @@ pub struct NoiseChunk { /// the slice-fill phase can run in parallel: each `cx` boundary's noise /// tree evaluation is independent. The per-block trilerp loop then /// indexes `slices[cx]` and `slices[cx + 1]` sequentially. - slices: Vec>, + slices: Vec>, /// Number of active interpolation channels. interp_count: usize, /// Number of Y corners per Z column (`cell_count_y` + 1). @@ -98,10 +94,6 @@ impl NoiseChunk { let slice_len = z_corners * corners_y; let interp_count = N::interpolated_count(); - assert!( - slice_len <= MAX_SLICE_LEN, - "slice_len {slice_len} exceeds MAX_SLICE_LEN {MAX_SLICE_LEN}" - ); assert!( interp_count <= MAX_INTERP, "interp_count {interp_count} exceeds MAX_INTERP {MAX_INTERP}" @@ -112,16 +104,10 @@ impl NoiseChunk { .collect(); let n_slices = cell_count_xz + 1; + let slice_storage_len = MAX_INTERP * slice_len; let mut slices = Vec::with_capacity(n_slices); for _ in 0..n_slices { - // The boxed fixed-size array keeps the `[f64; N]` type that the SIMD - // `fill` path and its `get_unchecked` SAFETY proofs rely on. This is a - // per-chunk constructor, not a hot path, so the stack temporary is fine. - #[expect( - clippy::large_stack_arrays, - reason = "fixed-size boxed array keeps the [f64; N] type the SIMD fill path relies on; cold per-chunk constructor" - )] - slices.push(Box::new([0.0; MAX_INTERP * MAX_SLICE_LEN])); + slices.push(vec![0.0; slice_storage_len].into_boxed_slice()); } Self { @@ -146,7 +132,7 @@ impl NoiseChunk { reason = "slice filling needs the precomputed geometry and per-thread cache" )] fn fill_slice_into( - slice: &mut [f64; MAX_INTERP * MAX_SLICE_LEN], + slice: &mut [f64], cell_x: i32, block_ys: &[i32], blended_column: &mut [f64], @@ -328,8 +314,8 @@ impl NoiseChunk { // result is bit-identical to vanilla. // // SAFETY: max index = (z1_base + cell_y_idx + 1) * MAX_INTERP + (ch_batch+3) - // ≤ ((cell_count_xz+1)*corners_y - 1) * MAX_INTERP + MAX_INTERP - 1 - // < MAX_SLICE_LEN * MAX_INTERP + // ≤ (slice_len - 1) * MAX_INTERP + MAX_INTERP - 1 + // < slice.len() let i0_base = (z0_base + cell_y_idx) * MAX_INTERP; let i1_base = (z1_base + cell_y_idx) * MAX_INTERP; let i0_next = i0_base + MAX_INTERP; From 734dfdef9c72cb892e0cf9260dadd3f7bb7ae6c6 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:54:39 +0200 Subject: [PATCH 02/54] make salt being i64 --- steel-registry/src/structure_set.rs | 4 ++-- steel-worldgen/src/structure/placement.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/steel-registry/src/structure_set.rs b/steel-registry/src/structure_set.rs index 9206f3f22495..e3f55c372065 100644 --- a/steel-registry/src/structure_set.rs +++ b/steel-registry/src/structure_set.rs @@ -40,7 +40,7 @@ pub enum PlacementData { /// Spread type: `"linear"` or `"triangular"`. spread_type: SpreadTypeData, /// Unique seed modifier. - salt: i32, + salt: i64, /// Generation probability (0.0–1.0). Default: 1.0. frequency: f32, /// Frequency reduction method name. Default: `"default"`. @@ -61,7 +61,7 @@ pub enum PlacementData { /// Biomes that ring positions prefer to snap to. preferred_biomes: Vec, /// Unique seed modifier. - salt: i32, + salt: i64, /// Generation probability. Default: 1.0. frequency: f32, /// Frequency reduction method name. diff --git a/steel-worldgen/src/structure/placement.rs b/steel-worldgen/src/structure/placement.rs index eb259b134fcc..655384f8dce9 100644 --- a/steel-worldgen/src/structure/placement.rs +++ b/steel-worldgen/src/structure/placement.rs @@ -363,7 +363,7 @@ fn convert_structure_set(data: StructureSetData) -> (Identifier, StructureSet) { exclusion_zone, locate_offset, } => StructurePlacement { - salt, + salt: salt as i32, frequency, frequency_reduction_method: frequency_reduction_method.into(), exclusion_zone: exclusion_zone.map(|ez| ExclusionZone { @@ -387,7 +387,7 @@ fn convert_structure_set(data: StructureSetData) -> (Identifier, StructureSet) { frequency_reduction_method, locate_offset, } => StructurePlacement { - salt, + salt: salt as i32, frequency, frequency_reduction_method: frequency_reduction_method.into(), exclusion_zone: None, From 58b1443f843006df276222f3114abcc7ec7416e3 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:55:00 +0200 Subject: [PATCH 03/54] add alias --- .../src/structure_processor/data.rs | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/steel-registry/src/structure_processor/data.rs b/steel-registry/src/structure_processor/data.rs index dd2368ad0876..1feaa6419bdb 100644 --- a/steel-registry/src/structure_processor/data.rs +++ b/steel-registry/src/structure_processor/data.rs @@ -20,7 +20,7 @@ pub struct StructureProcessorListData { #[serde(tag = "processor_type")] pub enum StructureProcessorKind { /// Randomly drops input blocks. - #[serde(rename = "minecraft:block_rot")] + #[serde(rename = "minecraft:block_rot", alias = "block_rot")] BlockRot { /// Optional tag restricting which blocks may be dropped. #[serde(default, deserialize_with = "deserialize_optional_tag_identifier")] @@ -29,32 +29,64 @@ pub enum StructureProcessorKind { integrity: f32, }, /// Prevents replacement of protected world blocks. - #[serde(rename = "minecraft:protected_blocks")] + #[serde(rename = "minecraft:protected_blocks", alias = "protected_blocks")] ProtectedBlocks { /// Vanilla field name is `value`; it stores the cannot-replace tag. #[serde(rename = "value", deserialize_with = "deserialize_tag_identifier")] cannot_replace: Identifier, }, /// Applies the first matching rule. - #[serde(rename = "minecraft:rule")] + #[serde(rename = "minecraft:rule", alias = "rule")] Rule { rules: Vec }, /// Ages stone/obsidian structure blocks, used by ruined portals. - #[serde(rename = "minecraft:block_age")] + #[serde(rename = "minecraft:block_age", alias = "block_age")] BlockAge { mossiness: f32 }, /// Keeps non-full structure blocks submerged in existing lava. - #[serde(rename = "minecraft:lava_submerged_block")] + #[serde(rename = "minecraft:lava_submerged_block", alias = "lava_submerged_block")] LavaSubmergedBlock, /// Replaces stone ruin blocks with blackstone variants. - #[serde(rename = "minecraft:blackstone_replace")] + #[serde(rename = "minecraft:blackstone_replace", alias = "blackstone_replace")] BlackstoneReplace, + /// Skips placing blocks whose template block type is in the ignore list. + #[serde(rename = "minecraft:block_ignore", alias = "block_ignore")] + BlockIgnore { blocks: Vec }, + /// Snaps template blocks to a heightmap column plus their template-relative Y. + #[serde(rename = "minecraft:gravity", alias = "gravity")] + Gravity { + #[serde(default = "default_gravity_heightmap")] + heightmap: StructureProcessorHeightmap, + #[serde(default)] + offset: i32, + }, /// Delegates to another processor but caps successful modifications. - #[serde(rename = "minecraft:capped")] + #[serde(rename = "minecraft:capped", alias = "capped")] Capped { delegate: Box, limit: IntProvider, }, } +/// Heightmap types referenced by structure processors. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub enum StructureProcessorHeightmap { + #[serde(rename = "WORLD_SURFACE")] + WorldSurface, + #[serde(rename = "MOTION_BLOCKING")] + MotionBlocking, + #[serde(rename = "MOTION_BLOCKING_NO_LEAVES")] + MotionBlockingNoLeaves, + #[serde(rename = "OCEAN_FLOOR")] + OceanFloor, + #[serde(rename = "WORLD_SURFACE_WG")] + WorldSurfaceWg, + #[serde(rename = "OCEAN_FLOOR_WG")] + OceanFloorWg, +} + +const fn default_gravity_heightmap() -> StructureProcessorHeightmap { + StructureProcessorHeightmap::WorldSurfaceWg +} + /// One rule inside vanilla's `RuleProcessor`. #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] @@ -72,16 +104,24 @@ pub struct ProcessorRuleData { #[derive(Debug, Clone, Deserialize)] #[serde(tag = "predicate_type")] pub enum StructureRuleTestData { - #[serde(rename = "minecraft:always_true")] + #[serde(rename = "minecraft:always_true", alias = "always_true")] AlwaysTrue, - #[serde(rename = "minecraft:block_match")] + #[serde(rename = "minecraft:block_match", alias = "block_match")] BlockMatch { block: Identifier }, - #[serde(rename = "minecraft:random_block_match")] + #[serde(rename = "minecraft:random_block_match", alias = "random_block_match")] RandomBlockMatch { block: Identifier, probability: f32 }, - #[serde(rename = "minecraft:tag_match")] + #[serde(rename = "minecraft:tag_match", alias = "tag_match")] TagMatch { tag: Identifier }, - #[serde(rename = "minecraft:blockstate_match")] + #[serde(rename = "minecraft:blockstate_match", alias = "blockstate_match")] BlockStateMatch { block_state: BlockStateData }, + #[serde( + rename = "minecraft:random_blockstate_match", + alias = "random_blockstate_match" + )] + RandomBlockStateMatch { + block_state: BlockStateData, + probability: f32, + }, } /// Position rule tests used by `RuleProcessor`. @@ -89,9 +129,12 @@ pub enum StructureRuleTestData { #[serde(tag = "predicate_type")] pub enum PosRuleTestData { #[default] - #[serde(rename = "minecraft:always_true")] + #[serde(rename = "minecraft:always_true", alias = "always_true")] AlwaysTrue, - #[serde(rename = "minecraft:axis_aligned_linear_pos")] + #[serde( + rename = "minecraft:axis_aligned_linear_pos", + alias = "axis_aligned_linear_pos" + )] AxisAlignedLinearPos { #[serde( default = "default_structure_processor_axis", From fd75d7956dd151cb09fddc6c7e5d3e1baed13050 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:57:23 +0200 Subject: [PATCH 04/54] add color also as i32 --- steel-registry/build/biomes.rs | 37 +++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/steel-registry/build/biomes.rs b/steel-registry/build/biomes.rs index e945776b0620..12a53e97779e 100644 --- a/steel-registry/build/biomes.rs +++ b/steel-registry/build/biomes.rs @@ -108,22 +108,39 @@ pub struct BiomeEffects { particle: Option, } +/// JSON color: `#RRGGBB` string or packed RGB integer (vanilla `STRING_RGB_COLOR`). +#[derive(Deserialize)] +#[serde(untagged)] +enum ColorJson { + Int(i32), + Hex(String), +} + +impl ColorJson { + fn to_i32(self) -> i32 { + match self { + ColorJson::Int(value) => value, + ColorJson::Hex(hex) => parse_hex_color(&hex), + } + } +} + #[derive(Deserialize)] struct BiomeEffectsJson { #[serde(default = "default_water_color")] - water_color: String, + water_color: ColorJson, #[serde(default)] - foliage_color: Option, + foliage_color: Option, #[serde(default)] - grass_color: Option, + grass_color: Option, #[serde(default)] - dry_foliage_color: Option, + dry_foliage_color: Option, #[serde(default)] grass_color_modifier: GrassColorModifier, } -fn default_water_color() -> String { - "#3f76e4".to_string() +fn default_water_color() -> ColorJson { + ColorJson::Hex("#3f76e4".to_string()) } impl From for BiomeEffects { @@ -131,11 +148,11 @@ impl From for BiomeEffects { BiomeEffects { fog_color: 12638463, // Default value, will be overridden from attributes sky_color: 8103167, // Default value, will be overridden from attributes - water_color: parse_hex_color(&json.water_color), + water_color: json.water_color.to_i32(), water_fog_color: 329011, // Default value, will be overridden from attributes - foliage_color: json.foliage_color.map(|s| parse_hex_color(&s)), - grass_color: json.grass_color.map(|s| parse_hex_color(&s)), - dry_foliage_color: json.dry_foliage_color.map(|s| parse_hex_color(&s)), + foliage_color: json.foliage_color.map(ColorJson::to_i32), + grass_color: json.grass_color.map(ColorJson::to_i32), + dry_foliage_color: json.dry_foliage_color.map(ColorJson::to_i32), grass_color_modifier: json.grass_color_modifier, music: None, // Will be populated from attributes ambient_sound: None, // Will be populated from attributes From ed17c34618caa0d67d2192d62a17b74196dfca8f Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sat, 4 Jul 2026 19:54:19 +0200 Subject: [PATCH 05/54] add missing StructureProcessorKind, StructureRuleTestData --- steel-core/src/worldgen/template.rs | 57 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/steel-core/src/worldgen/template.rs b/steel-core/src/worldgen/template.rs index 02abeba64d86..438decc9ccd0 100644 --- a/steel-core/src/worldgen/template.rs +++ b/steel-core/src/worldgen/template.rs @@ -21,7 +21,7 @@ use steel_registry::shared_structs::BlockStateData; use steel_registry::structure::LiquidSettingsData; use steel_registry::structure_processor::{ PosRuleTestData, ProcessorRuleData, RuleBlockEntityModifierData, StructureProcessorAxis, - StructureProcessorKind, StructureRuleTestData, + StructureProcessorHeightmap, StructureProcessorKind, StructureRuleTestData, }; use steel_registry::template_pool::Projection; use steel_registry::vanilla_block_tags::BlockTag; @@ -1316,10 +1316,54 @@ impl StructureTemplate { StructureProcessorKind::BlackstoneReplace => { Some(Self::process_blackstone_replace(registry, current)) } + StructureProcessorKind::BlockIgnore { blocks } => { + let block = Self::block_for_state(registry, current.state); + let ignored = blocks.iter().any(|block_state| { + registry + .blocks + .by_key(&block_state.name) + .is_some_and(|ignore_block| block == ignore_block) + }); + (!ignored).then_some(current) + } + StructureProcessorKind::Gravity { heightmap, offset } => { + Some(Self::process_gravity(region, original, current, *heightmap, *offset)) + } StructureProcessorKind::Capped { .. } => Some(current), } } + fn process_gravity( + region: &WorldGenRegion<'_>, + original: &ProcessedBlockInfo, + current: ProcessedBlockInfo, + heightmap: StructureProcessorHeightmap, + offset: i32, + ) -> ProcessedBlockInfo { + let heightmap_type = Self::processor_heightmap_type(heightmap); + let x = current.world_pos.x(); + let z = current.world_pos.z(); + let height = region.height_at(heightmap_type, x, z) + offset; + let new_y = height + original.template_pos.y(); + ProcessedBlockInfo { + world_pos: BlockPos::new(x, new_y, z), + ..current + } + } + + fn processor_heightmap_type(heightmap: StructureProcessorHeightmap) -> HeightmapType { + match heightmap { + StructureProcessorHeightmap::WorldSurface => HeightmapType::WorldSurface, + StructureProcessorHeightmap::MotionBlocking => HeightmapType::MotionBlocking, + StructureProcessorHeightmap::MotionBlockingNoLeaves => { + HeightmapType::MotionBlockingNoLeaves + } + StructureProcessorHeightmap::OceanFloor => HeightmapType::OceanFloor, + StructureProcessorHeightmap::WorldSurfaceWg => HeightmapType::WorldSurfaceWg, + StructureProcessorHeightmap::OceanFloorWg => HeightmapType::OceanFloorWg, + } + } + fn process_block_age( registry: &Registry, current: ProcessedBlockInfo, @@ -1884,6 +1928,17 @@ impl StructureTemplate { "structure processor block-state predicate", ) } + StructureRuleTestData::RandomBlockStateMatch { + block_state, + probability, + } => { + state + == WorldgenStateResolver::block_state_from_data( + registry, + block_state, + "structure processor random block-state predicate", + ) && random.next_f32() < *probability + } } } From 4c29555b6ea788447f4d2914a972f75623521662 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:44:30 +0200 Subject: [PATCH 06/54] allow datapack to simplify by not putting a namespace --- steel-registry/build/feature_data.rs | 105 ++++++++++++++++++-- steel-registry/build/features.rs | 66 +++++------- steel-registry/build/generator_functions.rs | 49 +++++++++ 3 files changed, 170 insertions(+), 50 deletions(-) diff --git a/steel-registry/build/feature_data.rs b/steel-registry/build/feature_data.rs index 6e1435a4e749..719858901891 100644 --- a/steel-registry/build/feature_data.rs +++ b/steel-registry/build/feature_data.rs @@ -5,6 +5,7 @@ //! data in `src/feature/data.rs` can use typed registry refs because this module //! owns the extracted JSON decoding step. +use crate::generator_functions::parse_loose_identifier; use crate::shared_structs::deserialize_tag_identifier; pub use crate::shared_structs::{BlockStateData, FluidStateData}; use serde::{Deserialize, Deserializer, de::Error as _}; @@ -14,6 +15,83 @@ use steel_utils::{ value_providers::{FloatProvider, HeightProvider, IntProvider, UniformIntProvider}, }; +fn parse_loose_identifier_de(raw: &str) -> Result { + parse_loose_identifier(raw).map_err(E::custom) +} + +fn normalize_loose_identifier(raw: &str) -> String { + parse_loose_identifier(raw) + .unwrap_or_else(|error| panic!("invalid loose feature identifier {raw}: {error}")) + .to_string() +} + +fn normalize_loose_identifier_value(value: &mut Value) { + if let Value::String(name) = value { + *name = normalize_loose_identifier(name); + } +} + +fn normalize_loose_identifier_or_tag(raw: &str) -> String { + if let Some(tag) = raw.strip_prefix('#') { + format!("#{}", normalize_loose_identifier(tag)) + } else { + normalize_loose_identifier(raw) + } +} + +fn normalize_block_reference(value: &mut Value) { + match value { + Value::String(name) => *name = normalize_loose_identifier_or_tag(name), + Value::Array(items) => { + for item in items { + normalize_block_reference(item); + } + } + _ => {} + } +} + +fn normalize_datapack_type_fields(value: &mut Value) { + const TYPE_KEYS: &[&str] = &["type", "feature_type", "predicate_type", "processor_type"]; + + match value { + Value::Object(map) => { + for (key, child) in map { + if TYPE_KEYS.contains(&key.as_str()) { + normalize_loose_identifier_value(child); + } else if key == "Name" { + normalize_loose_identifier_value(child); + } else if matches!(key.as_str(), "blocks" | "block") { + normalize_block_reference(child); + } + normalize_datapack_type_fields(child); + } + } + Value::Array(items) => { + for item in items { + normalize_datapack_type_fields(item); + } + } + _ => {} + } +} + +pub fn parse_configured_feature_json(registry_id: &str, content: &str) -> ConfiguredFeatureKind { + let mut value: Value = serde_json::from_str(content) + .unwrap_or_else(|err| panic!("failed to parse configured feature {registry_id}: {err}")); + normalize_datapack_type_fields(&mut value); + serde_json::from_value(value) + .unwrap_or_else(|err| panic!("failed to parse configured feature {registry_id}: {err}")) +} + +pub fn parse_placed_feature_json(registry_id: &str, content: &str) -> PlacedFeatureData { + let mut value: Value = serde_json::from_str(content) + .unwrap_or_else(|err| panic!("failed to parse placed feature {registry_id}: {err}")); + normalize_datapack_type_fields(&mut value); + serde_json::from_value(value) + .unwrap_or_else(|err| panic!("failed to parse placed feature {registry_id}: {err}")) +} + /// A configured feature reference, either a registry key or an inline configured feature. #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] @@ -285,13 +363,18 @@ impl<'de> Deserialize<'de> for IdentifierList { #[derive(Deserialize)] #[serde(untagged)] enum Raw { - Single(Identifier), - Many(Vec), + Single(String), + Many(Vec), } Ok(match Raw::deserialize(deserializer)? { - Raw::Single(value) => Self(vec![value]), - Raw::Many(values) => Self(values), + Raw::Single(value) => Self(vec![parse_loose_identifier_de(&value)?]), + Raw::Many(values) => Self( + values + .iter() + .map(|value| parse_loose_identifier_de(value.as_str())) + .collect::>()?, + ), }) } } @@ -309,20 +392,26 @@ impl<'de> Deserialize<'de> for BlockHolderSet { #[serde(untagged)] enum Raw { Single(String), - Many(Vec), + Many(Vec), } match Raw::deserialize(deserializer)? { Raw::Single(value) => { if let Some(tag) = value.strip_prefix('#') { - let tag = tag.parse().map_err(D::Error::custom)?; + let tag = parse_loose_identifier_de(tag)?; Ok(Self::Tag(tag)) } else { - let entry = value.parse().map_err(D::Error::custom)?; + let entry = parse_loose_identifier_de(&value)?; Ok(Self::Entries(vec![entry])) } } - Raw::Many(values) => Ok(Self::Entries(values)), + Raw::Many(values) => { + let entries = values + .iter() + .map(|value| parse_loose_identifier_de(value)) + .collect::>()?; + Ok(Self::Entries(entries)) + } } } } diff --git a/steel-registry/build/features.rs b/steel-registry/build/features.rs index 803d796ffd4b..afee4d5be6e1 100644 --- a/steel-registry/build/features.rs +++ b/steel-registry/build/features.rs @@ -2,6 +2,9 @@ use std::fs; +use crate::generator_functions::{ + generate_static_identifier as generate_identifier, registry_entry_ident, +}; use heck::ToShoutySnakeCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -16,33 +19,14 @@ mod feature_data; use feature_data::*; -fn sorted_json_files(dir: &str) -> Vec { - let mut files: Vec<_> = fs::read_dir(dir) - .unwrap_or_else(|err| panic!("{dir} missing: {err}")) - .filter_map(Result::ok) - .filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("json")) - .collect(); - files.sort_by_key(|entry| entry.file_name()); - files -} - -fn resource_name(entry: &fs::DirEntry) -> String { - entry - .path() - .file_stem() - .and_then(|stem| stem.to_str()) - .unwrap_or_else(|| panic!("invalid feature file name: {:?}", entry.path())) - .to_owned() -} - -fn generate_identifier(identifier: &Identifier) -> TokenStream { - let namespace = identifier.namespace.as_ref(); - let path = identifier.path.as_ref(); - if namespace == Identifier::VANILLA_NAMESPACE { - quote! { Identifier::vanilla_static(#path) } - } else { - quote! { Identifier::new_static(#namespace, #path) } - } +fn sorted_json_registry_entries( + overlay: &DatapackOverlay, + path_suffix: &str, +) -> Vec<(String, String)> { + overlay + .list_json_registry_ids_with_suffix(path_suffix) + .into_iter() + .collect() } fn vanilla_registry_ident(identifier: &Identifier, kind: &str) -> Ident { @@ -1956,19 +1940,13 @@ fn generate_vegetation_patch_kind( } } -pub(crate) fn build_configured() -> TokenStream { - let dir = "../steel-utils/build_assets/builtin_datapacks/minecraft/worldgen/configured_feature"; - println!("cargo:rerun-if-changed={dir}"); - +pub(crate) fn build_configured(overlay: &DatapackOverlay) -> TokenStream { let mut entries = Vec::new(); - for entry in sorted_json_files(dir) { - let name = resource_name(&entry); - let path = entry.path(); - let content = - fs::read_to_string(&path).unwrap_or_else(|err| panic!("failed to read {name}: {err}")); - let kind = serde_json::from_str::(&content) - .unwrap_or_else(|err| panic!("failed to parse configured feature {name}: {err}")); - entries.push((name, generate_configured_feature_kind(&kind))); + for (registry_id, content) in + sorted_json_registry_entries(overlay, "worldgen/configured_feature") + { + let kind = parse_configured_feature_json(®istry_id, &content); + entries.push((registry_id, generate_configured_feature_kind(&kind))); } let mut stream = TokenStream::new(); @@ -1984,12 +1962,16 @@ pub(crate) fn build_configured() -> TokenStream { }); let mut register = TokenStream::new(); - for (name, kind) in &entries { - let ident = Ident::new(&name.to_shouty_snake_case(), Span::call_site()); + for (registry_id, kind) in &entries { + let ident = registry_entry_ident(registry_id); + let identifier = Identifier::from_str(registry_id).unwrap_or_else(|err| { + panic!("invalid configured feature registry id {registry_id}: {err}") + }); + let key = generate_identifier(&identifier); stream.extend(quote! { pub static #ident: LazyLock = LazyLock::new(|| { ConfiguredFeature { - key: Identifier::vanilla_static(#name), + key: #key, kind: #kind, id: OnceLock::new(), } diff --git a/steel-registry/build/generator_functions.rs b/steel-registry/build/generator_functions.rs index a6e1cb7be430..652bc52480d7 100644 --- a/steel-registry/build/generator_functions.rs +++ b/steel-registry/build/generator_functions.rs @@ -35,6 +35,55 @@ pub fn generate_identifier(resource: &Identifier) -> TokenStream { quote! { Identifier { namespace: Cow::Borrowed(#namespace), path: Cow::Borrowed(#path) } } } +pub fn parse_loose_identifier(raw: &str) -> Result { + let (namespace, path) = raw + .split_once(':') + .map_or((Identifier::VANILLA_NAMESPACE, raw), |(namespace, path)| { + (namespace, path) + }); + + if !Identifier::validate(namespace, path) { + return Err(format!("invalid identifier {raw}")); + } + + Ok(Identifier::new(namespace.to_owned(), path.to_owned())) +} + +pub fn generate_static_identifier(resource: &Identifier) -> TokenStream { + let namespace = resource.namespace.as_ref(); + let path = resource.path.as_ref(); + if namespace == Identifier::VANILLA_NAMESPACE { + quote! { Identifier::vanilla_static(#path) } + } else { + quote! { Identifier::new_static(#namespace, #path) } + } +} + +pub fn generate_static_identifier_from_str(raw: &str, context: &str) -> TokenStream { + let identifier = parse_loose_identifier(raw) + .unwrap_or_else(|error| panic!("invalid {context} identifier {raw}: {error}")); + generate_static_identifier(&identifier) +} + +pub fn generate_owned_identifier_from_str(raw: &str, context: &str) -> TokenStream { + let identifier = parse_loose_identifier(raw) + .unwrap_or_else(|error| panic!("invalid {context} identifier {raw}: {error}")); + let namespace = identifier.namespace.as_ref(); + let path = identifier.path.as_ref(); + if namespace == Identifier::VANILLA_NAMESPACE { + quote! { Identifier::vanilla(#path.to_string()) } + } else { + quote! { Identifier::new(#namespace, #path) } + } +} + +pub fn registry_entry_ident(registry_id: &str) -> Ident { + Ident::new( + ®istry_id.replace([':', '/'], "_").to_shouty_snake_case(), + Span::call_site(), + ) +} + pub fn generate_sound_event_ref(resource: &Identifier) -> TokenStream { assert_eq!( resource.namespace.as_ref(), From ea2f47d76b8ddee4c0b2a758cb8475bd8a08500a Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:04:16 +0200 Subject: [PATCH 07/54] ignore unknow properties --- steel-registry/src/blocks/mod.rs | 59 +++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/steel-registry/src/blocks/mod.rs b/steel-registry/src/blocks/mod.rs index f6d596150363..c6cf77b1e588 100644 --- a/steel-registry/src/blocks/mod.rs +++ b/steel-registry/src/blocks/mod.rs @@ -512,16 +512,20 @@ impl BlockRegistry { properties: impl IntoIterator, ) -> Option<()> { for (prop_name, prop_value) in properties { - let prop_idx = block - .properties - .iter() - .position(|p| p.get_name() == prop_name)?; + let Some(prop_idx) = block.properties.iter().position(|p| p.get_name() == prop_name) + else { + // Vanilla's block state codec is lenient and ignores unknown properties. + continue; + }; let prop = block.properties[prop_idx]; - let value_idx = prop + let Some(value_idx) = prop .get_possible_values() .iter() - .position(|v| *v == prop_value)?; + .position(|v| *v == prop_value) + else { + return None; + }; property_indices[prop_idx] = value_idx; } @@ -1098,13 +1102,18 @@ mod tests { } #[test] - fn test_state_id_from_properties_invalid_property() { + fn test_state_id_from_properties_ignores_unknown_property() { let registry = create_test_registry(); let key = Identifier::vanilla_static("redstone_wire"); let invalid_props = [("invalid_property", "value")]; - let result = registry.state_id_from_properties(&key, &invalid_props); - assert!(result.is_none(), "Should return None for invalid property"); + let result = registry + .state_id_from_properties(&key, &invalid_props) + .expect("unknown properties should be ignored"); + let with_power = registry + .state_id_from_properties(&key, &[("power", "5")]) + .expect("valid properties should still resolve"); + assert_ne!(result, with_power); } #[test] @@ -1146,22 +1155,32 @@ mod tests { } #[test] - fn test_state_id_from_properties_rejects_properties_on_propertyless_block() { + fn test_state_id_from_properties_ignores_unknown_property_on_propertyless_block() { let registry = create_test_registry(); let key = Identifier::vanilla_static("stone"); let stone = registry.by_key(&key).expect("stone should exist"); - let result = registry.state_id_from_properties(&key, &[("power", "1")]); - assert!( - result.is_none(), - "Should return None for invalid property on propertyless block" - ); + let result = registry + .state_id_from_properties(&key, &[("power", "1")]) + .expect("unknown properties should be ignored"); + assert_eq!(result, registry.get_default_state_id(stone)); - let result = registry.state_id_from_block_defaulted_properties(stone, [("power", "1")]); - assert!( - result.is_none(), - "Should return None for invalid defaulted property on propertyless block" - ); + let result = registry + .state_id_from_block_defaulted_properties(stone, [("power", "1")]) + .expect("unknown defaulted properties should be ignored"); + assert_eq!(result, registry.get_default_state_id(stone)); + } + + #[test] + fn test_state_id_from_block_defaulted_properties_ignores_removed_grass_path_snowy() { + let registry = create_test_registry(); + let key = Identifier::vanilla_static("dirt_path"); + let block = registry.by_key(&key).expect("dirt_path should exist"); + + let state_id = registry + .state_id_from_block_defaulted_properties(block, [("snowy", "false")]) + .expect("legacy snowy on dirt_path should be ignored"); + assert_eq!(state_id, registry.get_default_state_id(block)); } #[test] From e9ae7624a06edcd3850ee44539542585e5f8014b Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:28:50 +0200 Subject: [PATCH 08/54] add place no_op --- steel-core/src/worldgen/feature/configured.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/steel-core/src/worldgen/feature/configured.rs b/steel-core/src/worldgen/feature/configured.rs index 5c310b3cb22d..eaf4fca9be47 100644 --- a/steel-core/src/worldgen/feature/configured.rs +++ b/steel-core/src/worldgen/feature/configured.rs @@ -135,10 +135,18 @@ impl FeatureDecorationRunner { place_waterlogged_vegetation_patch } ConfiguredFeatureKind::WeepingVines => place_weeping_vines, + ConfiguredFeatureKind::NoOp => place_no_op, } } } +const fn place_no_op( + _context: &mut ConfiguredFeaturePlaceContext<'_, '_>, + _kind: &ConfiguredFeatureKind, +) -> bool { + true +} + fn place_random_boolean_selector( context: &mut ConfiguredFeaturePlaceContext<'_, '_>, kind: &ConfiguredFeatureKind, From 7a5cba210361830cc40b78b29dca1bbf2b32689a Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:36:23 +0200 Subject: [PATCH 09/54] use and complete holderset --- .../feature/features/multiface_growth.rs | 8 ++-- steel-core/src/worldgen/feature/predicates.rs | 4 +- steel-registry/src/feature/data.rs | 41 ++++++++++++++++--- steel-worldgen/src/state_resolver.rs | 26 ++++++++++-- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/steel-core/src/worldgen/feature/features/multiface_growth.rs b/steel-core/src/worldgen/feature/features/multiface_growth.rs index b4cdf49bcd98..b110f7e5832d 100644 --- a/steel-core/src/worldgen/feature/features/multiface_growth.rs +++ b/steel-core/src/worldgen/feature/features/multiface_growth.rs @@ -1,6 +1,7 @@ use super::super::prelude::*; use super::super::runner::FeatureDecorationRunner; use smallvec::SmallVec; +use steel_registry::feature::BlockHolderSet; use steel_registry::vanilla_block_tags::BlockTag; #[derive(Clone, Copy)] @@ -19,7 +20,7 @@ struct ResolvedMultifaceGrowth<'a> { raw: &'a MultifaceGrowthConfiguration, place_block: BlockRef, default_state: BlockStateId, - can_be_placed_on: &'a [BlockRef], + can_be_placed_on: &'a BlockHolderSet, is_sculk_vein: bool, } @@ -402,10 +403,7 @@ impl FeatureDecorationRunner { config: &ResolvedMultifaceGrowth<'_>, state: BlockStateId, ) -> bool { - config - .can_be_placed_on - .iter() - .any(|block| state.get_block() == *block) + config.can_be_placed_on.contains(state.get_block()) } fn multiface_is_place_block(config: &ResolvedMultifaceGrowth<'_>, state: BlockStateId) -> bool { diff --git a/steel-core/src/worldgen/feature/predicates.rs b/steel-core/src/worldgen/feature/predicates.rs index 03248e3a0333..31c19cf74b5c 100644 --- a/steel-core/src/worldgen/feature/predicates.rs +++ b/steel-core/src/worldgen/feature/predicates.rs @@ -63,12 +63,12 @@ impl FeatureDecorationRunner { } BlockPredicate::MatchingBlocks { blocks, offset } => { let state = region.block_state(Self::offset(origin, offset)); - blocks.0.contains(&state.get_block()) + blocks.contains(state.get_block()) } BlockPredicate::MatchingFluids { fluids, offset } => { let state = region.block_state(Self::offset(origin, offset)); let fluid_state = get_fluid_state_from_block(state); - fluids.0.contains(&fluid_state.fluid_id) + fluids.contains(fluid_state.fluid_id) } BlockPredicate::Solid { offset } => { region.block_state(Self::offset(origin, offset)).is_solid() diff --git a/steel-registry/src/feature/data.rs b/steel-registry/src/feature/data.rs index cfc58bb9f1d6..7b60fd5b6240 100644 --- a/steel-registry/src/feature/data.rs +++ b/steel-registry/src/feature/data.rs @@ -84,6 +84,7 @@ pub enum ConfiguredFeatureKind { MultifaceGrowth(MultifaceGrowthConfiguration), NetherForestVegetation(NetherForestVegetationConfiguration), NetherrackReplaceBlobs(NetherrackReplaceBlobsConfiguration), + NoOp, Ore(OreConfiguration), PointedDripstone(PointedDripstoneConfiguration), RandomBooleanSelector(RandomBooleanSelectorConfiguration), @@ -127,6 +128,33 @@ pub enum BlockHolderSet { Entries(Vec), } +impl BlockHolderSet { + /// Check if this holder set contains a block. + pub fn contains(&self, block: BlockRef) -> bool { + match self { + BlockHolderSet::Tag(tag) => block.has_tag(tag), + BlockHolderSet::Entries(entries) => entries.contains(&block), + } + } +} + +/// Fluid holder set decoded from vanilla's holder-set codec shape at build time. +#[derive(Debug, Clone)] +pub enum FluidHolderSet { + Tag(Identifier), + Entries(Vec), +} + +impl FluidHolderSet { + /// Check if this holder set contains a fluid. + pub fn contains(&self, fluid: FluidRef) -> bool { + match self { + FluidHolderSet::Tag(tag) => fluid.has_tag(tag), + FluidHolderSet::Entries(entries) => entries.contains(&fluid), + } + } +} + /// Block state data emitted by the feature generator without baking a state id. #[derive(Debug, Clone)] pub struct BlockStateData { @@ -166,11 +194,11 @@ pub enum BlockPredicate { offset: Offset, }, MatchingBlocks { - blocks: BlockRefList, + blocks: BlockHolderSet, offset: Offset, }, MatchingFluids { - fluids: FluidRefList, + fluids: FluidHolderSet, offset: Offset, }, Solid { @@ -565,7 +593,7 @@ pub struct MultifaceGrowthConfiguration { pub can_place_on_ceiling: bool, pub can_place_on_wall: bool, pub chance_of_spreading: f32, - pub can_be_placed_on: Vec, + pub can_be_placed_on: BlockHolderSet, } #[derive(Debug, Clone)] @@ -598,6 +626,7 @@ pub struct OreTarget { #[derive(Debug, Clone)] pub enum RuleTest { BlockMatch { block: BlockRef }, + RandomBlockMatch { block: BlockRef, probability: f32 }, TagMatch { tag: Identifier }, } @@ -892,7 +921,7 @@ pub enum RootPlacer { pub struct MangroveRootPlacer { pub trunk_offset_y: IntProvider, pub root_provider: BlockStateProvider, - pub above_root_placement: AboveRootPlacement, + pub above_root_placement: Option, pub mangrove_root_placement: MangroveRootPlacement, } @@ -904,8 +933,8 @@ pub struct AboveRootPlacement { #[derive(Debug, Clone)] pub struct MangroveRootPlacement { - pub can_grow_through: Identifier, - pub muddy_roots_in: Vec, + pub can_grow_through: BlockHolderSet, + pub muddy_roots_in: BlockHolderSet, pub muddy_roots_provider: BlockStateProvider, pub max_root_width: i32, pub max_root_length: i32, diff --git a/steel-worldgen/src/state_resolver.rs b/steel-worldgen/src/state_resolver.rs index cca0ddfc3b08..116fcc725da8 100644 --- a/steel-worldgen/src/state_resolver.rs +++ b/steel-worldgen/src/state_resolver.rs @@ -7,6 +7,21 @@ use steel_utils::BlockStateId; /// Resolves vanilla JSON/NBT block-state data to Steel block-state ids. pub struct WorldgenStateResolver; +fn map_block_name(name: &steel_utils::Identifier) -> steel_utils::Identifier { + let namespace = name.namespace.as_ref(); + let path = name.path.as_ref(); + if namespace == "minecraft" { + match path { + "grass" => steel_utils::Identifier::vanilla_static("short_grass"), + "grass_path" => steel_utils::Identifier::vanilla_static("dirt_path"), + "chain" => steel_utils::Identifier::vanilla_static("iron_chain"), + _ => name.clone(), + } + } else { + name.clone() + } +} + impl WorldgenStateResolver { /// Resolves a block state from data. /// @@ -18,13 +33,18 @@ impl WorldgenStateResolver { data: &shared_structs::BlockStateData, context: &str, ) -> BlockStateId { - let Some(block) = registry.blocks.by_key(&data.name) else { - panic!("{context} references unknown block {}", data.name); + let name = map_block_name(&data.name); + let Some(block) = registry.blocks.by_key(&name) else { + println!( + "CRITICAL: WorldgenStateResolver references unknown block: {:?}", + name + ); + panic!("{context} references unknown block {}", name); }; Self::block_state_from_parts( registry, block, - &data.name, + &name, data.properties .iter() .map(|(key, value)| (key.as_str(), value.as_str())), From e1f48ae0550c4cd6ed4e2cb852643f4369bd5c15 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:53:03 +0200 Subject: [PATCH 10/54] add ore missing feature --- .../src/worldgen/feature/features/ore.rs | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/steel-core/src/worldgen/feature/features/ore.rs b/steel-core/src/worldgen/feature/features/ore.rs index 5113057dfe33..1dbfa783d302 100644 --- a/steel-core/src/worldgen/feature/features/ore.rs +++ b/steel-core/src/worldgen/feature/features/ore.rs @@ -184,7 +184,7 @@ impl FeatureDecorationRunner { pending_section.key.chunk_z, pending_section.key.section_index, &pending_section.positions, - |block_state| targets.matching_replacement(registry, block_state), + |block_state| targets.matching_replacement(registry, block_state, random), ); } if let Some(started_at) = batch_apply_started_at @@ -384,7 +384,7 @@ impl FeatureDecorationRunner { ) -> bool { if config.discard_chance_on_air_exposure <= 0.0 { return sections.replace_ore_target_block_state(pos, |block_state| { - targets.matching_replacement(registry, block_state) + targets.matching_replacement(registry, block_state, random) }); } @@ -410,7 +410,8 @@ impl FeatureDecorationRunner { pos: BlockPos, block_id: usize, ) -> bool { - if !target.matches_block_id(block_id) { + let state = region.block_state(pos); + if !target.matches_state(state, block_id, random) { return false; } @@ -430,7 +431,8 @@ impl FeatureDecorationRunner { pos: BlockPos, block_id: usize, ) -> bool { - if !target.matches_block_id(block_id) { + let state = sections.ore_target_block_state(pos); + if !target.matches_state(state, block_id, random) { return false; } @@ -514,7 +516,9 @@ struct ResolvedOreTarget { enum ResolvedOreRuleTest { Block(usize), + BlockState(BlockStateId), Tag(SmallVec<[usize; 8]>), + RandomBlock(usize, f32), } #[derive(Clone, Copy)] @@ -559,6 +563,16 @@ impl ResolvedOreTargets { for target in &config.targets { let matcher = match &target.target { RuleTest::BlockMatch { block } => ResolvedOreRuleTest::Block(block.id()), + RuleTest::BlockStateMatch { block_state } => ResolvedOreRuleTest::BlockState( + WorldgenStateResolver::feature_block_state_from_data( + registry, + block_state, + "ore feature target", + ), + ), + RuleTest::RandomBlockMatch { block, probability } => { + ResolvedOreRuleTest::RandomBlock(block.id(), *probability) + } RuleTest::TagMatch { tag } => { let block_ids = registry .blocks @@ -587,11 +601,14 @@ impl ResolvedOreTargets { &self, registry: &Registry, state: BlockStateId, + random: &mut WorldgenRandom, ) -> Option { let block_id = Self::block_id_for_state(registry, state); - self.targets - .iter() - .find_map(|target| target.matches_block_id(block_id).then_some(target.state)) + self.targets.iter().find_map(|target| { + target + .matches_state(state, block_id, random) + .then_some(target.state) + }) } fn block_id_for_state(registry: &Registry, state: BlockStateId) -> usize { @@ -603,10 +620,19 @@ impl ResolvedOreTargets { } impl ResolvedOreTarget { - fn matches_block_id(&self, block_id: usize) -> bool { + fn matches_state( + &self, + state: BlockStateId, + block_id: usize, + random: &mut WorldgenRandom, + ) -> bool { match &self.matcher { ResolvedOreRuleTest::Block(target_block_id) => block_id == *target_block_id, + ResolvedOreRuleTest::BlockState(target_state) => state == *target_state, ResolvedOreRuleTest::Tag(block_ids) => block_ids.contains(&block_id), + ResolvedOreRuleTest::RandomBlock(target_block_id, probability) => { + block_id == *target_block_id && random.next_f32() < *probability + } } } } From 3c1a18c31036761b6068b6b0494bc9de67771aba Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:17:24 +0200 Subject: [PATCH 11/54] fix a test --- steel-worldgen/src/structure/placement.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/steel-worldgen/src/structure/placement.rs b/steel-worldgen/src/structure/placement.rs index 655384f8dce9..5f75de047da7 100644 --- a/steel-worldgen/src/structure/placement.rs +++ b/steel-worldgen/src/structure/placement.rs @@ -538,7 +538,11 @@ mod tests { #[test] fn test_load_vanilla_structure_sets() { let sets = load_vanilla_structure_sets(); - assert_eq!(sets.len(), 20); + let vanilla_sets_count = sets + .iter() + .filter(|(k, _)| k.namespace == "minecraft") + .count(); + assert_eq!(vanilla_sets_count, 20); // Verify villages loaded correctly from datapack let (key, villages) = sets From 2a4627ab7dade56b2cfd4c2a3c565d7ea4bfc926 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:09:12 +0200 Subject: [PATCH 12/54] add more vanilla feature in structure --- .../structure_piece_placer/pool_element.rs | 1 + steel-core/src/worldgen/template.rs | 30 ++++++++++++++++--- steel-core/tests/structure_starts.rs | 1 + 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/steel-core/src/worldgen/structure_piece_placer/pool_element.rs b/steel-core/src/worldgen/structure_piece_placer/pool_element.rs index 9d1d9a532c58..360ebc8c9c81 100644 --- a/steel-core/src/worldgen/structure_piece_placer/pool_element.rs +++ b/steel-core/src/worldgen/structure_piece_placer/pool_element.rs @@ -158,6 +158,7 @@ impl StructurePiecePlacer { ) -> &'a [StructureProcessorKind] { match processors { ProcessorList::Empty => &[], + ProcessorList::Direct(processors) => processors, ProcessorList::Registry(key) => { let Some(processor_list) = registry.structure_processors.by_key(key) else { panic!("template pool references unknown processor list {key}"); diff --git a/steel-core/src/worldgen/template.rs b/steel-core/src/worldgen/template.rs index 438decc9ccd0..6673e3b02a5b 100644 --- a/steel-core/src/worldgen/template.rs +++ b/steel-core/src/worldgen/template.rs @@ -1326,10 +1326,12 @@ impl StructureTemplate { }); (!ignored).then_some(current) } - StructureProcessorKind::Gravity { heightmap, offset } => { - Some(Self::process_gravity(region, original, current, *heightmap, *offset)) + StructureProcessorKind::Gravity { heightmap, offset } => Some(Self::process_gravity( + region, original, current, *heightmap, *offset, + )), + StructureProcessorKind::Capped { .. } | StructureProcessorKind::JigsawReplacement => { + Some(current) } - StructureProcessorKind::Capped { .. } => Some(current), } } @@ -1937,7 +1939,8 @@ impl StructureTemplate { registry, block_state, "structure processor random block-state predicate", - ) && random.next_f32() < *probability + ) + && random.next_f32() < *probability } } } @@ -1994,10 +1997,29 @@ impl StructureTemplate { nbt.insert("LootTableSeed", NbtTag::Long(random.next_i64())); Some(nbt) } + RuleBlockEntityModifierData::AppendStatic { data } => { + let mut nbt = current.nbt.unwrap_or_default(); + Self::merge_nbt_compound(&mut nbt, data); + Some(nbt) + } }; current } + fn merge_nbt_compound(target: &mut NbtCompound, source: &NbtCompound) { + for (key, source_value) in source.iter() { + let key = key.to_string(); + if let Some(NbtTag::Compound(target_child)) = target.get_mut(&key) + && let NbtTag::Compound(source_child) = source_value + { + Self::merge_nbt_compound(target_child, source_child); + continue; + } + let _ = target.remove(&key); + target.insert(key, source_value.clone()); + } + } + fn place_block_entity( region: &mut WorldGenRegion<'_>, pos: BlockPos, diff --git a/steel-core/tests/structure_starts.rs b/steel-core/tests/structure_starts.rs index 77ac0c0d16b0..eb928db7d986 100644 --- a/steel-core/tests/structure_starts.rs +++ b/steel-core/tests/structure_starts.rs @@ -904,6 +904,7 @@ fn processors_to_value(processors: &ProcessorList) -> Value { match processors { ProcessorList::Empty => json!({ "processors": [] }), ProcessorList::Registry(id) => Value::String(id.to_string()), + ProcessorList::Direct(_) => json!({ "processors": "direct" }), } } From 0cf491562aca88cb73800c549c4cb7856f19667c Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:20:54 +0200 Subject: [PATCH 13/54] more alias for density function --- steel-worldgen/build/density_functions.rs | 75 ++++++++++++----------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/steel-worldgen/build/density_functions.rs b/steel-worldgen/build/density_functions.rs index 7681f11ffaae..cfad8c135934 100644 --- a/steel-worldgen/build/density_functions.rs +++ b/steel-worldgen/build/density_functions.rs @@ -32,25 +32,25 @@ pub enum DensityFunctionJson { #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type")] pub enum DensityFunctionData { - #[serde(rename = "minecraft:constant")] + #[serde(rename = "minecraft:constant", alias = "constant")] Constant { #[serde(alias = "argument")] value: f64, }, - #[serde(rename = "minecraft:y_clamped_gradient")] + #[serde(rename = "minecraft:y_clamped_gradient", alias = "y_clamped_gradient")] YClampedGradient { from_y: i32, to_y: i32, from_value: f64, to_value: f64, }, - #[serde(rename = "minecraft:noise")] + #[serde(rename = "minecraft:noise", alias = "noise")] Noise { xz_scale: f64, y_scale: f64, noise: String, }, - #[serde(rename = "minecraft:shifted_noise")] + #[serde(rename = "minecraft:shifted_noise", alias = "shifted_noise")] ShiftedNoise { shift_x: Box, shift_y: Box, @@ -59,85 +59,85 @@ pub enum DensityFunctionData { y_scale: f64, noise: String, }, - #[serde(rename = "minecraft:shift_a")] + #[serde(rename = "minecraft:shift_a", alias = "shift_a")] ShiftA { #[serde(rename = "argument")] noise: String, }, - #[serde(rename = "minecraft:shift_b")] + #[serde(rename = "minecraft:shift_b", alias = "shift_b")] ShiftB { #[serde(rename = "argument")] noise: String, }, - #[serde(rename = "minecraft:shift")] + #[serde(rename = "minecraft:shift", alias = "shift")] Shift { #[serde(rename = "argument")] noise: String, }, - #[serde(rename = "minecraft:clamp")] + #[serde(rename = "minecraft:clamp", alias = "clamp")] Clamp { input: Box, min: f64, max: f64, }, - #[serde(rename = "minecraft:abs")] + #[serde(rename = "minecraft:abs", alias = "abs")] Abs { #[serde(rename = "argument")] input: Box, }, - #[serde(rename = "minecraft:square")] + #[serde(rename = "minecraft:square", alias = "square")] Square { #[serde(rename = "argument")] input: Box, }, - #[serde(rename = "minecraft:cube")] + #[serde(rename = "minecraft:cube", alias = "cube")] Cube { #[serde(rename = "argument")] input: Box, }, - #[serde(rename = "minecraft:half_negative")] + #[serde(rename = "minecraft:half_negative", alias = "half_negative")] HalfNegative { #[serde(rename = "argument")] input: Box, }, - #[serde(rename = "minecraft:quarter_negative")] + #[serde(rename = "minecraft:quarter_negative", alias = "quarter_negative")] QuarterNegative { #[serde(rename = "argument")] input: Box, }, - #[serde(rename = "minecraft:invert")] + #[serde(rename = "minecraft:invert", alias = "invert")] Invert { #[serde(rename = "argument")] input: Box, }, - #[serde(rename = "minecraft:squeeze")] + #[serde(rename = "minecraft:squeeze", alias = "squeeze")] Squeeze { #[serde(rename = "argument")] input: Box, }, - #[serde(rename = "minecraft:add")] + #[serde(rename = "minecraft:add", alias = "add")] Add { argument1: Box, argument2: Box, }, - #[serde(rename = "minecraft:mul")] + #[serde(rename = "minecraft:mul", alias = "mul")] Mul { argument1: Box, argument2: Box, }, - #[serde(rename = "minecraft:min")] + #[serde(rename = "minecraft:min", alias = "min")] Min { argument1: Box, argument2: Box, }, - #[serde(rename = "minecraft:max")] + #[serde(rename = "minecraft:max", alias = "max")] Max { argument1: Box, argument2: Box, }, - #[serde(rename = "minecraft:spline")] + #[serde(rename = "minecraft:spline", alias = "spline")] Spline { spline: SplineJson }, - #[serde(rename = "minecraft:range_choice")] + #[serde(rename = "minecraft:range_choice", alias = "range_choice")] RangeChoice { input: Box, min_inclusive: f64, @@ -145,42 +145,45 @@ pub enum DensityFunctionData { when_in_range: Box, when_out_of_range: Box, }, - #[serde(rename = "minecraft:interval_select")] + #[serde(rename = "minecraft:interval_select", alias = "interval_select")] IntervalSelect { input: Box, thresholds: Vec, functions: Vec, }, - #[serde(rename = "minecraft:interpolated")] + #[serde(rename = "minecraft:interpolated", alias = "interpolated")] Interpolated { argument: Box }, - #[serde(rename = "minecraft:flat_cache")] + #[serde(rename = "minecraft:flat_cache", alias = "flat_cache")] FlatCache { argument: Box }, - #[serde(rename = "minecraft:cache_once")] + #[serde(rename = "minecraft:cache_once", alias = "cache_once")] CacheOnce { argument: Box }, - #[serde(rename = "minecraft:cache_2d")] + #[serde(rename = "minecraft:cache_2d", alias = "cache_2d")] Cache2d { argument: Box }, - #[serde(rename = "minecraft:cache_all_in_cell")] + #[serde(rename = "minecraft:cache_all_in_cell", alias = "cache_all_in_cell")] CacheAllInCell { argument: Box }, - #[serde(rename = "minecraft:blend_offset")] + #[serde(rename = "minecraft:blend_offset", alias = "blend_offset")] BlendOffset {}, - #[serde(rename = "minecraft:blend_alpha")] + #[serde(rename = "minecraft:blend_alpha", alias = "blend_alpha")] BlendAlpha {}, - #[serde(rename = "minecraft:blend_density")] + #[serde(rename = "minecraft:blend_density", alias = "blend_density")] BlendDensity { #[serde(rename = "argument")] input: Box, }, - #[serde(rename = "minecraft:beardifier")] + #[serde(rename = "minecraft:beardifier", alias = "beardifier")] Beardifier {}, - #[serde(rename = "minecraft:end_islands")] + #[serde(rename = "minecraft:end_islands", alias = "end_islands")] EndIslands {}, - #[serde(rename = "minecraft:weird_scaled_sampler")] + #[serde( + rename = "minecraft:weird_scaled_sampler", + alias = "weird_scaled_sampler" + )] WeirdScaledSampler { input: Box, noise: String, rarity_value_mapper: String, }, - #[serde(rename = "minecraft:old_blended_noise")] + #[serde(rename = "minecraft:old_blended_noise", alias = "old_blended_noise")] OldBlendedNoise { xz_scale: f64, y_scale: f64, @@ -188,7 +191,7 @@ pub enum DensityFunctionData { y_factor: f64, smear_scale_multiplier: f64, }, - #[serde(rename = "minecraft:find_top_surface")] + #[serde(rename = "minecraft:find_top_surface", alias = "find_top_surface")] FindTopSurface { density: Box, upper_bound: Box, @@ -206,7 +209,7 @@ pub enum DensityFunctionData { pub enum SplineJson { Constant(f32), Multipoint { - coordinate: String, + coordinate: Box, #[serde(default)] points: Vec, }, From a3de968a3f473fae8e53bf2b2d687d6280ac6360 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:21:57 +0200 Subject: [PATCH 14/54] more alias surface rule --- steel-worldgen/build/surface_rules.rs | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/steel-worldgen/build/surface_rules.rs b/steel-worldgen/build/surface_rules.rs index 29d6860ecac4..bb59b5a73cb8 100644 --- a/steel-worldgen/build/surface_rules.rs +++ b/steel-worldgen/build/surface_rules.rs @@ -15,16 +15,16 @@ use std::{mem, slice}; #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type")] pub enum SurfaceRuleJson { - #[serde(rename = "minecraft:block")] + #[serde(rename = "minecraft:block", alias = "block")] Block { result_state: ResultStateJson }, - #[serde(rename = "minecraft:sequence")] + #[serde(rename = "minecraft:sequence", alias = "sequence")] Sequence { sequence: Vec }, - #[serde(rename = "minecraft:condition")] + #[serde(rename = "minecraft:condition", alias = "condition")] Condition { if_true: SurfaceConditionJson, then_run: Box, }, - #[serde(rename = "minecraft:bandlands")] + #[serde(rename = "minecraft:bandlands", alias = "bandlands")] Bandlands {}, } @@ -43,18 +43,21 @@ pub struct ResultStateJson { #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type")] pub enum SurfaceConditionJson { - #[serde(rename = "minecraft:stone_depth")] + #[serde(rename = "minecraft:stone_depth", alias = "stone_depth")] StoneDepth { offset: i32, add_surface_depth: bool, secondary_depth_range: i32, surface_type: String, }, - #[serde(rename = "minecraft:above_preliminary_surface")] + #[serde( + rename = "minecraft:above_preliminary_surface", + alias = "above_preliminary_surface" + )] AbovePreliminarySurface {}, - #[serde(rename = "minecraft:biome")] + #[serde(rename = "minecraft:biome", alias = "biome")] BiomeIs { biome_is: SingleOrList }, - #[serde(rename = "minecraft:noise_threshold")] + #[serde(rename = "minecraft:noise_threshold", alias = "noise_threshold")] NoiseThreshold { noise: String, #[serde(default)] @@ -62,31 +65,31 @@ pub enum SurfaceConditionJson { min_threshold: f64, max_threshold: f64, }, - #[serde(rename = "minecraft:vertical_gradient")] + #[serde(rename = "minecraft:vertical_gradient", alias = "vertical_gradient")] VerticalGradient { random_name: String, true_at_and_below: VerticalAnchorJson, false_at_and_above: VerticalAnchorJson, }, - #[serde(rename = "minecraft:y_above")] + #[serde(rename = "minecraft:y_above", alias = "y_above")] YAbove { anchor: VerticalAnchorJson, surface_depth_multiplier: i32, add_stone_depth: bool, }, - #[serde(rename = "minecraft:water")] + #[serde(rename = "minecraft:water", alias = "water")] Water { offset: i32, surface_depth_multiplier: i32, add_stone_depth: bool, }, - #[serde(rename = "minecraft:temperature")] + #[serde(rename = "minecraft:temperature", alias = "temperature")] Temperature {}, - #[serde(rename = "minecraft:steep")] + #[serde(rename = "minecraft:steep", alias = "steep")] Steep {}, - #[serde(rename = "minecraft:hole")] + #[serde(rename = "minecraft:hole", alias = "hole")] Hole {}, - #[serde(rename = "minecraft:not")] + #[serde(rename = "minecraft:not", alias = "not")] Not { invert: Box }, } From 9c4a4feb496034abf14dbfc13f6e0004dfa62f9e Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:25:19 +0200 Subject: [PATCH 15/54] add ReplaceSingleBlock --- steel-core/src/worldgen/feature/configured.rs | 48 +++++++++++++++++++ steel-worldgen/build/multi_noise.rs | 24 +++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/steel-core/src/worldgen/feature/configured.rs b/steel-core/src/worldgen/feature/configured.rs index eaf4fca9be47..aa797e5692a2 100644 --- a/steel-core/src/worldgen/feature/configured.rs +++ b/steel-core/src/worldgen/feature/configured.rs @@ -111,6 +111,7 @@ impl FeatureDecorationRunner { ConfiguredFeatureKind::PointedDripstone(_) => place_pointed_dripstone, ConfiguredFeatureKind::RandomBooleanSelector(_) => place_random_boolean_selector, ConfiguredFeatureKind::RandomSelector(_) => place_random_selector, + ConfiguredFeatureKind::ReplaceSingleBlock(_) => place_replace_single_block, ConfiguredFeatureKind::WeightedRandomSelector(_) => place_weighted_random_selector, ConfiguredFeatureKind::RootSystem(_) => place_root_system, ConfiguredFeatureKind::ScatteredOre(_) => place_scattered_ore, @@ -140,6 +141,53 @@ impl FeatureDecorationRunner { } } +fn place_replace_single_block( + context: &mut ConfiguredFeaturePlaceContext<'_, '_>, + kind: &ConfiguredFeatureKind, +) -> bool { + let ConfiguredFeatureKind::ReplaceSingleBlock(config) = kind else { + panic!("replace_single_block placer received wrong configured feature kind"); + }; + for target in &config.targets { + if rule_test_matches( + context.region, + context.registry, + context.random, + &target.target, + context.origin, + ) { + let state = + FeatureDecorationRunner::block_state_from_data(context.registry, &target.state); + let _ = + context + .region + .set_block_state(context.origin, state, UpdateFlags::UPDATE_CLIENTS); + break; + } + } + true +} + +fn rule_test_matches( + region: &WorldGenRegion<'_>, + registry: &Registry, + random: &mut WorldgenRandom, + rule: &RuleTest, + pos: BlockPos, +) -> bool { + let state = region.block_state(pos); + match rule { + RuleTest::BlockMatch { block } => state.get_block() == *block, + RuleTest::BlockStateMatch { block_state } => { + state == FeatureDecorationRunner::block_state_from_data(registry, block_state) + } + RuleTest::RandomBlockMatch { block, probability } => { + state.get_block() == *block && random.next_f32() < *probability + } + RuleTest::TagMatch { tag } => registry.blocks.is_in_tag(state.get_block(), tag), + } +} + const fn place_no_op( _context: &mut ConfiguredFeaturePlaceContext<'_, '_>, _kind: &ConfiguredFeatureKind, diff --git a/steel-worldgen/build/multi_noise.rs b/steel-worldgen/build/multi_noise.rs index dd61f430eecf..32f15d90f62d 100644 --- a/steel-worldgen/build/multi_noise.rs +++ b/steel-worldgen/build/multi_noise.rs @@ -6,12 +6,34 @@ use quote::quote; use serde::Deserialize; /// A biome entry from the extracted multi-noise biome source parameter list. -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] struct BiomeEntry { biome: String, parameters: BiomeParameters, } +/// A climate parameter range, which can be deserialized from either a single number or a pair. +#[derive(Clone, Debug)] +struct ParameterRange([f64; 2]); + +impl<'de> Deserialize<'de> for ParameterRange { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum RawRange { + Single(f64), + Pair([f64; 2]), + } + match RawRange::deserialize(deserializer)? { + RawRange::Single(val) => Ok(ParameterRange([val, val])), + RawRange::Pair(pair) => Ok(ParameterRange(pair)), + } + } +} + /// Climate parameters for a biome entry. #[derive(Deserialize)] struct BiomeParameters { From dc9fafa1300ea69ced8307cc2c9cfcc2461b5f85 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:25:46 +0200 Subject: [PATCH 16/54] finish add foliage --- steel-core/src/worldgen/feature/features/tree/foliage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/steel-core/src/worldgen/feature/features/tree/foliage.rs b/steel-core/src/worldgen/feature/features/tree/foliage.rs index 4ff6ca890fbd..acb1d3846e9d 100644 --- a/steel-core/src/worldgen/feature/features/tree/foliage.rs +++ b/steel-core/src/worldgen/feature/features/tree/foliage.rs @@ -29,7 +29,7 @@ impl FeatureDecorationRunner { FoliagePlacer::Acacia(_) => 0, FoliagePlacer::DarkOak(_) => 4, FoliagePlacer::Jungle(placer) => placer.height.sample(random), - FoliagePlacer::RandomSpread(placer) => placer.foliage_height, + FoliagePlacer::RandomSpread(placer) => placer.foliage_height.sample(random), FoliagePlacer::Cherry(placer) => placer.height.sample(random), } } From bec7f99ebcc4d4ad95ca8e3cbccfeaf59337b99f Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:26:47 +0200 Subject: [PATCH 17/54] some add for tree --- .../src/worldgen/feature/features/tree/mod.rs | 19 +++++++++++++++++-- .../worldgen/feature/features/tree/trunk.rs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/steel-core/src/worldgen/feature/features/tree/mod.rs b/steel-core/src/worldgen/feature/features/tree/mod.rs index a15735260af2..7c0a4d6b7c01 100644 --- a/steel-core/src/worldgen/feature/features/tree/mod.rs +++ b/steel-core/src/worldgen/feature/features/tree/mod.rs @@ -1,3 +1,4 @@ +use steel_registry::feature::BlockHolderSet; use steel_registry::vanilla_block_tags::BlockTag; use super::super::prelude::*; @@ -187,7 +188,7 @@ impl FeatureDecorationRunner { ) -> bool { match trunk_placer { TrunkPlacer::UpwardsBranching(placer) => { - Self::tree_valid_pos_or_tag(region, pos, &placer.can_grow_through) + Self::tree_valid_pos_or_tag_id(region, pos, &placer.can_grow_through) } TrunkPlacer::Straight(_) | TrunkPlacer::Forking(_) @@ -200,7 +201,21 @@ impl FeatureDecorationRunner { } } - fn tree_valid_pos_or_tag(region: &WorldGenRegion<'_>, pos: BlockPos, tag: &Identifier) -> bool { + fn tree_valid_pos_or_tag( + region: &WorldGenRegion<'_>, + pos: BlockPos, + tag: &BlockHolderSet, + ) -> bool { + let state = region.block_state(pos); + let block = state.get_block(); + state.is_air() || block.has_tag(&BlockTag::REPLACEABLE_BY_TREES) || tag.contains(block) + } + + fn tree_valid_pos_or_tag_id( + region: &WorldGenRegion<'_>, + pos: BlockPos, + tag: &Identifier, + ) -> bool { let state = region.block_state(pos); let block = state.get_block(); state.is_air() || block.has_tag(&BlockTag::REPLACEABLE_BY_TREES) || block.has_tag(tag) diff --git a/steel-core/src/worldgen/feature/features/tree/trunk.rs b/steel-core/src/worldgen/feature/features/tree/trunk.rs index 998a5ff37a18..8d787de3edc4 100644 --- a/steel-core/src/worldgen/feature/features/tree/trunk.rs +++ b/steel-core/src/worldgen/feature/features/tree/trunk.rs @@ -1193,7 +1193,7 @@ impl FeatureDecorationRunner { config: &TreeConfiguration, placement: &mut TreePlacement, ) -> bool { - if !Self::tree_valid_pos_or_tag(region, pos, can_grow_through) { + if !Self::tree_valid_pos_or_tag_id(region, pos, can_grow_through) { return false; } From b772f98501e26ccce090ef68c03ebe131c7b6b4c Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:55:14 +0200 Subject: [PATCH 18/54] add support for startHeight of vanilla --- steel-worldgen/src/structure/jigsaw.rs | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/steel-worldgen/src/structure/jigsaw.rs b/steel-worldgen/src/structure/jigsaw.rs index fdad0777139d..f008de1f9555 100644 --- a/steel-worldgen/src/structure/jigsaw.rs +++ b/steel-worldgen/src/structure/jigsaw.rs @@ -7,7 +7,7 @@ use std::{cmp::Reverse, mem}; use glam::IVec3; use rustc_hash::FxHashMap; use steel_registry::structure::{ - JigsawConfig, LiquidSettingsData, PoolAlias, StartHeight, StructureData, + JigsawConfig, LiquidSettingsData, PoolAlias, StartHeight, StructureData, VerticalAnchorData, }; use steel_registry::template_pool::{ JigsawOrientation, JointType, PoolElement, Projection, TemplateData, TemplatePoolData, @@ -114,10 +114,27 @@ pub fn resolve_aliases( map } -fn sample_start_height(config: &JigsawConfig, rng: &mut impl Random) -> i32 { +fn resolve_vertical_anchor(anchor: &VerticalAnchorData, min_y: i32, height: i32) -> i32 { + match anchor { + VerticalAnchorData::Absolute(y) => *y, + VerticalAnchorData::AboveBottom(offset) => min_y + *offset, + VerticalAnchorData::BelowTop(offset) => min_y + height - 1 - *offset, + } +} + +fn sample_start_height( + config: &JigsawConfig, + rng: &mut impl Random, + min_y: i32, + height: i32, +) -> i32 { match &config.start_height { - StartHeight::Constant(y) => *y, - StartHeight::Uniform { min, max } => rng.next_i32_between(*min, *max), + StartHeight::Constant(anchor) => resolve_vertical_anchor(anchor, min_y, height), + StartHeight::Uniform { min, max } => { + let min = resolve_vertical_anchor(min, min_y, height); + let max = resolve_vertical_anchor(max, min_y, height); + rng.next_i32_between(min, max) + } } } @@ -554,7 +571,7 @@ fn start_assembly( min_y: i32, max_y: i32, ) -> Option { - let start_y = sample_start_height(config, rng); + let start_y = sample_start_height(config, rng, min_y, max_y - min_y); let start_x = chunk_x * 16; let start_z = chunk_z * 16; let center_rotation = Rotation::get_random(rng); @@ -775,7 +792,8 @@ impl Structure for JigsawStructure { let mut alias_position_rng = LegacyRandom::from_seed(0); alias_position_rng.set_large_feature_seed(ctx.seed(), ctx.chunk_x(), ctx.chunk_z()); - let start_y = sample_start_height(config, &mut alias_position_rng); + let start_y = + sample_start_height(config, &mut alias_position_rng, ctx.min_y(), ctx.height()); let mut alias_source = LegacyRandom::from_seed(ctx.seed() as u64); let mut alias_rng = alias_source From b60475c7783a3dd6e9bed3d3eaa3fdd819ca0258 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:56:58 +0200 Subject: [PATCH 19/54] fix around range --- steel-worldgen/build/multi_noise.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/steel-worldgen/build/multi_noise.rs b/steel-worldgen/build/multi_noise.rs index 32f15d90f62d..103d7e15c5fe 100644 --- a/steel-worldgen/build/multi_noise.rs +++ b/steel-worldgen/build/multi_noise.rs @@ -34,15 +34,22 @@ impl<'de> Deserialize<'de> for ParameterRange { } } +impl std::ops::Deref for ParameterRange { + type Target = [f64; 2]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// Climate parameters for a biome entry. -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] struct BiomeParameters { - temperature: [f64; 2], - humidity: [f64; 2], - continentalness: [f64; 2], - erosion: [f64; 2], - depth: [f64; 2], - weirdness: [f64; 2], + temperature: ParameterRange, + humidity: ParameterRange, + continentalness: ParameterRange, + erosion: ParameterRange, + depth: ParameterRange, + weirdness: ParameterRange, offset: f64, } From 33feb818211b8be098ab2e13d5c35a1b34618a29 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:59:53 +0200 Subject: [PATCH 20/54] add missing field on structure --- .../src/structure_processor/data.rs | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/steel-registry/src/structure_processor/data.rs b/steel-registry/src/structure_processor/data.rs index 1feaa6419bdb..5947ea5233ac 100644 --- a/steel-registry/src/structure_processor/data.rs +++ b/steel-registry/src/structure_processor/data.rs @@ -1,6 +1,7 @@ //! Typed structure processor-list codec data. use serde::{Deserialize, Deserializer, de::Error as _}; +use simdnbt::owned::{NbtCompound, NbtList, NbtTag}; use steel_utils::{Identifier, value_providers::IntProvider}; use crate::shared_structs::{ @@ -42,8 +43,14 @@ pub enum StructureProcessorKind { #[serde(rename = "minecraft:block_age", alias = "block_age")] BlockAge { mossiness: f32 }, /// Keeps non-full structure blocks submerged in existing lava. - #[serde(rename = "minecraft:lava_submerged_block", alias = "lava_submerged_block")] + #[serde( + rename = "minecraft:lava_submerged_block", + alias = "lava_submerged_block" + )] LavaSubmergedBlock, + /// Replaces jigsaw blocks with their `final_state` block state during placement. + #[serde(rename = "minecraft:jigsaw_replacement", alias = "jigsaw_replacement")] + JigsawReplacement, /// Replaces stone ruin blocks with blackstone variants. #[serde(rename = "minecraft:blackstone_replace", alias = "blackstone_replace")] BlackstoneReplace, @@ -186,4 +193,67 @@ pub enum RuleBlockEntityModifierData { /// Appends loot table metadata to the output block entity. #[serde(rename = "minecraft:append_loot")] AppendLoot { loot_table: Identifier }, + /// Merges static NBT into the output block entity. + #[serde(rename = "minecraft:append_static")] + AppendStatic { + #[serde(deserialize_with = "deserialize_static_nbt_compound")] + data: NbtCompound, + }, +} + +fn deserialize_static_nbt_compound<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value = serde_json::Value::deserialize(deserializer)?; + json_value_to_nbt_compound(&value).map_err(D::Error::custom) +} + +fn json_value_to_nbt_compound(value: &serde_json::Value) -> Result { + let serde_json::Value::Object(object) = value else { + return Err("append_static data must be an object".to_owned()); + }; + + let mut compound = NbtCompound::new(); + for (key, value) in object { + compound.insert(key.as_str(), json_value_to_nbt_tag(value)?); + } + Ok(compound) +} + +fn json_value_to_nbt_tag(value: &serde_json::Value) -> Result { + match value { + serde_json::Value::Null => Err("null is not a valid NBT tag".to_owned()), + serde_json::Value::Bool(value) => Ok(NbtTag::Byte(i8::from(*value))), + serde_json::Value::Number(value) => json_number_to_nbt_tag(value), + serde_json::Value::String(value) => Ok(NbtTag::String(value.clone().into())), + serde_json::Value::Array(values) => { + let values = values + .iter() + .map(json_value_to_nbt_tag) + .collect::, _>>()?; + Ok(NbtTag::List(NbtList::from(values))) + } + serde_json::Value::Object(_) => Ok(NbtTag::Compound(json_value_to_nbt_compound(value)?)), + } +} + +fn json_number_to_nbt_tag(value: &serde_json::Number) -> Result { + if let Some(value) = value.as_i64() { + return i32::try_from(value) + .map(NbtTag::Int) + .or_else(|_| Ok(NbtTag::Long(value))); + } + + if let Some(value) = value.as_u64() { + return i32::try_from(value) + .map(NbtTag::Int) + .or_else(|_| i64::try_from(value).map(NbtTag::Long)) + .map_err(|_| format!("NBT integer value {value} does not fit i64")); + } + + value + .as_f64() + .map(NbtTag::Double) + .ok_or_else(|| format!("invalid NBT numeric value {value}")) } From 1b7e51a52ad2c804bfd37f04e6120aa0cbb83b30 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:00:13 +0200 Subject: [PATCH 21/54] add missing generation step --- steel-registry/src/structure.rs | 70 ++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/steel-registry/src/structure.rs b/steel-registry/src/structure.rs index 7f25e445b0d1..c549c61efee5 100644 --- a/steel-registry/src/structure.rs +++ b/steel-registry/src/structure.rs @@ -107,26 +107,47 @@ impl crate::RegistryEntry for StructureData { /// Structure generation step. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StructureGenerationStep { - /// `surface_structures`. - SurfaceStructures, + /// `raw_generation`. + RawGeneration, + /// `lakes`. + Lakes, + /// `local_modifications`. + LocalModifications, /// `underground_structures`. UndergroundStructures, + /// `surface_structures`. + SurfaceStructures, + /// `strongholds`. + Strongholds, + /// `underground_ores`. + UndergroundOres, /// `underground_decoration`. UndergroundDecoration, + /// `fluid_springs`. + FluidSprings, + /// `vegetal_decoration`. + VegetalDecoration, + /// `top_layer_modification`. + TopLayerModification, } impl StructureGenerationStep { /// Decoration-stage ordinal used by vanilla `GenerationStep.Decoration`. /// - /// Structure JSON only names the three structure-capable decoration stages; - /// feature generation still runs all eleven decoration stages, so these - /// values intentionally leave the vanilla gaps intact. #[must_use] pub const fn decoration_ordinal(self) -> usize { match self { + Self::RawGeneration => 0, + Self::Lakes => 1, + Self::LocalModifications => 2, Self::UndergroundStructures => 3, Self::SurfaceStructures => 4, + Self::Strongholds => 5, + Self::UndergroundOres => 6, Self::UndergroundDecoration => 7, + Self::FluidSprings => 8, + Self::VegetalDecoration => 9, + Self::TopLayerModification => 10, } } } @@ -188,6 +209,15 @@ mod tests { #[test] fn structure_generation_steps_use_vanilla_decoration_ordinals() { + assert_eq!( + StructureGenerationStep::RawGeneration.decoration_ordinal(), + 0 + ); + assert_eq!(StructureGenerationStep::Lakes.decoration_ordinal(), 1); + assert_eq!( + StructureGenerationStep::LocalModifications.decoration_ordinal(), + 2 + ); assert_eq!( StructureGenerationStep::UndergroundStructures.decoration_ordinal(), 3 @@ -196,10 +226,27 @@ mod tests { StructureGenerationStep::SurfaceStructures.decoration_ordinal(), 4 ); + assert_eq!(StructureGenerationStep::Strongholds.decoration_ordinal(), 5); + assert_eq!( + StructureGenerationStep::UndergroundOres.decoration_ordinal(), + 6 + ); assert_eq!( StructureGenerationStep::UndergroundDecoration.decoration_ordinal(), 7 ); + assert_eq!( + StructureGenerationStep::FluidSprings.decoration_ordinal(), + 8 + ); + assert_eq!( + StructureGenerationStep::VegetalDecoration.decoration_ordinal(), + 9 + ); + assert_eq!( + StructureGenerationStep::TopLayerModification.decoration_ordinal(), + 10 + ); } } @@ -285,13 +332,16 @@ pub struct JigsawConfig { pub liquid_settings: LiquidSettingsData, } -/// Start height configuration used by currently-generated jigsaw structures. +/// Start height configuration for jigsaw structures. #[derive(Debug, Clone)] pub enum StartHeight { - /// Fixed absolute Y. - Constant(i32), - /// Uniform random between min and max (inclusive). - Uniform { min: i32, max: i32 }, + /// Fixed vertical anchor. + Constant(VerticalAnchorData), + /// Uniform random between resolved anchors, inclusive. + Uniform { + min: VerticalAnchorData, + max: VerticalAnchorData, + }, } /// Dimension padding (how close pieces can be to world height limits). From 0413cc08ac68c6cd8c802e778ff85db14fbfe2b7 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:12:16 +0200 Subject: [PATCH 22/54] Update data.rs --- steel-registry/src/feature/data.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/steel-registry/src/feature/data.rs b/steel-registry/src/feature/data.rs index 7b60fd5b6240..1154e41d7f88 100644 --- a/steel-registry/src/feature/data.rs +++ b/steel-registry/src/feature/data.rs @@ -8,6 +8,7 @@ use super::{ConfiguredFeatureEntryRef, PlacedFeatureEntryRef}; use crate::blocks::BlockRef; use crate::fluid::FluidRef; +use crate::template_pool::ProcessorList; use glam::IVec3; use steel_utils::{ Direction, Identifier, Rotation, @@ -89,6 +90,7 @@ pub enum ConfiguredFeatureKind { PointedDripstone(PointedDripstoneConfiguration), RandomBooleanSelector(RandomBooleanSelectorConfiguration), RandomSelector(RandomSelectorConfiguration), + ReplaceSingleBlock(ReplaceBlockConfiguration), RootSystem(RootSystemConfiguration), ScatteredOre(OreConfiguration), SculkPatch(SculkPatchConfiguration), @@ -495,8 +497,8 @@ pub struct FallenTreeConfiguration { pub struct FossilConfiguration { pub fossil_structures: Vec, pub overlay_structures: Vec, - pub fossil_processors: Identifier, - pub overlay_processors: Identifier, + pub fossil_processors: ProcessorList, + pub overlay_processors: ProcessorList, pub max_empty_corners_allowed: i32, } @@ -617,6 +619,11 @@ pub struct OreConfiguration { pub discard_chance_on_air_exposure: f32, } +#[derive(Debug, Clone)] +pub struct ReplaceBlockConfiguration { + pub targets: Vec, +} + #[derive(Debug, Clone)] pub struct OreTarget { pub target: RuleTest, @@ -626,6 +633,7 @@ pub struct OreTarget { #[derive(Debug, Clone)] pub enum RuleTest { BlockMatch { block: BlockRef }, + BlockStateMatch { block_state: BlockStateData }, RandomBlockMatch { block: BlockRef, probability: f32 }, TagMatch { tag: Identifier }, } @@ -873,7 +881,7 @@ pub struct MegaPineFoliagePlacer { pub struct RandomSpreadFoliagePlacer { pub radius: IntProvider, pub offset: IntProvider, - pub foliage_height: i32, + pub foliage_height: IntProvider, pub leaf_placement_attempts: i32, } From 58b394532ed350967058f5eac99751857ed098ac Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:13:54 +0200 Subject: [PATCH 23/54] Update fossil.rs --- .../src/worldgen/feature/features/fossil.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/steel-core/src/worldgen/feature/features/fossil.rs b/steel-core/src/worldgen/feature/features/fossil.rs index d9a26d8ac8f3..ccc7f3f5ab22 100644 --- a/steel-core/src/worldgen/feature/features/fossil.rs +++ b/steel-core/src/worldgen/feature/features/fossil.rs @@ -9,6 +9,7 @@ use super::super::prelude::*; use super::super::runner::FeatureDecorationRunner; use steel_registry::structure::LiquidSettingsData; use steel_registry::structure_processor::StructureProcessorKind; +use steel_registry::template_pool::ProcessorList; const VANILLA_ROTATIONS: [Rotation; 4] = [ Rotation::None, @@ -129,13 +130,19 @@ impl FeatureDecorationRunner { fn structure_processors<'a>( registry: &'a Registry, - key: &Identifier, + processors: &'a ProcessorList, context: &str, ) -> &'a [StructureProcessorKind] { - let Some(processor_list) = registry.structure_processors.by_key(key) else { - panic!("{context} references unknown processor list {key}"); - }; - &processor_list.data.processors + match processors { + ProcessorList::Empty => &[], + ProcessorList::Direct(processors) => processors, + ProcessorList::Registry(key) => { + let Some(processor_list) = registry.structure_processors.by_key(key) else { + panic!("{context} references unknown processor list {key}"); + }; + &processor_list.data.processors + } + } } fn lowest_fossil_surface_y( From e1f2bf78b55d1d1a3db4d6eed1de70e78eea5093 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:14:26 +0200 Subject: [PATCH 24/54] tiring to make clean commit --- .../worldgen/feature/features/tree/roots.rs | 28 ++--- steel-core/src/worldgen/feature/tests.rs | 111 +++++++++++------- steel-core/src/worldgen/generators/vanilla.rs | 2 + 3 files changed, 78 insertions(+), 63 deletions(-) diff --git a/steel-core/src/worldgen/feature/features/tree/roots.rs b/steel-core/src/worldgen/feature/features/tree/roots.rs index c9b9c7667b98..618289353405 100644 --- a/steel-core/src/worldgen/feature/features/tree/roots.rs +++ b/steel-core/src/worldgen/feature/features/tree/roots.rs @@ -183,11 +183,11 @@ impl FeatureDecorationRunner { placer: &MangroveRootPlacer, placement: &mut TreePlacement, ) { - if Self::block_matches_identifiers( - registry, - region.block_state(pos), - &placer.mangrove_root_placement.muddy_roots_in, - ) { + if placer + .mangrove_root_placement + .muddy_roots_in + .contains(region.block_state(pos).get_block()) + { let state = Self::sample_block_state_provider( region, registry, @@ -210,31 +210,19 @@ impl FeatureDecorationRunner { placement.set_root(region, pos, state); let above = pos.above(); - if random.next_f32() < placer.above_root_placement.above_root_placement_chance + if let Some(above_root) = &placer.above_root_placement + && random.next_f32() < above_root.above_root_placement_chance && region.block_state(above).is_air() { let state = Self::sample_block_state_provider( region, registry, random, - &placer.above_root_placement.above_root_provider, + &above_root.above_root_provider, above, ); let state = Self::copy_waterlogged_from(region, above, state); placement.set_root(region, above, state); } } - - fn block_matches_identifiers( - registry: &Registry, - state: BlockStateId, - blocks: &[Identifier], - ) -> bool { - blocks.iter().any(|block_key| { - let Some(block) = registry.blocks.by_key(block_key) else { - panic!("mangrove root placement references unknown block {block_key}"); - }; - state.get_block() == block - }) - } } diff --git a/steel-core/src/worldgen/feature/tests.rs b/steel-core/src/worldgen/feature/tests.rs index d154a93c69c8..ba7484cf97bf 100644 --- a/steel-core/src/worldgen/feature/tests.rs +++ b/steel-core/src/worldgen/feature/tests.rs @@ -98,11 +98,13 @@ fn structures_for_decoration_step_use_registry_order_inside_vanilla_step() { let underground_decoration = FeatureDecorationRunner::structures_for_decoration_step(®istry, 7); + let underground_paths: Vec<_> = underground + .iter() + .filter(|s| s.key.namespace == "minecraft") + .map(|structure| structure.key.path.as_ref()) + .collect(); assert_eq!( - underground - .iter() - .map(|structure| structure.key.path.as_ref()) - .collect::>(), + underground_paths, [ "buried_treasure", "mineshaft", @@ -111,45 +113,55 @@ fn structures_for_decoration_step_use_registry_order_inside_vanilla_step() { "trial_chambers", ] ); + + let surface_paths: Vec<_> = surface + .iter() + .filter(|s| s.key.namespace == "minecraft") + .map(|structure| structure.key.path.as_ref()) + .collect(); + let mut expected_surface = vec![ + "bastion_remnant", + "desert_pyramid", + "end_city", + "igloo", + "jungle_pyramid", + "mansion", + "monument", + "ocean_ruin_cold", + "ocean_ruin_warm", + "pillager_outpost", + "ruined_portal", + "ruined_portal_desert", + "ruined_portal_jungle", + "ruined_portal_mountain", + "ruined_portal_nether", + "ruined_portal_ocean", + "ruined_portal_swamp", + "shipwreck", + "shipwreck_beached", + "stronghold", + "swamp_hut", + "village_desert", + "village_plains", + "village_savanna", + "village_snowy", + "village_taiga", + ]; + if matches!( + steel_worldgen::multi_noise::END_BIOME_SOURCE_KIND, + steel_worldgen::multi_noise::EndBiomeSourceKind::MultiNoise + ) { + expected_surface.retain(|&s| s != "end_city"); + } + assert_eq!(surface_paths, expected_surface); + + let underground_decoration_paths: Vec<_> = underground_decoration + .iter() + .filter(|s| s.key.namespace == "minecraft") + .map(|structure| structure.key.path.as_ref()) + .collect(); assert_eq!( - surface - .iter() - .map(|structure| structure.key.path.as_ref()) - .collect::>(), - [ - "bastion_remnant", - "desert_pyramid", - "end_city", - "igloo", - "jungle_pyramid", - "mansion", - "monument", - "ocean_ruin_cold", - "ocean_ruin_warm", - "pillager_outpost", - "ruined_portal", - "ruined_portal_desert", - "ruined_portal_jungle", - "ruined_portal_mountain", - "ruined_portal_nether", - "ruined_portal_ocean", - "ruined_portal_swamp", - "shipwreck", - "shipwreck_beached", - "stronghold", - "swamp_hut", - "village_desert", - "village_plains", - "village_savanna", - "village_snowy", - "village_taiga", - ] - ); - assert_eq!( - underground_decoration - .iter() - .map(|structure| structure.key.path.as_ref()) - .collect::>(), + underground_decoration_paths, ["ancient_city", "fortress", "nether_fossil"] ); @@ -168,7 +180,20 @@ fn structures_for_decoration_step_use_registry_order_inside_vanilla_step() { .iter() .all(|structure| structure.step.decoration_ordinal() == 7) ); - assert!(FeatureDecorationRunner::structures_for_decoration_step(®istry, 0).is_empty()); + if matches!( + steel_worldgen::multi_noise::END_BIOME_SOURCE_KIND, + steel_worldgen::multi_noise::EndBiomeSourceKind::MultiNoise + ) { + assert_eq!( + FeatureDecorationRunner::structures_for_decoration_step(®istry, 0) + .iter() + .map(|s| s.key.path.as_ref()) + .collect::>(), + vec!["end_city"] + ); + } else { + assert!(FeatureDecorationRunner::structures_for_decoration_step(®istry, 0).is_empty()); + } } #[test] diff --git a/steel-core/src/worldgen/generators/vanilla.rs b/steel-core/src/worldgen/generators/vanilla.rs index de18053ed6e6..b73a964375a8 100644 --- a/steel-core/src/worldgen/generators/vanilla.rs +++ b/steel-core/src/worldgen/generators/vanilla.rs @@ -465,6 +465,8 @@ impl ChunkGenerator for VanillaGenerator { // Start scanning from one above the highest non-air block let mut start_height = chunk.height_at(HeightmapType::WorldSurfaceWg, local_x, local_z); + let max_valid_y = min_y + (section_count * 16) as i32 - 1; + start_height = start_height.min(max_valid_y); // Column-local Voronoi cache for fuzzed biome lookups. let mut biome_col = biome_data.as_deref().map(|biome_data| { From 0b26a8c7f43e8741e52ea9aac7474027c6ba9556 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:20:00 +0200 Subject: [PATCH 25/54] Update biomes.rs --- steel-registry/build/biomes.rs | 89 +++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/steel-registry/build/biomes.rs b/steel-registry/build/biomes.rs index 12a53e97779e..b8bd1f2fd6d6 100644 --- a/steel-registry/build/biomes.rs +++ b/steel-registry/build/biomes.rs @@ -33,6 +33,64 @@ where } } +fn deserialize_identifier_or_vanilla<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let raw = String::deserialize(deserializer)?; + Identifier::parse_or_vanilla(&raw).map_err(D::Error::custom) +} + +fn deserialize_identifier_vec_or_single<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let raw = VecOrSingle::::deserialize(deserializer)?; + match raw { + VecOrSingle::Vec(values) => values + .into_iter() + .map(|value| Identifier::parse_or_vanilla(&value).map_err(D::Error::custom)) + .collect::, _>>() + .map(VecOrSingle::Vec), + VecOrSingle::Single(value) => Identifier::parse_or_vanilla(&value) + .map(VecOrSingle::Single) + .map_err(D::Error::custom), + } +} + +fn deserialize_identifier_grid<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let raw = Vec::>::deserialize(deserializer)?; + raw.into_iter() + .map(|row| { + row.into_iter() + .map(|value| Identifier::parse_or_vanilla(&value).map_err(D::Error::custom)) + .collect() + }) + .collect() +} + +fn deserialize_identifier_map<'de, D, T>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + let raw = FxHashMap::::deserialize(deserializer)?; + raw.into_iter() + .map(|(key, value)| { + Identifier::parse_or_vanilla(&key) + .map(|key| (key, value)) + .map_err(D::Error::custom) + }) + .collect() +} + /// Parse a hex color string (#RRGGBB) to an i32 RGB value fn parse_hex_color(hex: &str) -> i32 { if let Some(hex_str) = hex.strip_prefix('#') { @@ -70,10 +128,12 @@ pub struct BiomeJson { creature_spawn_probability: f32, #[serde(default)] spawners: FxHashMap>, - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_identifier_map")] spawn_costs: FxHashMap, + #[serde(deserialize_with = "deserialize_identifier_vec_or_single")] carvers: VecOrSingle, + #[serde(deserialize_with = "deserialize_identifier_grid")] features: Vec>, } @@ -165,7 +225,10 @@ impl From for BiomeEffects { #[derive(Serialize, Deserialize, Debug)] pub struct SpawnerData { - #[serde(rename = "type")] + #[serde( + rename = "type", + deserialize_with = "deserialize_identifier_or_vanilla" + )] entity_type: Identifier, weight: i32, #[serde(rename = "minCount")] @@ -208,17 +271,20 @@ pub struct Music { replace_current_music: bool, max_delay: i32, min_delay: i32, + #[serde(deserialize_with = "deserialize_identifier_or_vanilla")] sound: Identifier, } #[derive(Serialize, Deserialize, Debug)] pub struct AdditionsSound { + #[serde(deserialize_with = "deserialize_identifier_or_vanilla")] sound: Identifier, tick_chance: f64, } #[derive(Serialize, Deserialize, Debug)] pub struct MoodSound { + #[serde(deserialize_with = "deserialize_identifier_or_vanilla")] sound: Identifier, tick_delay: i32, block_search_extent: i32, @@ -233,7 +299,10 @@ pub struct Particle { #[derive(Serialize, Deserialize, Debug)] pub struct ParticleOptions { - #[serde(rename = "type")] + #[serde( + rename = "type", + deserialize_with = "deserialize_identifier_or_vanilla" + )] particle_type: Identifier, } @@ -241,6 +310,7 @@ pub struct ParticleOptions { struct BackgroundMusicEntry { max_delay: i32, min_delay: i32, + #[serde(deserialize_with = "deserialize_identifier_or_vanilla")] sound: Identifier, } @@ -552,10 +622,17 @@ pub(crate) fn build() -> TokenStream { // Generate static biome definitions let mut register_stream = TokenStream::new(); for (biome_name, biome) in &biomes { - let biome_ident = Ident::new(&biome_name.to_shouty_snake_case(), Span::call_site()); - let biome_name_str = biome_name.clone(); + let identifier = crate::generator_functions::parse_loose_identifier(biome_name) + .unwrap_or_else(|e| panic!("invalid biome name {biome_name}: {e}")); + let key = crate::generator_functions::generate_static_identifier(&identifier); + let biome_ident_str = if identifier.namespace == steel_utils::Identifier::VANILLA_NAMESPACE + { + identifier.path.to_shouty_snake_case() + } else { + biome_name.replace([':', '/'], "_").to_shouty_snake_case() + }; + let biome_ident = Ident::new(&biome_ident_str, Span::call_site()); - let key = quote! { Identifier::vanilla_static(#biome_name_str) }; let has_precipitation = biome.has_precipitation; let temperature = biome.temperature; let downfall = biome.downfall; From 095eea101252f3145789a2f1a4cfd06bd69f8040 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:20:13 +0200 Subject: [PATCH 26/54] Update biomes.rs --- steel-registry/build/biomes.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/steel-registry/build/biomes.rs b/steel-registry/build/biomes.rs index b8bd1f2fd6d6..4cabbd2cd650 100644 --- a/steel-registry/build/biomes.rs +++ b/steel-registry/build/biomes.rs @@ -7,7 +7,8 @@ use crate::generator_functions::{ use heck::ToShoutySnakeCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use serde::{Deserialize, Serialize}; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use steel_utils::Identifier; @@ -663,7 +664,6 @@ pub(crate) fn build() -> TokenStream { id: OnceLock::new(), }); }); - let biome_ident = Ident::new(&biome_name.to_shouty_snake_case(), Span::call_site()); register_stream.extend(quote! { registry.register(&#biome_ident); }); From 9b4c99a5fed92839d9977a29c36240fc7f9348a0 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:21:57 +0200 Subject: [PATCH 27/54] Update block_tags.rs --- steel-registry/build/block_tags.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/steel-registry/build/block_tags.rs b/steel-registry/build/block_tags.rs index a67c48f53e37..5b3aba38ac02 100644 --- a/steel-registry/build/block_tags.rs +++ b/steel-registry/build/block_tags.rs @@ -19,7 +19,7 @@ fn read_all_fabric_tags(tag_file: &str) -> FxHashMap> { && let Ok(content) = fs::read_to_string(tag_file) { let tag: TagFile = serde_json::from_str(&content) - .unwrap_or_else(|e| panic!("Failed to parse {}: {}", tag_file, e)); + .unwrap_or_else(|e| panic!("Failed to parse {tag_file}: {e}")); return tag.block; } FxHashMap::default() From e297f28189a2bdc71571d73235997af59f0247f7 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:22:20 +0200 Subject: [PATCH 28/54] Update carvers.rs --- steel-registry/build/carvers.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/steel-registry/build/carvers.rs b/steel-registry/build/carvers.rs index a3ed05a9aeaf..f8a2a948f85b 100644 --- a/steel-registry/build/carvers.rs +++ b/steel-registry/build/carvers.rs @@ -188,8 +188,8 @@ fn parse_replaceable_tag(s: &str) -> Identifier { let stripped = s .strip_prefix('#') .unwrap_or_else(|| panic!("carver `replaceable` must be a `#tag` reference, got `{s}`")); - let (ns, path) = stripped.split_once(':').unwrap_or(("minecraft", stripped)); - Identifier::new(ns.to_owned(), path.to_owned()) + Identifier::parse_or_vanilla(stripped) + .unwrap_or_else(|error| panic!("invalid carver replaceable tag {s}: {error}")) } fn generate_base(base: &CarverConfigBaseJson) -> TokenStream { @@ -318,8 +318,16 @@ pub(crate) fn build() -> TokenStream { let mut register = TokenStream::new(); for (name, kind) in &entries { - let ident = Ident::new(&name.to_shouty_snake_case(), Span::call_site()); - let key = quote! { Identifier::vanilla_static(#name) }; + let identifier = crate::generator_functions::parse_loose_identifier(name) + .unwrap_or_else(|e| panic!("invalid configured carver name {name}: {e}")); + let key = crate::generator_functions::generate_static_identifier(&identifier); + let ident_str = if identifier.namespace == steel_utils::Identifier::VANILLA_NAMESPACE { + identifier.path.to_shouty_snake_case() + } else { + name.replace([':', '/'], "_").to_shouty_snake_case() + }; + let ident = Ident::new(&ident_str, Span::call_site()); + stream.extend(quote! { pub static #ident: LazyLock = LazyLock::new(|| ConfiguredCarver { key: #key, From 67ecc9ce71d1a305028df484be7c397a53ac5fae Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:24:54 +0200 Subject: [PATCH 29/54] add alias --- steel-registry/build/dimension_types.rs | 90 +++++++++++++++++++------ 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/steel-registry/build/dimension_types.rs b/steel-registry/build/dimension_types.rs index 5dc33d5d32b8..dda10d219203 100644 --- a/steel-registry/build/dimension_types.rs +++ b/steel-registry/build/dimension_types.rs @@ -41,53 +41,101 @@ pub struct DimensionTypeJson { #[derive(Deserialize, Debug, Default)] #[serde(default)] struct DimensionAttributes { - #[serde(rename = "minecraft:gameplay/respawn_anchor_works")] + #[serde( + rename = "minecraft:gameplay/respawn_anchor_works", + alias = "gameplay/respawn_anchor_works" + )] respawn_anchor_works: Option, - #[serde(rename = "minecraft:gameplay/can_start_raid")] + #[serde( + rename = "minecraft:gameplay/can_start_raid", + alias = "gameplay/can_start_raid" + )] can_start_raid: Option, - #[serde(rename = "minecraft:visual/cloud_height")] + #[serde( + rename = "minecraft:visual/cloud_height", + alias = "visual/cloud_height" + )] cloud_height: Option, - #[serde(rename = "minecraft:visual/sky_color")] + #[serde(rename = "minecraft:visual/sky_color", alias = "visual/sky_color")] sky_color: Option, - #[serde(rename = "minecraft:visual/fog_color")] + #[serde(rename = "minecraft:visual/fog_color", alias = "visual/fog_color")] fog_color: Option, - #[serde(rename = "minecraft:visual/cloud_color")] + #[serde(rename = "minecraft:visual/cloud_color", alias = "visual/cloud_color")] cloud_color: Option, // New visual attributes - #[serde(rename = "minecraft:visual/ambient_light_color")] + #[serde( + rename = "minecraft:visual/ambient_light_color", + alias = "visual/ambient_light_color" + )] ambient_light_color: Option, - #[serde(rename = "minecraft:visual/sky_light_color")] + #[serde( + rename = "minecraft:visual/sky_light_color", + alias = "visual/sky_light_color" + )] sky_light_color: Option, - #[serde(rename = "minecraft:visual/sky_light_factor")] + #[serde( + rename = "minecraft:visual/sky_light_factor", + alias = "visual/sky_light_factor" + )] sky_light_factor: Option, - #[serde(rename = "minecraft:visual/fog_start_distance")] + #[serde( + rename = "minecraft:visual/fog_start_distance", + alias = "visual/fog_start_distance" + )] fog_start_distance: Option, - #[serde(rename = "minecraft:visual/fog_end_distance")] + #[serde( + rename = "minecraft:visual/fog_end_distance", + alias = "visual/fog_end_distance" + )] fog_end_distance: Option, - #[serde(rename = "minecraft:visual/default_dripstone_particle")] + #[serde( + rename = "minecraft:visual/default_dripstone_particle", + alias = "visual/default_dripstone_particle" + )] default_dripstone_particle: Option, // New gameplay attributes - #[serde(rename = "minecraft:gameplay/fast_lava")] + #[serde(rename = "minecraft:gameplay/fast_lava", alias = "gameplay/fast_lava")] fast_lava: Option, - #[serde(rename = "minecraft:gameplay/piglins_zombify")] + #[serde( + rename = "minecraft:gameplay/piglins_zombify", + alias = "gameplay/piglins_zombify" + )] piglins_zombify: Option, - #[serde(rename = "minecraft:gameplay/sky_light_level")] + #[serde( + rename = "minecraft:gameplay/sky_light_level", + alias = "gameplay/sky_light_level" + )] sky_light_level: Option, - #[serde(rename = "minecraft:gameplay/snow_golem_melts")] + #[serde( + rename = "minecraft:gameplay/snow_golem_melts", + alias = "gameplay/snow_golem_melts" + )] snow_golem_melts: Option, - #[serde(rename = "minecraft:gameplay/water_evaporates")] + #[serde( + rename = "minecraft:gameplay/water_evaporates", + alias = "gameplay/water_evaporates" + )] water_evaporates: Option, - #[serde(rename = "minecraft:gameplay/nether_portal_spawns_piglin")] + #[serde( + rename = "minecraft:gameplay/nether_portal_spawns_piglin", + alias = "gameplay/nether_portal_spawns_piglin" + )] nether_portal_spawns_piglin: Option, - #[serde(rename = "minecraft:gameplay/bed_rule")] + #[serde(rename = "minecraft:gameplay/bed_rule", alias = "gameplay/bed_rule")] bed_rule: Option, // Audio attributes - #[serde(rename = "minecraft:audio/ambient_sounds")] + #[serde( + rename = "minecraft:audio/ambient_sounds", + alias = "audio/ambient_sounds" + )] ambient_sounds: Option, - #[serde(rename = "minecraft:audio/background_music")] + #[serde( + rename = "minecraft:audio/background_music", + alias = "audio/background_music" + )] background_music: Option, } From 7c163a0baf780482dc070a15f15a0b03bd83396b Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:26:33 +0200 Subject: [PATCH 30/54] Update feature_data.rs --- steel-registry/build/feature_data.rs | 160 +++++++++++++++++++++------ 1 file changed, 125 insertions(+), 35 deletions(-) diff --git a/steel-registry/build/feature_data.rs b/steel-registry/build/feature_data.rs index 719858901891..071bb4259a3b 100644 --- a/steel-registry/build/feature_data.rs +++ b/steel-registry/build/feature_data.rs @@ -8,6 +8,7 @@ use crate::generator_functions::parse_loose_identifier; use crate::shared_structs::deserialize_tag_identifier; pub use crate::shared_structs::{BlockStateData, FluidStateData}; +use crate::template_pools::ProcessorsJson; use serde::{Deserialize, Deserializer, de::Error as _}; use serde_json::Value; use steel_utils::{ @@ -19,6 +20,21 @@ fn parse_loose_identifier_de(raw: &str) -> Result>(deserializer: D) -> Result { + let value = Value::deserialize(deserializer)?; + let Some(number) = value.as_f64() else { + return Err(D::Error::custom("expected numeric value")); + }; + + if !number.is_finite() || number < i32::MIN as f64 || number > i32::MAX as f64 { + return Err(D::Error::custom(format!( + "integer value out of range: {number}" + ))); + } + + Ok(number as i32) +} + fn normalize_loose_identifier(raw: &str) -> String { parse_loose_identifier(raw) .unwrap_or_else(|error| panic!("invalid loose feature identifier {raw}: {error}")) @@ -93,8 +109,7 @@ pub fn parse_placed_feature_json(registry_id: &str, content: &str) -> PlacedFeat } /// A configured feature reference, either a registry key or an inline configured feature. -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] +#[derive(Debug, Clone)] pub enum ConfiguredFeatureRef { /// Registry-backed configured feature. Reference(Identifier), @@ -103,8 +118,20 @@ pub enum ConfiguredFeatureRef { } /// A placed feature reference, either a registry key or an inline placed feature. -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] +impl<'de> Deserialize<'de> for ConfiguredFeatureRef { + fn deserialize>(deserializer: D) -> Result { + let value = Value::deserialize(deserializer)?; + if let Some(id) = value.as_str() { + return parse_loose_identifier_de(id).map(Self::Reference); + } + serde_json::from_value(value) + .map(Box::new) + .map(Self::Inline) + .map_err(D::Error::custom) + } +} + +#[derive(Debug, Clone)] pub enum PlacedFeatureRef { /// Registry-backed placed feature. Reference(Identifier), @@ -112,6 +139,19 @@ pub enum PlacedFeatureRef { Inline(Box), } +impl<'de> Deserialize<'de> for PlacedFeatureRef { + fn deserialize>(deserializer: D) -> Result { + let value = Value::deserialize(deserializer)?; + if let Some(id) = value.as_str() { + return parse_loose_identifier_de(id).map(Self::Reference); + } + serde_json::from_value(value) + .map(Box::new) + .map(Self::Inline) + .map_err(D::Error::custom) + } +} + /// A placed feature: configured feature plus ordered placement modifiers. #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] @@ -166,10 +206,12 @@ pub enum ConfiguredFeatureKind { MultifaceGrowth(MultifaceGrowthConfiguration), NetherForestVegetation(NetherForestVegetationConfiguration), NetherrackReplaceBlobs(NetherrackReplaceBlobsConfiguration), + NoOp, Ore(OreConfiguration), PointedDripstone(PointedDripstoneConfiguration), RandomBooleanSelector(RandomBooleanSelectorConfiguration), RandomSelector(RandomSelectorConfiguration), + ReplaceSingleBlock(ReplaceBlockConfiguration), RootSystem(RootSystemConfiguration), ScatteredOre(OreConfiguration), SculkPatch(SculkPatchConfiguration), @@ -290,6 +332,7 @@ fn deserialize_configured_feature_kind( "minecraft:netherrack_replace_blobs" => ConfiguredFeatureKind::NetherrackReplaceBlobs( parse!(NetherrackReplaceBlobsConfiguration)?, ), + "minecraft:no_op" => ConfiguredFeatureKind::NoOp, "minecraft:ore" => ConfiguredFeatureKind::Ore(parse!(OreConfiguration)?), "minecraft:pointed_dripstone" => { ConfiguredFeatureKind::PointedDripstone(parse!(PointedDripstoneConfiguration)?) @@ -300,6 +343,9 @@ fn deserialize_configured_feature_kind( "minecraft:random_selector" => { ConfiguredFeatureKind::RandomSelector(parse!(RandomSelectorConfiguration)?) } + "minecraft:replace_single_block" => { + ConfiguredFeatureKind::ReplaceSingleBlock(parse!(ReplaceBlockConfiguration)?) + } "minecraft:weighted_random_selector" => ConfiguredFeatureKind::WeightedRandomSelector( parse!(WeightedRandomFeatureConfiguration)?, ), @@ -354,11 +400,21 @@ fn deserialize_configured_feature_kind( }) } -/// Identifier list that accepts vanilla's single-or-list codec shape. +/// Vanilla holder set for blocks, preserving tag-vs-entry semantics. +#[derive(Debug, Clone)] +pub enum BlockHolderSet { + Tag(Identifier), + Entries(Vec), +} + +/// Vanilla holder set for fluids, preserving tag-vs-entry semantics. #[derive(Debug, Clone)] -pub struct IdentifierList(pub Vec); +pub enum FluidHolderSet { + Tag(Identifier), + Entries(Vec), +} -impl<'de> Deserialize<'de> for IdentifierList { +impl<'de> Deserialize<'de> for BlockHolderSet { fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] #[serde(untagged)] @@ -367,26 +423,28 @@ impl<'de> Deserialize<'de> for IdentifierList { Many(Vec), } - Ok(match Raw::deserialize(deserializer)? { - Raw::Single(value) => Self(vec![parse_loose_identifier_de(&value)?]), - Raw::Many(values) => Self( - values + match Raw::deserialize(deserializer)? { + Raw::Single(value) => { + if let Some(tag) = value.strip_prefix('#') { + let tag = parse_loose_identifier_de(tag)?; + Ok(Self::Tag(tag)) + } else { + let entry = parse_loose_identifier_de(&value)?; + Ok(Self::Entries(vec![entry])) + } + } + Raw::Many(values) => { + let entries = values .iter() - .map(|value| parse_loose_identifier_de(value.as_str())) - .collect::>()?, - ), - }) + .map(|value| parse_loose_identifier_de(value)) + .collect::>()?; + Ok(Self::Entries(entries)) + } + } } } -/// Vanilla holder set for blocks, preserving tag-vs-entry semantics. -#[derive(Debug, Clone)] -pub enum BlockHolderSet { - Tag(Identifier), - Entries(Vec), -} - -impl<'de> Deserialize<'de> for BlockHolderSet { +impl<'de> Deserialize<'de> for FluidHolderSet { fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] #[serde(untagged)] @@ -472,13 +530,13 @@ pub enum BlockPredicate { }, #[serde(rename = "minecraft:matching_blocks")] MatchingBlocks { - blocks: IdentifierList, + blocks: BlockHolderSet, #[serde(default = "default_offset")] offset: Offset, }, #[serde(rename = "minecraft:matching_fluids")] MatchingFluids { - fluids: IdentifierList, + fluids: FluidHolderSet, #[serde(default = "default_offset")] offset: Offset, }, @@ -574,6 +632,12 @@ pub struct NoiseProvider { pub scale: f32, pub seed: i64, pub states: Vec, + #[serde(default, rename = "slow_noise")] + pub _slow_noise: Option, + #[serde(default, rename = "slow_scale")] + pub _slow_scale: Option, + #[serde(default, rename = "variety")] + pub _variety: Option, } #[derive(Debug, Clone, Deserialize)] @@ -598,7 +662,7 @@ pub struct DualNoiseProvider { pub slow_noise: FeatureNoiseParameters, pub slow_scale: f32, pub states: Vec, - pub variety: [i32; 2], + pub variety: UniformIntProvider, } /// Feature placement modifiers. @@ -862,8 +926,8 @@ pub struct FallenTreeConfiguration { pub struct FossilConfiguration { pub fossil_structures: Vec, pub overlay_structures: Vec, - pub fossil_processors: Identifier, - pub overlay_processors: Identifier, + pub fossil_processors: ProcessorsJson, + pub overlay_processors: ProcessorsJson, pub max_empty_corners_allowed: i32, } @@ -929,7 +993,10 @@ pub struct GeodeCrackSettings { pub generate_crack_chance: f64, #[serde(default = "default_geode_base_crack_size")] pub base_crack_size: f64, - #[serde(default = "default_geode_crack_point_offset")] + #[serde( + default = "default_geode_crack_point_offset", + deserialize_with = "deserialize_vanilla_i32" + )] pub crack_point_offset: i32, } @@ -1072,7 +1139,7 @@ pub struct MultifaceGrowthConfiguration { pub can_place_on_wall: bool, #[serde(default = "default_multiface_chance_of_spreading")] pub chance_of_spreading: f32, - pub can_be_placed_on: Vec, + pub can_be_placed_on: BlockHolderSet, } const fn default_multiface_search_range() -> i32 { @@ -1108,6 +1175,12 @@ pub struct OreConfiguration { pub discard_chance_on_air_exposure: f32, } +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ReplaceBlockConfiguration { + pub targets: Vec, +} + #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct OreTarget { @@ -1120,6 +1193,10 @@ pub struct OreTarget { pub enum RuleTest { #[serde(rename = "minecraft:block_match")] BlockMatch { block: Identifier }, + #[serde(rename = "minecraft:blockstate_match")] + BlockStateMatch { block_state: BlockStateData }, + #[serde(rename = "minecraft:random_block_match")] + RandomBlockMatch { block: Identifier, probability: f32 }, #[serde(rename = "minecraft:tag_match")] TagMatch { tag: Identifier }, } @@ -1328,6 +1405,7 @@ const fn default_spring_hole_count() -> i32 { #[serde(deny_unknown_fields)] pub struct TreeConfiguration { pub trunk_provider: BlockStateProvider, + #[serde(alias = "dirt_provider")] pub below_trunk_provider: BlockStateProvider, pub foliage_provider: BlockStateProvider, pub trunk_placer: TrunkPlacer, @@ -1337,7 +1415,10 @@ pub struct TreeConfiguration { pub decorators: Vec, #[serde(default)] pub root_placer: Option, + #[serde(default)] pub ignore_vines: bool, + #[serde(default, rename = "force_dirt")] + pub _force_dirt: Option, } #[derive(Debug, Clone, Deserialize)] @@ -1377,10 +1458,15 @@ pub struct BendingTrunkPlacer { pub base_height: i32, pub height_rand_a: i32, pub height_rand_b: i32, + #[serde(default = "default_bending_trunk_min_height_for_leaves")] pub min_height_for_leaves: i32, pub bend_length: IntProvider, } +const fn default_bending_trunk_min_height_for_leaves() -> i32 { + 1 +} + #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct UpwardsBranchingTrunkPlacer { @@ -1438,6 +1524,10 @@ pub enum FoliagePlacer { pub struct FoliagePlacerBase { pub radius: IntProvider, pub offset: IntProvider, + #[serde(default, rename = "foliage_height")] + pub _foliage_height: Option, + #[serde(default, rename = "leaf_placement_attempts")] + pub _leaf_placement_attempts: Option, } #[derive(Debug, Clone, Deserialize)] @@ -1477,7 +1567,7 @@ pub struct MegaPineFoliagePlacer { pub struct RandomSpreadFoliagePlacer { pub radius: IntProvider, pub offset: IntProvider, - pub foliage_height: i32, + pub foliage_height: IntProvider, pub leaf_placement_attempts: i32, } @@ -1548,7 +1638,8 @@ pub enum RootPlacer { pub struct MangroveRootPlacer { pub trunk_offset_y: IntProvider, pub root_provider: BlockStateProvider, - pub above_root_placement: AboveRootPlacement, + #[serde(default)] + pub above_root_placement: Option, pub mangrove_root_placement: MangroveRootPlacement, } @@ -1562,9 +1653,8 @@ pub struct AboveRootPlacement { #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct MangroveRootPlacement { - #[serde(deserialize_with = "deserialize_tag_identifier")] - pub can_grow_through: Identifier, - pub muddy_roots_in: Vec, + pub can_grow_through: BlockHolderSet, + pub muddy_roots_in: BlockHolderSet, pub muddy_roots_provider: BlockStateProvider, pub max_root_width: i32, pub max_root_length: i32, From 47bbf6de4683a1638f5bdebed5f9f42503fb4192 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:29:52 +0200 Subject: [PATCH 31/54] Update features.rs --- steel-registry/build/features.rs | 82 +++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/steel-registry/build/features.rs b/steel-registry/build/features.rs index afee4d5be6e1..c7227118ad5d 100644 --- a/steel-registry/build/features.rs +++ b/steel-registry/build/features.rs @@ -48,12 +48,14 @@ fn generate_fluid_ref(identifier: &Identifier) -> TokenStream { } fn generate_configured_feature_entry_ref(identifier: &Identifier) -> TokenStream { - let ident = vanilla_registry_ident(identifier, "configured feature"); + let registry_id = format!("{}:{}", identifier.namespace, identifier.path); + let ident = registry_entry_ident(®istry_id); quote! { &crate::vanilla_configured_features::#ident } } fn generate_placed_feature_entry_ref(identifier: &Identifier) -> TokenStream { - let ident = vanilla_registry_ident(identifier, "placed feature"); + let registry_id = format!("{}:{}", identifier.namespace, identifier.path); + let ident = registry_entry_ident(®istry_id); quote! { &crate::vanilla_placed_features::#ident } } @@ -82,11 +84,6 @@ fn generate_offset(offset: &[i32; 3]) -> TokenStream { quote! { IVec3::new(#x, #y, #z) } } -fn generate_block_ref_list(list: &IdentifierList) -> TokenStream { - let values = generate_vec(&list.0, generate_block_ref); - quote! { BlockRefList(#values) } -} - fn generate_block_holder_set(set: &BlockHolderSet) -> TokenStream { match set { BlockHolderSet::Tag(tag) => { @@ -100,9 +97,17 @@ fn generate_block_holder_set(set: &BlockHolderSet) -> TokenStream { } } -fn generate_fluid_ref_list(list: &IdentifierList) -> TokenStream { - let values = generate_vec(&list.0, generate_fluid_ref); - quote! { FluidRefList(#values) } +fn generate_fluid_holder_set(set: &FluidHolderSet) -> TokenStream { + match set { + FluidHolderSet::Tag(tag) => { + let tag = generate_identifier(tag); + quote! { FluidHolderSet::Tag(#tag) } + } + FluidHolderSet::Entries(entries) => { + let entries = generate_vec(entries, generate_fluid_ref); + quote! { FluidHolderSet::Entries(#entries) } + } + } } fn generate_block_state_data(data: &BlockStateData) -> TokenStream { @@ -423,7 +428,8 @@ fn generate_dual_noise_provider(provider: &DualNoiseProvider) -> TokenStream { let slow_noise = generate_feature_noise_parameters(&provider.slow_noise); let slow_scale = provider.slow_scale; let states = generate_vec(&provider.states, generate_block_state_data); - let [variety_min, variety_max] = provider.variety; + let variety_min = provider.variety.min_inclusive; + let variety_max = provider.variety.max_inclusive; quote! { DualNoiseProvider { noise: #noise, @@ -532,12 +538,12 @@ fn generate_block_predicate(predicate: &BlockPredicate) -> TokenStream { quote! { BlockPredicate::MatchingBlockTag { tag: #tag, offset: #offset } } } BlockPredicate::MatchingBlocks { blocks, offset } => { - let blocks = generate_block_ref_list(blocks); + let blocks = generate_block_holder_set(blocks); let offset = generate_offset(offset); quote! { BlockPredicate::MatchingBlocks { blocks: #blocks, offset: #offset } } } BlockPredicate::MatchingFluids { fluids, offset } => { - let fluids = generate_fluid_ref_list(fluids); + let fluids = generate_fluid_holder_set(fluids); let offset = generate_offset(offset); quote! { BlockPredicate::MatchingFluids { fluids: #fluids, offset: #offset } } } @@ -814,6 +820,14 @@ fn generate_rule_test(rule: &RuleTest) -> TokenStream { let block = generate_block_ref(block); quote! { RuleTest::BlockMatch { block: #block } } } + RuleTest::BlockStateMatch { block_state } => { + let block_state = generate_block_state_data(block_state); + quote! { RuleTest::BlockStateMatch { block_state: #block_state } } + } + RuleTest::RandomBlockMatch { block, probability } => { + let block = generate_block_ref(block); + quote! { RuleTest::RandomBlockMatch { block: #block, probability: #probability } } + } RuleTest::TagMatch { tag } => { let tag = generate_identifier(tag); quote! { RuleTest::TagMatch { tag: #tag } } @@ -1023,7 +1037,7 @@ fn generate_foliage_placer(placer: &FoliagePlacer) -> TokenStream { FoliagePlacer::RandomSpread(placer) => { let radius = generate_int_provider(&placer.radius); let offset = generate_int_provider(&placer.offset); - let foliage_height = placer.foliage_height; + let foliage_height = generate_int_provider(&placer.foliage_height); let leaf_placement_attempts = placer.leaf_placement_attempts; quote! { FoliagePlacer::RandomSpread(RandomSpreadFoliagePlacer { @@ -1108,8 +1122,8 @@ fn generate_above_root_placement(placement: &AboveRootPlacement) -> TokenStream } fn generate_mangrove_root_placement(placement: &MangroveRootPlacement) -> TokenStream { - let can_grow_through = generate_identifier(&placement.can_grow_through); - let muddy_roots_in = generate_vec(&placement.muddy_roots_in, generate_identifier); + let can_grow_through = generate_block_holder_set(&placement.can_grow_through); + let muddy_roots_in = generate_block_holder_set(&placement.muddy_roots_in); let muddy_roots_provider = generate_block_state_provider(&placement.muddy_roots_provider); let max_root_width = placement.max_root_width; let max_root_length = placement.max_root_length; @@ -1131,7 +1145,8 @@ fn generate_root_placer(placer: &RootPlacer) -> TokenStream { RootPlacer::Mangrove(placer) => { let trunk_offset_y = generate_int_provider(&placer.trunk_offset_y); let root_provider = generate_block_state_provider(&placer.root_provider); - let above_root_placement = generate_above_root_placement(&placer.above_root_placement); + let above_root_placement = + generate_option(&placer.above_root_placement, generate_above_root_placement); let mangrove_root_placement = generate_mangrove_root_placement(&placer.mangrove_root_placement); quote! { @@ -1459,8 +1474,10 @@ fn generate_configured_feature_kind(kind: &ConfiguredFeatureKind) -> TokenStream ConfiguredFeatureKind::Fossil(config) => { let fossil_structures = generate_vec(&config.fossil_structures, generate_identifier); let overlay_structures = generate_vec(&config.overlay_structures, generate_identifier); - let fossil_processors = generate_identifier(&config.fossil_processors); - let overlay_processors = generate_identifier(&config.overlay_processors); + let fossil_processors = + crate::template_pools::gen_processors(Some(&config.fossil_processors), "fossil"); + let overlay_processors = + crate::template_pools::gen_processors(Some(&config.overlay_processors), "fossil"); let max_empty_corners_allowed = config.max_empty_corners_allowed; quote! { ConfiguredFeatureKind::Fossil(FossilConfiguration { @@ -1588,7 +1605,7 @@ fn generate_configured_feature_kind(kind: &ConfiguredFeatureKind) -> TokenStream let can_place_on_ceiling = config.can_place_on_ceiling; let can_place_on_wall = config.can_place_on_wall; let chance_of_spreading = config.chance_of_spreading; - let can_be_placed_on = generate_vec(&config.can_be_placed_on, generate_block_ref); + let can_be_placed_on = generate_block_holder_set(&config.can_be_placed_on); quote! { ConfiguredFeatureKind::MultifaceGrowth(MultifaceGrowthConfiguration { block: #block, @@ -1625,6 +1642,7 @@ fn generate_configured_feature_kind(kind: &ConfiguredFeatureKind) -> TokenStream }) } } + ConfiguredFeatureKind::NoOp => quote! { ConfiguredFeatureKind::NoOp }, ConfiguredFeatureKind::Ore(config) => { let targets = generate_vec(&config.targets, generate_ore_target); let size = config.size; @@ -1679,6 +1697,14 @@ fn generate_configured_feature_kind(kind: &ConfiguredFeatureKind) -> TokenStream }) } } + ConfiguredFeatureKind::ReplaceSingleBlock(config) => { + let targets = generate_vec(&config.targets, generate_ore_target); + quote! { + ConfiguredFeatureKind::ReplaceSingleBlock(ReplaceBlockConfiguration { + targets: #targets, + }) + } + } ConfiguredFeatureKind::RootSystem(config) => { let feature = generate_placed_feature_ref(&config.feature); let required_vertical_space_for_tree = config.required_vertical_space_for_tree; @@ -1952,6 +1978,12 @@ pub(crate) fn build_configured(overlay: &DatapackOverlay) -> TokenStream { let mut stream = TokenStream::new(); stream.extend(quote! { use crate::{feature::*, vanilla_blocks, vanilla_fluids}; + use crate::structure_processor::{ + PosRuleTestData, ProcessorRuleData, RuleBlockEntityModifierData, StructureProcessorAxis, + StructureProcessorHeightmap, StructureProcessorKind, StructureRuleTestData, + }; + use crate::template_pool::ProcessorList; + use simdnbt::owned::{NbtCompound, NbtList, NbtTag}; use steel_utils::value_providers::{ FloatProvider, HeightProvider, IntProvider, UniformIntProvider, VerticalAnchor, WeightedIntProvider, @@ -2019,12 +2051,16 @@ pub(crate) fn build_placed() -> TokenStream { }); let mut register = TokenStream::new(); - for (name, data) in &entries { - let ident = Ident::new(&name.to_shouty_snake_case(), Span::call_site()); + for (registry_id, data) in &entries { + let ident = registry_entry_ident(registry_id); + let identifier = Identifier::from_str(registry_id).unwrap_or_else(|err| { + panic!("invalid placed feature registry id {registry_id}: {err}") + }); + let key = generate_identifier(&identifier); stream.extend(quote! { pub static #ident: LazyLock = LazyLock::new(|| { PlacedFeature { - key: Identifier::vanilla_static(#name), + key: #key, data: #data, id: OnceLock::new(), } From 457a88c06f8e6068fde1e7d21510ea7059e59cfe Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:53:34 +0200 Subject: [PATCH 32/54] Update generator_functions.rs --- steel-registry/build/generator_functions.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/steel-registry/build/generator_functions.rs b/steel-registry/build/generator_functions.rs index 652bc52480d7..d2fe23dbd485 100644 --- a/steel-registry/build/generator_functions.rs +++ b/steel-registry/build/generator_functions.rs @@ -36,17 +36,7 @@ pub fn generate_identifier(resource: &Identifier) -> TokenStream { } pub fn parse_loose_identifier(raw: &str) -> Result { - let (namespace, path) = raw - .split_once(':') - .map_or((Identifier::VANILLA_NAMESPACE, raw), |(namespace, path)| { - (namespace, path) - }); - - if !Identifier::validate(namespace, path) { - return Err(format!("invalid identifier {raw}")); - } - - Ok(Identifier::new(namespace.to_owned(), path.to_owned())) + Identifier::parse_or_vanilla(raw).map_err(|error| format!("{error}: {raw}")) } pub fn generate_static_identifier(resource: &Identifier) -> TokenStream { From c994c2b38d297bc71adf9b1aeeffe6ab7a354707 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:53:41 +0200 Subject: [PATCH 33/54] Update item_tags.rs --- steel-registry/build/item_tags.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/steel-registry/build/item_tags.rs b/steel-registry/build/item_tags.rs index 3d5842d92e02..1e3213173472 100644 --- a/steel-registry/build/item_tags.rs +++ b/steel-registry/build/item_tags.rs @@ -19,7 +19,7 @@ fn read_all_fabric_tags(tag_file: &str) -> FxHashMap> { && let Ok(content) = fs::read_to_string(tag_file) { let tag: TagFile = serde_json::from_str(&content) - .unwrap_or_else(|e| panic!("Failed to parse {}: {}", tag_file, e)); + .unwrap_or_else(|e| panic!("Failed to parse {tag_file}: {e}")); return tag.item; } FxHashMap::default() From 880680eaa76a431966d5c8dbe1851bbcf3540778 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:54:01 +0200 Subject: [PATCH 34/54] Update items.rs --- steel-registry/build/items.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/steel-registry/build/items.rs b/steel-registry/build/items.rs index b47e9ddf4203..f80d1bca6417 100644 --- a/steel-registry/build/items.rs +++ b/steel-registry/build/items.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, fs, str::FromStr}; -use crate::generator_functions::generate_sound_event_ref; +use crate::generator_functions::{generate_sound_event_ref, parse_loose_identifier}; use heck::ToShoutySnakeCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -80,14 +80,10 @@ fn parse_block_or_tag(s: &str) -> TokenStream { (false, s) }; - // Split namespace:path - let parts: Vec<&str> = rest.splitn(2, ':').collect(); - let (namespace, path) = if parts.len() == 2 { - (parts[0], parts[1]) - } else { - // Default to minecraft namespace - ("minecraft", rest) - }; + let id = parse_loose_identifier(rest) + .unwrap_or_else(|error| panic!("invalid item tool rule block/tag reference {s}: {error}")); + let namespace = id.namespace.as_ref(); + let path = id.path.as_ref(); if is_tag { // Prefix namespace with # for tags @@ -98,18 +94,21 @@ fn parse_block_or_tag(s: &str) -> TokenStream { } } -fn split_identifier(s: &str) -> (&str, &str) { - s.split_once(':').unwrap_or(("minecraft", s)) +fn parse_identifier_or_vanilla(s: &str) -> Identifier { + parse_loose_identifier(s) + .unwrap_or_else(|error| panic!("invalid item build identifier {s}: {error}")) } fn identifier_token(s: &str) -> TokenStream { - let (namespace, path) = split_identifier(s); + let id = parse_identifier_or_vanilla(s); + let namespace = id.namespace.as_ref(); + let path = id.path.as_ref(); quote! { Identifier::new_static(#namespace, #path) } } fn entity_type_ref_token(s: &str) -> Option { - let (namespace, path) = split_identifier(s); - if namespace != "minecraft" { + let id = parse_identifier_or_vanilla(s); + if id.namespace != Identifier::VANILLA_NAMESPACE { return None; } @@ -190,11 +189,12 @@ fn optional_identifier_token(value: &Value, field: &str) -> TokenStream { } fn attribute_ref_token(s: &str) -> Option { - let (namespace, path) = split_identifier(s); - if namespace != "minecraft" { + let id = parse_identifier_or_vanilla(s); + if id.namespace != Identifier::VANILLA_NAMESPACE { return None; } + let path = id.path.as_ref(); let ident = Ident::new(&path.to_shouty_snake_case(), Span::call_site()); Some(quote! { vanilla_attributes::#ident }) } From b44a654467f96380dcd435bd3058b2c84b1e21d3 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:54:05 +0200 Subject: [PATCH 35/54] Update items.rs --- steel-registry/build/items.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/steel-registry/build/items.rs b/steel-registry/build/items.rs index f80d1bca6417..1ec103ec5ead 100644 --- a/steel-registry/build/items.rs +++ b/steel-registry/build/items.rs @@ -112,6 +112,7 @@ fn entity_type_ref_token(s: &str) -> Option { return None; } + let path = id.path.as_ref(); let ident = Ident::new(&path.to_shouty_snake_case(), Span::call_site()); Some(quote! { &vanilla_entities::#ident }) } From 7a8431a59572abb3f147bd771cf2600f6c8e3a09 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 01:02:11 +0200 Subject: [PATCH 36/54] Update loot_tables.rs --- steel-registry/build/loot_tables.rs | 824 +++++++++++++++++++++++----- 1 file changed, 691 insertions(+), 133 deletions(-) diff --git a/steel-registry/build/loot_tables.rs b/steel-registry/build/loot_tables.rs index a04e271ac4dd..f30eb89d9635 100644 --- a/steel-registry/build/loot_tables.rs +++ b/steel-registry/build/loot_tables.rs @@ -7,12 +7,20 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use rustc_hash::FxHashMap; use serde::Deserialize; +use steel_utils::Identifier; /// A number provider can be a constant number or an object with type. +#[derive(Deserialize, Debug, Clone)] +struct UniformRangeJson { + min: Box, + max: Box, +} + #[derive(Deserialize, Debug, Clone)] #[serde(untagged)] enum NumberProviderJson { Constant(f32), + UniformRange(UniformRangeJson), Object { #[serde(rename = "type")] provider_type: String, @@ -20,12 +28,20 @@ enum NumberProviderJson { value: Option, #[serde(default)] min: Option, + min: Option>, #[serde(default)] max: Option, + max: Option>, #[serde(default)] n: Option, // Can be float in JSON, convert to i32 later #[serde(default)] p: Option, + #[serde(default)] + target: Option, + #[serde(default)] + score: Option, + #[serde(default)] + scale: Option, }, } @@ -48,14 +64,31 @@ enum EnchantmentOptionsJson { #[serde(untagged)] enum LootTableValueJson { Reference(String), - Inline(Box), + Inline(Box), } #[derive(Deserialize, Debug, Clone)] -#[serde(deny_unknown_fields)] -struct InlineLootTableJson { - #[serde(default)] - pools: Vec, +#[serde(untagged)] +enum ScoreboardTargetJson { + Name(String), + Object { + #[serde(rename = "type")] + target_type: String, + #[serde(default)] + name: Option, + }, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +enum NumberProviderRangeJson { + Exact(f32), + Range { + #[serde(default)] + min: Option, + #[serde(default)] + max: Option, + }, } /// Enchanted chance can be a constant or linear formula. @@ -120,6 +153,8 @@ struct LootTableJson { functions: Vec, #[serde(default)] random_sequence: Option, + #[serde(default, rename = "__smithed__")] + _smithed: Option, } #[derive(Deserialize, Debug, Clone)] @@ -127,8 +162,8 @@ struct LootTableJson { struct LootPoolJson { #[serde(default)] rolls: NumberProviderJson, - #[serde(default)] - bonus_rolls: f32, + #[serde(default = "default_bonus_rolls")] + bonus_rolls: NumberProviderJson, #[serde(default)] entries: Vec, #[serde(default)] @@ -164,10 +199,17 @@ fn default_weight() -> i32 { 1 } +fn default_bonus_rolls() -> NumberProviderJson { + NumberProviderJson::Constant(0.0) +} + #[derive(Deserialize, Debug, Clone)] #[serde(deny_unknown_fields)] struct LootConditionJson { condition: String, + // reference + #[serde(default)] + name: Option, // block_state_property #[serde(default)] block: Option, @@ -189,7 +231,12 @@ struct LootConditionJson { terms: Option>, // random_chance #[serde(default)] - chance: Option, + chance: Option, + // value_check / time_check + #[serde(default)] + value: Option, + #[serde(default)] + range: Option, // random_chance_with_enchanted_bonus #[serde(default)] unenchanted_chance: Option, @@ -323,10 +370,11 @@ struct ToolPredicateJson { } #[derive(Deserialize, Debug, Clone)] -#[serde(deny_unknown_fields)] struct ToolPredicatesJson { #[serde(rename = "minecraft:enchantments", default)] enchantments: Option>, + #[serde(rename = "minecraft:custom_data", default)] + custom_data: Option, } #[derive(Deserialize, Debug, Clone)] @@ -369,9 +417,16 @@ struct LootFunctionJson { // enchant_randomly / enchant_with_levels / set_instrument #[serde(default)] options: Option, + #[serde(default = "default_true")] + only_compatible: bool, + #[serde(default)] + #[serde(rename = "include_additional_cost_component")] + _include_additional_cost_component: bool, // enchant_with_levels #[serde(default)] levels: Option, + #[serde(default, rename = "treasure")] + _treasure: Option, // copy_components #[serde(default)] source: Option, @@ -398,11 +453,48 @@ struct LootFunctionJson { zoom: Option, #[serde(default)] skip_existing_chunks: Option, + #[serde(default)] + search_radius: Option, + // set_fireworks + #[serde(default)] + #[serde(rename = "explosions")] + _explosions: Option, + #[serde(default)] + flight_duration: Option, + // set_firework_explosion + #[serde(default)] + shape: Option, + #[serde(default)] + colors: Vec, + #[serde(default)] + fade_colors: Vec, + #[serde(default)] + has_trail: bool, + #[serde(default)] + has_twinkle: bool, + // set_attributes + #[serde(default)] + modifiers: Vec, + #[serde(default)] + replace: bool, + // set_banner_pattern + #[serde(default)] + patterns: Vec, + #[serde(default)] + append: bool, // set_name (keep as raw value for text component) #[serde(default)] name: Option, #[serde(default)] target: Option, + #[serde(default)] + #[serde(rename = "entity")] + _entity: Option, + // set_lore + #[serde(default)] + lore: Vec, + #[serde(default)] + mode: Option, // set_ominous_bottle_amplifier #[serde(default)] amplifier: Option, @@ -418,6 +510,46 @@ struct LootFunctionJson { // conditions for conditional functions #[serde(default)] conditions: Option>, + // filtered + #[serde(default)] + item_filter: Option, + #[serde(default)] + modifier: Option>, + #[serde(default)] + on_pass: Option>, + #[serde(default)] + #[serde(rename = "on_fail")] + _on_fail: Option>, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +struct AttributeModifierJson { + attribute: String, + operation: String, + amount: NumberProviderJson, + id: String, + slot: String, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +struct BannerPatternJson { + pattern: String, + color: String, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +enum ListOperationJson { + Mode(String), + Object { + mode: String, + #[serde(default)] + offset: Option, + #[serde(default)] + size: Option, + }, } #[derive(Deserialize, Debug, Clone)] @@ -431,11 +563,85 @@ struct BonusParametersJson { probability: Option, } +fn number_provider_constant(value: &NumberProviderJson) -> Option { + match value { + NumberProviderJson::Constant(value) => Some(*value), + _ => None, + } +} + +fn generate_uniform_number_provider( + min: &NumberProviderJson, + max: &NumberProviderJson, +) -> TokenStream { + if let (Some(min), Some(max)) = (number_provider_constant(min), number_provider_constant(max)) { + return quote! { NumberProvider::Uniform { min: #min, max: #max } }; + } + + let min = generate_number_provider(min); + let max = generate_number_provider(max); + quote! { + NumberProvider::UniformProvider { + min: &#min, + max: &#max, + } + } +} + +fn generate_scoreboard_target(target: Option<&ScoreboardTargetJson>) -> TokenStream { + match target { + Some(ScoreboardTargetJson::Name(name)) => match name.as_str() { + "this" => quote! { ScoreboardTarget::This }, + "killer" => quote! { ScoreboardTarget::Killer }, + "direct_killer" => quote! { ScoreboardTarget::DirectKiller }, + "killer_player" => quote! { ScoreboardTarget::KillerPlayer }, + fixed => quote! { ScoreboardTarget::Fixed(#fixed) }, + }, + Some(ScoreboardTargetJson::Object { target_type, name }) => match target_type.as_str() { + "minecraft:this" | "this" => quote! { ScoreboardTarget::This }, + "minecraft:killer" | "killer" => quote! { ScoreboardTarget::Killer }, + "minecraft:direct_killer" | "direct_killer" => { + quote! { ScoreboardTarget::DirectKiller } + } + "minecraft:killer_player" | "killer_player" => { + quote! { ScoreboardTarget::KillerPlayer } + } + "minecraft:fixed" | "fixed" => { + let name = name + .as_deref() + .unwrap_or_else(|| panic!("fixed scoreboard target missing name")); + quote! { ScoreboardTarget::Fixed(#name) } + } + other => panic!("Unknown scoreboard target type: {other}"), + }, + None => quote! { ScoreboardTarget::This }, + } +} + +fn generate_number_provider_range(range: &NumberProviderRangeJson) -> TokenStream { + match range { + NumberProviderRangeJson::Exact(value) => quote! { NumberProviderRange::exact(#value) }, + NumberProviderRangeJson::Range { min, max } => { + let min = generate_option(min, generate_number_provider); + let max = generate_option(max, generate_number_provider); + quote! { + NumberProviderRange { + min: #min, + max: #max, + } + } + } + } +} + fn generate_number_provider(value: &NumberProviderJson) -> TokenStream { match value { NumberProviderJson::Constant(v) => { quote! { NumberProvider::Constant(#v) } } + NumberProviderJson::UniformRange(range) => { + generate_uniform_number_provider(&range.min, &range.max) + } NumberProviderJson::Object { provider_type, value, @@ -443,17 +649,36 @@ fn generate_number_provider(value: &NumberProviderJson) -> TokenStream { max, n, p, + target, + score, + scale, } => match provider_type.as_str() { "minecraft:uniform" => { - let min = min.unwrap_or(0.0); - let max = max.unwrap_or(1.0); - quote! { NumberProvider::Uniform { min: #min, max: #max } } + let default_min = NumberProviderJson::Constant(0.0); + let default_max = NumberProviderJson::Constant(1.0); + let min = min.as_deref().unwrap_or(&default_min); + let max = max.as_deref().unwrap_or(&default_max); + generate_uniform_number_provider(min, max) } "minecraft:binomial" => { let n = n.unwrap_or(1.0) as i32; let p = p.unwrap_or(0.5); quote! { NumberProvider::Binomial { n: #n, p: #p } } } + "minecraft:score" => { + let target = generate_scoreboard_target(target.as_ref()); + let score = score + .as_deref() + .unwrap_or_else(|| panic!("score number provider missing score")); + let scale = scale.unwrap_or(1.0); + quote! { + NumberProvider::Score { + target: #target, + score: #score, + scale: #scale, + } + } + } _ => { let v = value.unwrap_or(1.0); quote! { NumberProvider::Constant(#v) } @@ -474,8 +699,6 @@ fn generate_loot_context_entity(entity: &str) -> TokenStream { } } -/// Generate the EquipmentSlotGroup enum variant at build time. -#[expect(dead_code)] fn generate_equipment_slot_group(slot: &str) -> TokenStream { match slot { "any" => quote! { EquipmentSlotGroup::Any }, @@ -492,8 +715,43 @@ fn generate_equipment_slot_group(slot: &str) -> TokenStream { } } -/// Generate the DyeColor enum variant at build time. -#[expect(dead_code)] +fn generate_attribute_operation(operation: &str) -> TokenStream { + match operation { + "add_value" => quote! { AttributeOperation::AddValue }, + "add_multiplied_base" => quote! { AttributeOperation::AddMultipliedBase }, + "add_multiplied_total" => quote! { AttributeOperation::AddMultipliedTotal }, + other => panic!("Unknown attribute modifier operation: {other}"), + } +} + +fn generate_attribute_modifier(modifier: &AttributeModifierJson) -> TokenStream { + let attribute = generate_static_identifier_from_str(&modifier.attribute, "attribute modifier"); + let operation = generate_attribute_operation(&modifier.operation); + let amount = generate_number_provider(&modifier.amount); + let id = generate_static_identifier_from_str(&modifier.id, "attribute modifier"); + let slot = generate_equipment_slot_group(&modifier.slot); + quote! { + AttributeModifier { + attribute: #attribute, + operation: #operation, + amount: #amount, + id: #id, + slot: #slot, + } + } +} + +fn generate_banner_pattern(pattern: &BannerPatternJson) -> TokenStream { + let pattern_id = generate_static_identifier_from_str(&pattern.pattern, "banner pattern"); + let color = generate_dye_color(&pattern.color); + quote! { + BannerPattern { + pattern: #pattern_id, + color: #color, + } + } +} + fn generate_dye_color(color: &str) -> TokenStream { match color { "white" => quote! { DyeColor::White }, @@ -516,9 +774,26 @@ fn generate_dye_color(color: &str) -> TokenStream { } } +fn generate_firework_shape(shape: &str) -> TokenStream { + match shape { + "small_ball" => quote! { FireworkShape::SmallBall }, + "large_ball" => quote! { FireworkShape::LargeBall }, + "star" => quote! { FireworkShape::Star }, + "creeper" => quote! { FireworkShape::Creeper }, + "burst" => quote! { FireworkShape::Burst }, + other => panic!("Unknown firework explosion shape: {other}"), + } +} + /// Generate the LootType enum variant at build time. fn generate_loot_type(loot_type: &str) -> TokenStream { - match loot_type { + let loot_type = if loot_type.contains(':') { + loot_type.to_string() + } else { + format!("minecraft:{loot_type}") + }; + + match loot_type.as_str() { "minecraft:block" => quote! { LootType::Block }, "minecraft:entity" => quote! { LootType::Entity }, "minecraft:chest" => quote! { LootType::Chest }, @@ -536,36 +811,8 @@ fn generate_loot_type(loot_type: &str) -> TokenStream { } } -fn generate_tool_predicate(predicate: &Option) -> TokenStream { - let Some(pred) = predicate else { - return quote! { ToolPredicate::Any }; - }; - - // Only handle tool predicates; location/entity/damage_source predicates return Any - let pred = match pred { - PredicateJson::Tool(p) => p, - PredicateJson::Location(_) => return quote! { ToolPredicate::Any }, - PredicateJson::DamageSource(_) => return quote! { ToolPredicate::Any }, - PredicateJson::Entity(_) => return quote! { ToolPredicate::Any }, - }; - - // Check for items field (can be a string or tag reference) - if let Some(item_str) = &pred.items { - if item_str.starts_with('#') { - // Tag reference like "#minecraft:pickaxes" - let tag = item_str - .strip_prefix("#minecraft:") - .unwrap_or(item_str.strip_prefix('#').unwrap_or(item_str)); - return quote! { ToolPredicate::Tag(Identifier::vanilla_static(#tag)) }; - } else { - let item = item_str.strip_prefix("minecraft:").unwrap_or(item_str); - return quote! { ToolPredicate::Item(Identifier::vanilla_static(#item)) }; - } - } - - // Check for enchantment predicates - if let Some(predicates) = &pred.predicates - && let Some(enchants) = &predicates.enchantments +fn generate_tool_predicate_from_predicates(predicates: &ToolPredicatesJson) -> Option { + if let Some(enchants) = &predicates.enchantments && let Some(first) = enchants.first() && let Some(enchant_name) = &first.enchantments { @@ -575,18 +822,61 @@ fn generate_tool_predicate(predicate: &Option) -> TokenStream { .unwrap_or(enchant_name), ); let min_level = first.levels.as_ref().and_then(|l| l.min).unwrap_or(1); - - return quote! { + return Some(quote! { ToolPredicate::HasEnchantment { enchantment: Identifier::vanilla_static(#enchant_name), min_level: #min_level, } - }; + }); + } + + if let Some(custom_data) = &predicates.custom_data { + let tag = custom_data.to_string(); + return Some(quote! { + ToolPredicate::CustomData { tag: #tag } + }); + } + + None +} + +fn generate_tool_predicate_from_item_predicate(pred: &ToolPredicateJson) -> TokenStream { + if let Some(item_str) = &pred.items { + if item_str.starts_with('#') { + let tag = item_str + .strip_prefix("#minecraft:") + .unwrap_or(item_str.strip_prefix('#').unwrap_or(item_str)); + return quote! { ToolPredicate::Tag(Identifier::vanilla_static(#tag)) }; + } + let item = generate_static_identifier_from_str(item_str, "loot"); + return quote! { ToolPredicate::Item(#item) }; + } + + if let Some(predicates) = &pred.predicates + && let Some(generated) = generate_tool_predicate_from_predicates(predicates) + { + return generated; } quote! { ToolPredicate::Any } } +fn generate_tool_predicate(predicate: &Option) -> TokenStream { + let Some(pred) = predicate else { + return quote! { ToolPredicate::Any }; + }; + + // Only handle tool predicates; location/entity/damage_source predicates return Any + let pred = match pred { + PredicateJson::Tool(p) => p, + PredicateJson::Location(_) => return quote! { ToolPredicate::Any }, + PredicateJson::DamageSource(_) => return quote! { ToolPredicate::Any }, + PredicateJson::Entity(_) => return quote! { ToolPredicate::Any }, + }; + + generate_tool_predicate_from_item_predicate(pred) +} + fn generate_enchantment_options(options: &Option) -> TokenStream { match options { Some(EnchantmentOptionsJson::Tag(s)) => { @@ -657,29 +947,15 @@ fn generate_equipment_slot_predicate(slot: &Option) -> TokenS .strip_prefix("#minecraft:") .unwrap_or(items.strip_prefix('#').unwrap_or(items)); return quote! { Some(ToolPredicate::Tag(Identifier::vanilla_static(#tag))) }; - } else { - let item = items.strip_prefix("minecraft:").unwrap_or(items); - return quote! { Some(ToolPredicate::Item(Identifier::vanilla_static(#item))) }; } + let item = generate_static_identifier_from_str(items, "loot"); + return quote! { Some(ToolPredicate::Item(#item)) }; } if let Some(predicates) = &s.predicates - && let Some(enchants) = &predicates.enchantments - && let Some(first) = enchants.first() - && let Some(enchant_name) = &first.enchantments + && let Some(generated) = generate_tool_predicate_from_predicates(predicates) { - let enchant_name = enchant_name.strip_prefix("#minecraft:").unwrap_or( - enchant_name - .strip_prefix("minecraft:") - .unwrap_or(enchant_name), - ); - let min_level = first.levels.as_ref().and_then(|l| l.min).unwrap_or(1); - return quote! { - Some(ToolPredicate::HasEnchantment { - enchantment: Identifier::vanilla_static(#enchant_name), - min_level: #min_level, - }) - }; + return quote! { Some(#generated) }; } quote! { Some(ToolPredicate::Any) } @@ -832,7 +1108,13 @@ fn generate_location_predicate(predicate: &LocationPredicateJson) -> TokenStream } fn generate_condition(condition: &LootConditionJson) -> TokenStream { - match condition.condition.as_str() { + let condition_name = if condition.condition.contains(':') { + condition.condition.clone() + } else { + format!("minecraft:{}", condition.condition) + }; + + match condition_name.as_str() { "minecraft:survives_explosion" => { quote! { LootCondition::SurvivesExplosion } } @@ -924,7 +1206,13 @@ fn generate_condition(condition: &LootConditionJson) -> TokenStream { quote! { LootCondition::AllOf(&[#(#terms),*]) } } "minecraft:random_chance" => { - let chance = condition.chance.unwrap_or(0.5); + let chance = match &condition.chance { + Some(chance) => { + // Score-backed chances need scoreboard context support. Until then, fail closed. + number_provider_constant(chance).unwrap_or(0.0) + } + None => 0.5, + }; quote! { LootCondition::RandomChance(#chance) } } "minecraft:random_chance_with_enchanted_bonus" => { @@ -1064,6 +1352,32 @@ fn generate_condition(condition: &LootConditionJson) -> TokenStream { } } } + "minecraft:reference" => { + let name = condition + .name + .as_deref() + .unwrap_or_else(|| panic!("reference loot condition missing name")); + let name = generate_static_identifier_from_str(name, "loot condition"); + quote! { LootCondition::Reference(#name) } + } + "minecraft:value_check" => { + let value = condition + .value + .as_ref() + .map(generate_number_provider) + .unwrap_or_else(|| quote! { NumberProvider::Constant(0.0) }); + let range = condition + .range + .as_ref() + .map(generate_number_provider_range) + .unwrap_or_else(|| quote! { NumberProviderRange::exact(0.0) }); + quote! { + LootCondition::ValueCheck { + value: #value, + range: #range, + } + } + } other => { panic!("Unknown loot condition type: {}", other); } @@ -1071,7 +1385,31 @@ fn generate_condition(condition: &LootConditionJson) -> TokenStream { } fn generate_function(function: &LootFunctionJson) -> TokenStream { - let func_body = match function.function.as_str() { + let func_body = generate_function_body(function); + + // Wrap the function with conditions + let conditions: Vec = function + .conditions + .as_ref() + .map(|conds| conds.iter().map(generate_condition).collect()) + .unwrap_or_default(); + + quote! { + ConditionalLootFunction { + function: #func_body, + conditions: &[#(#conditions),*], + } + } +} + +fn generate_function_body(function: &LootFunctionJson) -> TokenStream { + let function_name = if function.function.contains(':') { + function.function.clone() + } else { + format!("minecraft:{}", function.function) + }; + + match function_name.as_str() { "minecraft:set_count" => { let count = function .count @@ -1190,7 +1528,13 @@ fn generate_function(function: &LootFunctionJson) -> TokenStream { } "minecraft:enchant_randomly" => { let options = generate_enchantment_options(&function.options); - quote! { LootFunction::EnchantRandomly { options: #options } } + let only_compatible = function.only_compatible; + quote! { + LootFunction::EnchantRandomly { + options: #options, + only_compatible: #only_compatible, + } + } } "minecraft:enchant_with_levels" => { let levels = function @@ -1260,31 +1604,99 @@ fn generate_function(function: &LootFunctionJson) -> TokenStream { .unwrap_or_else(|| "{}".to_string()); quote! { LootFunction::SetComponents { components: #components_str } } } + "minecraft:set_attributes" => { + let modifiers: Vec = function + .modifiers + .iter() + .map(generate_attribute_modifier) + .collect(); + let replace = function.replace; + quote! { + LootFunction::SetAttributes { + modifiers: &[#(#modifiers),*], + replace: #replace, + } + } + } + "minecraft:set_banner_pattern" => { + let patterns: Vec = function + .patterns + .iter() + .map(generate_banner_pattern) + .collect(); + let append = function.append; + quote! { + LootFunction::SetBannerPattern { + patterns: &[#(#patterns),*], + append: #append, + } + } + } "minecraft:furnace_smelt" => { let use_input_count = function.use_input_count.unwrap_or(true); quote! { LootFunction::FurnaceSmelt { use_input_count: #use_input_count } } } "minecraft:exploration_map" => { - let destination = function - .destination - .as_deref() - .unwrap_or("minecraft:buried_treasure"); - let destination = destination - .strip_prefix("minecraft:") - .unwrap_or(destination); - - let decoration = function.decoration.as_deref().unwrap_or("minecraft:red_x"); - let decoration = decoration.strip_prefix("minecraft:").unwrap_or(decoration); + let destination = generate_static_identifier_from_str( + function + .destination + .as_deref() + .unwrap_or("minecraft:on_treasure_maps"), + "loot", + ); + let decoration = generate_static_identifier_from_str( + function + .decoration + .as_deref() + .unwrap_or("minecraft:mansion"), + "loot", + ); let zoom = function.zoom.unwrap_or(2); let skip_existing_chunks = function.skip_existing_chunks.unwrap_or(true); + let search_radius = function.search_radius.unwrap_or(50); quote! { LootFunction::ExplorationMap { - destination: Identifier::vanilla_static(#destination), - decoration: Identifier::vanilla_static(#decoration), + destination: #destination, + decoration: #decoration, zoom: #zoom, skip_existing_chunks: #skip_existing_chunks, + search_radius: #search_radius, + } + } + } + "minecraft:set_fireworks" => { + let flight_duration = match function.flight_duration { + Some(duration) => quote! { Some(#duration) }, + None => quote! { None }, + }; + quote! { + LootFunction::SetFireworks { + explosions: None, + flight_duration: #flight_duration, + } + } + } + "minecraft:set_firework_explosion" => { + let shape = function + .shape + .as_deref() + .map(generate_firework_shape) + .unwrap_or_else(|| quote! { FireworkShape::SmallBall }); + let colors = &function.colors; + let fade_colors = &function.fade_colors; + let has_trail = function.has_trail; + let has_twinkle = function.has_twinkle; + quote! { + LootFunction::SetFireworkExplosion { + explosion: FireworkExplosion { + shape: #shape, + colors: &[#(#colors),*], + fade_colors: &[#(#fade_colors),*], + has_trail: #has_trail, + has_twinkle: #has_twinkle, + }, } } } @@ -1308,6 +1720,17 @@ fn generate_function(function: &LootFunctionJson) -> TokenStream { } } } + "minecraft:set_lore" => { + let lore: Vec = function.lore.iter().map(|line| line.to_string()).collect(); + let mode = generate_list_operation(function.mode.as_ref()); + + quote! { + LootFunction::SetLore { + lore: &[#(#lore),*], + mode: #mode, + } + } + } "minecraft:set_ominous_bottle_amplifier" => { let amplifier = function .amplifier @@ -1381,39 +1804,92 @@ fn generate_function(function: &LootFunctionJson) -> TokenStream { } } } + "minecraft:reference" => { + let Some(name) = function.name.as_ref().and_then(|value| value.as_str()) else { + panic!("reference loot function missing name"); + }; + let name = generate_static_identifier_from_str(name, "loot"); + quote! { LootFunction::Reference(#name) } + } + "minecraft:filtered" => { + let item_filter = function + .item_filter + .as_ref() + .map(generate_tool_predicate_from_item_predicate) + .unwrap_or_else(|| quote! { ToolPredicate::Any }); + + let modifier_source = function.modifier.as_ref().or(function.on_pass.as_ref()); + let modifier_func = modifier_source + .map(|modifier| generate_function_body(modifier)) + .unwrap_or_else(|| { + quote! { LootFunction::SetCount { count: NumberProvider::Constant(1.0), add: false } } + }); + + quote! { + LootFunction::Filtered { + item_filter: #item_filter, + modifier: &ConditionalLootFunction { + function: #modifier_func, + conditions: &[], + }, + } + } + } other => { panic!("Unknown loot function type: {}", other); } + } +} + +fn generate_list_operation(mode: Option<&ListOperationJson>) -> TokenStream { + let Some(mode) = mode else { + return quote! { ListOperation::Append }; }; - // Wrap the function with conditions - let conditions: Vec = function - .conditions - .as_ref() - .map(|conds| conds.iter().map(generate_condition).collect()) - .unwrap_or_default(); + let (mode, offset, size) = match mode { + ListOperationJson::Mode(mode) => (mode.as_str(), None, None), + ListOperationJson::Object { mode, offset, size } => (mode.as_str(), *offset, *size), + }; - quote! { - ConditionalLootFunction { - function: #func_body, - conditions: &[#(#conditions),*], + match mode { + "append" => quote! { ListOperation::Append }, + "insert" => { + let offset = offset.unwrap_or(0); + quote! { ListOperation::InsertBefore { offset: #offset } } + } + "replace_all" => quote! { ListOperation::ReplaceAll }, + "replace_section" => { + let offset = offset.unwrap_or(0); + let size = match size { + Some(size) => quote! { Some(#size) }, + None => quote! { None }, + }; + quote! { ListOperation::ReplaceSection { offset: #offset, size: #size } } } + other => panic!("Unknown list operation mode: {}", other), } } fn generate_entry(entry: &LootEntryJson) -> TokenStream { let conditions: Vec = entry.conditions.iter().map(generate_condition).collect(); let functions: Vec = entry.functions.iter().map(generate_function).collect(); + let entry_type = if entry.entry_type.contains(':') { + entry.entry_type.clone() + } else { + format!("minecraft:{}", entry.entry_type) + }; - match entry.entry_type.as_str() { + match entry_type.as_str() { "minecraft:item" => { - let name = entry.name.as_deref().unwrap_or("minecraft:air"); - let name = name.strip_prefix("minecraft:").unwrap_or(name); + let name = generate_static_identifier_from_str( + entry.name.as_deref().unwrap_or("minecraft:air"), + "loot", + ); let weight = entry.weight; let quality = entry.quality; quote! { LootEntry::Item { - name: Identifier::vanilla_static(#name), + name: #name, weight: #weight, quality: #quality, conditions: &[#(#conditions),*], @@ -1427,10 +1903,10 @@ fn generate_entry(entry: &LootEntryJson) -> TokenStream { // Check if it's a string reference or inline loot table if let Some(name) = entry.name.as_deref() { - let name = name.strip_prefix("minecraft:").unwrap_or(name); + let name = generate_static_identifier_from_str(name, "loot"); quote! { LootEntry::LootTableRef { - name: Identifier::vanilla_static(#name), + name: #name, weight: #weight, quality: #quality, conditions: &[#(#conditions),*], @@ -1440,10 +1916,10 @@ fn generate_entry(entry: &LootEntryJson) -> TokenStream { } else if let Some(value) = &entry.value { match value { LootTableValueJson::Reference(s) => { - let name = s.strip_prefix("minecraft:").unwrap_or(s); + let name = generate_static_identifier_from_str(s, "loot"); quote! { LootEntry::LootTableRef { - name: Identifier::vanilla_static(#name), + name: #name, weight: #weight, quality: #quality, conditions: &[#(#conditions),*], @@ -1454,10 +1930,13 @@ fn generate_entry(entry: &LootEntryJson) -> TokenStream { LootTableValueJson::Inline(inline) => { let inline_pools: Vec = inline.pools.iter().map(generate_pool).collect(); + let inline_functions: Vec = + inline.functions.iter().map(generate_function).collect(); quote! { LootEntry::InlineLootTable { pools: &[#(#inline_pools),*], + table_functions: &[#(#inline_functions),*], weight: #weight, quality: #quality, conditions: &[#(#conditions),*], @@ -1479,14 +1958,16 @@ fn generate_entry(entry: &LootEntryJson) -> TokenStream { } } "minecraft:tag" => { - let name = entry.name.as_deref().unwrap_or("minecraft:empty"); - let name = name.strip_prefix("minecraft:").unwrap_or(name); + let name = generate_static_identifier_from_str( + entry.name.as_deref().unwrap_or("minecraft:empty"), + "loot", + ); let expand = entry.expand; let weight = entry.weight; let quality = entry.quality; quote! { LootEntry::Tag { - name: Identifier::vanilla_static(#name), + name: #name, expand: #expand, weight: #weight, quality: #quality, @@ -1549,7 +2030,7 @@ fn generate_entry(entry: &LootEntryJson) -> TokenStream { fn generate_pool(pool: &LootPoolJson) -> TokenStream { let rolls = generate_number_provider(&pool.rolls); - let bonus_rolls = pool.bonus_rolls; + let bonus_rolls = generate_number_provider(&pool.bonus_rolls); let entries: Vec = pool.entries.iter().map(generate_entry).collect(); let conditions: Vec = pool.conditions.iter().map(generate_condition).collect(); let functions: Vec = pool.functions.iter().map(generate_function).collect(); @@ -1566,9 +2047,13 @@ fn generate_pool(pool: &LootPoolJson) -> TokenStream { } struct LootTableData { - /// Full key path like "blocks/acacia_button" - key: String, - /// Rust identifier like "BLOCKS_ACACIA_BUTTON" + /// Full registry id like `minecraft:blocks/acacia_button`. + registry_id: String, + /// Category bucket for generated convenience structs. + category_key: String, + /// Field name within the category struct. + field_name: String, + /// Rust identifier like `MINECRAFT_BLOCKS_ACACIA_BUTTON` const_ident: Ident, /// The loot type as a TokenStream loot_type: TokenStream, @@ -1576,7 +2061,7 @@ struct LootTableData { pools: Vec, /// Table-level functions functions: Vec, - /// Random sequence identifier + /// Random sequence identifier path (without namespace) random_sequence: Option, } @@ -1584,6 +2069,10 @@ pub(crate) fn build() -> TokenStream { let loot_table_dir = "../steel-utils/build_assets/builtin_datapacks/minecraft/loot_table"; println!("cargo:rerun-if-changed={loot_table_dir}"); let mut tables: Vec = Vec::new(); +fn parsed_loot_table_id(registry_id: &str) -> Identifier { + Identifier::parse_or_vanilla(registry_id) + .unwrap_or_else(|error| panic!("invalid loot table identifier {registry_id}: {error}")) +} // Recursively read all loot table JSON files fn read_loot_tables(dir: &Path, base_dir: &Path, tables: &mut Vec) { @@ -1591,6 +2080,16 @@ pub(crate) fn build() -> TokenStream { Ok(e) => e, Err(_) => return, }; +fn generate_loot_table_key(registry_id: &str) -> TokenStream { + let id = parsed_loot_table_id(registry_id); + let namespace = id.namespace.as_ref(); + let path = id.path.as_ref(); + if namespace == Identifier::VANILLA_NAMESPACE { + quote! { Identifier::vanilla_static(#path) } + } else { + quote! { Identifier::new_static(#namespace, #path) } + } +} for entry in entries.flatten() { let path = entry.path(); @@ -1644,25 +2143,95 @@ pub(crate) fn build() -> TokenStream { }); } } +fn loot_table_category_key(registry_id: &str) -> String { + let id = parsed_loot_table_id(registry_id); + let namespace = id.namespace.as_ref(); + let path = id.path.as_ref(); + let top = path.split('/').next().unwrap_or("other"); + if namespace == Identifier::VANILLA_NAMESPACE { + top.to_string() + } else { + format!("{namespace}_{top}") } +} read_loot_tables( Path::new(loot_table_dir), Path::new(loot_table_dir), &mut tables, ); +fn loot_table_field_name(registry_id: &str) -> String { + let id = parsed_loot_table_id(registry_id); + let namespace = id.namespace.as_ref(); + let path = id.path.as_ref(); + let suffix = path + .split('/') + .skip(1) + .collect::>() + .join("_") + .to_snake_case(); + let base = if suffix.is_empty() { + path.to_snake_case() + } else { + suffix + }; + if namespace == Identifier::VANILLA_NAMESPACE { + base + } else { + format!("{}_{}", namespace.to_snake_case(), base) + } +} + +fn loot_table_const_ident(registry_id: &str) -> Ident { + let id = parsed_loot_table_id(registry_id); + let name = if id.namespace == Identifier::VANILLA_NAMESPACE { + id.path.into_owned() + } else { + registry_id.replace([':', '/'], "_") + }; + Ident::new(&name.to_shouty_snake_case(), Span::call_site()) +} + +fn parse_loot_table(registry_id: &str, content: &str) -> LootTableData { + let loot_table: LootTableJson = serde_json::from_str(content) + .unwrap_or_else(|err| panic!("Failed to parse loot table {registry_id}: {err}")); + + let const_ident = loot_table_const_ident(registry_id); + let pools: Vec = loot_table.pools.iter().map(generate_pool).collect(); + let functions: Vec = loot_table.functions.iter().map(generate_function).collect(); + let random_sequence = loot_table.random_sequence.as_ref().map(|sequence| { + sequence + .strip_prefix("minecraft:") + .unwrap_or(sequence.as_str()) + .to_string() + }); + + LootTableData { + registry_id: registry_id.to_string(), + category_key: loot_table_category_key(registry_id), + field_name: loot_table_field_name(registry_id), + const_ident, + loot_type: generate_loot_type(loot_table.loot_type.as_deref().unwrap_or("minecraft:empty")), + pools, + functions, + random_sequence, + } +} + let mut stream = TokenStream::new(); // Imports stream.extend(quote! { use crate::loot_table::{ - BlockPredicate, BonusFormula, ConditionalLootFunction, CopySource, DamageSourcePredicate, - DamageTagPredicate, DyeColor, EnchantedChance, EnchantmentOptions, EntityEquipment, - EntityFlags, EntityPredicate, EquipmentSlotGroup, LocationPredicate, LootCondition, - LootContextEntity, LootEntry, LootFunction, LootPool, LootTable, LootTableRef, - LootTableRegistry, LootType, NameTarget, NumberProvider, PropertyCheck, StewEffect, - ToolPredicate, + AttributeModifier, AttributeOperation, BannerPattern, BlockPredicate, BonusFormula, + ConditionalLootFunction, CopySource, DamageSourcePredicate, DamageTagPredicate, + DyeColor, EnchantedChance, EnchantmentOptions, EntityEquipment, EntityFlags, + EntityPredicate, EquipmentSlotGroup, FireworkExplosion, FireworkShape, + LocationPredicate, LootCondition, LootContextEntity, LootEntry, LootFunction, + LootPool, LootTable, LootTableRef, LootTableRegistry, LootType, ListOperation, + NameTarget, NumberProvider, NumberProviderRange, PropertyCheck, ScoreboardTarget, + StewEffect, ToolPredicate, }; use steel_utils::Identifier; }); @@ -1670,7 +2239,7 @@ pub(crate) fn build() -> TokenStream { // Generate static constants for each loot table for table in &tables { let const_ident = &table.const_ident; - let key = &table.key; + let key = generate_loot_table_key(&table.registry_id); let loot_type = &table.loot_type; let pools = &table.pools; let functions = &table.functions; @@ -1682,7 +2251,7 @@ pub(crate) fn build() -> TokenStream { stream.extend(quote! { pub static #const_ident: LootTable = LootTable { - key: Identifier::vanilla_static(#key), + key: #key, loot_type: #loot_type, pools: &[#(#pools),*], functions: &[#(#functions),*], @@ -1712,22 +2281,11 @@ pub(crate) fn build() -> TokenStream { std::collections::BTreeMap::new(); for table in &tables { - let category = table.key.split('/').next().unwrap_or("other").to_string(); - let field_name = table - .key - .split('/') - .skip(1) - .collect::>() - .join("_") - .to_snake_case(); - let field_name = if field_name.is_empty() { - table.key.to_snake_case() - } else { - field_name - }; - let field_ident = Ident::new(&field_name, Span::call_site()); + let category = &table.category_key; + let field_name = &table.field_name; + let field_ident = Ident::new(field_name, Span::call_site()); categories - .entry(category) + .entry(category.clone()) .or_default() .push((table, field_ident)); } From 6637fb94ab7afd1c7b0af344514c6feb4c254025 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 01:03:14 +0200 Subject: [PATCH 37/54] Update shared_structs.rs --- steel-registry/build/shared_structs.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/steel-registry/build/shared_structs.rs b/steel-registry/build/shared_structs.rs index 8b841c6e1603..f2f4bddabf5d 100644 --- a/steel-registry/build/shared_structs.rs +++ b/steel-registry/build/shared_structs.rs @@ -11,6 +11,8 @@ pub struct BlockStateData { pub name: Identifier, #[serde(rename = "Properties", default)] pub properties: BTreeMap, + #[serde(default, rename = "Properites")] + pub _properites: Option, } #[derive(Deserialize, Debug, Clone)] From dd5377e66057ab898cda7e0d8c273e5f681c9faa Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 03:49:20 +0200 Subject: [PATCH 38/54] Update structure_processors.rs --- steel-registry/build/structure_processors.rs | 182 +++++++++++++++---- 1 file changed, 144 insertions(+), 38 deletions(-) diff --git a/steel-registry/build/structure_processors.rs b/steel-registry/build/structure_processors.rs index 68d277316dc3..a95ea12027d2 100644 --- a/steel-registry/build/structure_processors.rs +++ b/steel-registry/build/structure_processors.rs @@ -4,8 +4,14 @@ use std::fs; use heck::ToShoutySnakeCase; use proc_macro2::{Ident, Span, TokenStream}; +use crate::generator_functions::{ + generate_static_identifier as generate_identifier, generate_static_identifier_from_str, + registry_entry_ident, +}; use quote::quote; use steel_utils::{Identifier, value_providers::IntProvider}; +use simdnbt::owned::{NbtCompound, NbtList, NbtTag}; +use steel_utils::value_providers::IntProvider; #[allow(dead_code)] #[path = "../src/structure_processor/data.rs"] @@ -14,41 +20,12 @@ mod structure_processor_data; use crate::shared_structs::BlockStateData; use structure_processor_data::{ PosRuleTestData, ProcessorRuleData, RuleBlockEntityModifierData, StructureProcessorAxis, - StructureProcessorKind, StructureProcessorListData, StructureRuleTestData, + StructureProcessorHeightmap, StructureProcessorKind, StructureProcessorListData, + StructureRuleTestData, }; -fn sorted_json_files(dir: &str) -> Vec { - let mut files: Vec<_> = fs::read_dir(dir) - .unwrap_or_else(|err| panic!("{dir} missing: {err}")) - .filter_map(Result::ok) - .filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("json")) - .collect(); - files.sort_by_key(|entry| entry.file_name()); - files -} - -fn resource_name(entry: &fs::DirEntry) -> String { - entry - .path() - .file_stem() - .and_then(|stem| stem.to_str()) - .unwrap_or_else(|| { - panic!( - "invalid structure processor-list file name: {:?}", - entry.path() - ) - }) - .to_owned() -} - -fn generate_identifier(identifier: &Identifier) -> TokenStream { - let namespace = identifier.namespace.as_ref(); - let path = identifier.path.as_ref(); - if namespace == Identifier::VANILLA_NAMESPACE { - quote! { Identifier::vanilla_static(#path) } - } else { - quote! { Identifier::new_static(#namespace, #path) } - } +fn generate_registry_key(registry_id: &str) -> TokenStream { + generate_static_identifier_from_str(registry_id, "structure processor-list registry") } fn generate_option(value: &Option, f: impl Fn(&T) -> TokenStream) -> TokenStream { @@ -83,7 +60,7 @@ fn generate_block_state_data(data: &BlockStateData) -> TokenStream { }; quote! { - BlockStateData { + crate::shared_structs::BlockStateData { name: #name, properties: #properties, } @@ -223,6 +200,18 @@ fn generate_rule_test(data: &StructureRuleTestData) -> TokenStream { let block_state = generate_block_state_data(block_state); quote! { StructureRuleTestData::BlockStateMatch { block_state: #block_state } } } + StructureRuleTestData::RandomBlockStateMatch { + block_state, + probability, + } => { + let block_state = generate_block_state_data(block_state); + quote! { + StructureRuleTestData::RandomBlockStateMatch { + block_state: #block_state, + probability: #probability, + } + } + } } } @@ -235,6 +224,83 @@ fn generate_block_entity_modifier(data: &RuleBlockEntityModifierData) -> TokenSt let loot_table = generate_identifier(loot_table); quote! { RuleBlockEntityModifierData::AppendLoot { loot_table: #loot_table } } } + RuleBlockEntityModifierData::AppendStatic { data } => { + let data = generate_nbt_compound(data); + quote! { RuleBlockEntityModifierData::AppendStatic { data: #data } } + } + } +} + +fn generate_nbt_compound(compound: &NbtCompound) -> TokenStream { + let entries = compound.iter().map(|(key, value)| { + let key = key.to_string(); + let value = generate_nbt_tag(value); + quote! { (#key.into(), #value) } + }); + quote! { NbtCompound::from_values(vec![#(#entries),*]) } +} + +fn generate_nbt_list(list: &NbtList) -> TokenStream { + match list { + NbtList::Empty => quote! { NbtList::Empty }, + NbtList::Byte(values) => quote! { NbtList::Byte(vec![#(#values),*]) }, + NbtList::Short(values) => quote! { NbtList::Short(vec![#(#values),*]) }, + NbtList::Int(values) => quote! { NbtList::Int(vec![#(#values),*]) }, + NbtList::Long(values) => quote! { NbtList::Long(vec![#(#values),*]) }, + NbtList::Float(values) => quote! { NbtList::Float(vec![#(#values),*]) }, + NbtList::Double(values) => quote! { NbtList::Double(vec![#(#values),*]) }, + NbtList::ByteArray(values) => { + let values = values.iter().map(|value| quote! { vec![#(#value),*] }); + quote! { NbtList::ByteArray(vec![#(#values),*]) } + } + NbtList::String(values) => { + let values = values + .iter() + .map(|value| value.as_str().to_str().into_owned()); + quote! { NbtList::String(vec![#(#values.into()),*]) } + } + NbtList::List(values) => { + let values = values.iter().map(generate_nbt_list); + quote! { NbtList::List(vec![#(#values),*]) } + } + NbtList::Compound(values) => { + let values = values.iter().map(generate_nbt_compound); + quote! { NbtList::Compound(vec![#(#values),*]) } + } + NbtList::IntArray(values) => { + let values = values.iter().map(|value| quote! { vec![#(#value),*] }); + quote! { NbtList::IntArray(vec![#(#values),*]) } + } + NbtList::LongArray(values) => { + let values = values.iter().map(|value| quote! { vec![#(#value),*] }); + quote! { NbtList::LongArray(vec![#(#values),*]) } + } + } +} + +fn generate_nbt_tag(tag: &NbtTag) -> TokenStream { + match tag { + NbtTag::Byte(value) => quote! { NbtTag::Byte(#value) }, + NbtTag::Short(value) => quote! { NbtTag::Short(#value) }, + NbtTag::Int(value) => quote! { NbtTag::Int(#value) }, + NbtTag::Long(value) => quote! { NbtTag::Long(#value) }, + NbtTag::Float(value) => quote! { NbtTag::Float(#value) }, + NbtTag::Double(value) => quote! { NbtTag::Double(#value) }, + NbtTag::ByteArray(value) => quote! { NbtTag::ByteArray(vec![#(#value),*]) }, + NbtTag::String(value) => { + let value = value.as_str().to_str().into_owned(); + quote! { NbtTag::String(#value.into()) } + } + NbtTag::List(value) => { + let value = generate_nbt_list(value); + quote! { NbtTag::List(#value) } + } + NbtTag::Compound(value) => { + let value = generate_nbt_compound(value); + quote! { NbtTag::Compound(#value) } + } + NbtTag::IntArray(value) => quote! { NbtTag::IntArray(vec![#(#value),*]) }, + NbtTag::LongArray(value) => quote! { NbtTag::LongArray(vec![#(#value),*]) }, } } @@ -256,7 +322,30 @@ fn generate_processor_rule(data: &ProcessorRuleData) -> TokenStream { } } -fn generate_processor_kind(data: &StructureProcessorKind) -> TokenStream { +fn generate_processor_heightmap(heightmap: StructureProcessorHeightmap) -> TokenStream { + match heightmap { + StructureProcessorHeightmap::WorldSurface => { + quote! { StructureProcessorHeightmap::WorldSurface } + } + StructureProcessorHeightmap::MotionBlocking => { + quote! { StructureProcessorHeightmap::MotionBlocking } + } + StructureProcessorHeightmap::MotionBlockingNoLeaves => { + quote! { StructureProcessorHeightmap::MotionBlockingNoLeaves } + } + StructureProcessorHeightmap::OceanFloor => { + quote! { StructureProcessorHeightmap::OceanFloor } + } + StructureProcessorHeightmap::WorldSurfaceWg => { + quote! { StructureProcessorHeightmap::WorldSurfaceWg } + } + StructureProcessorHeightmap::OceanFloorWg => { + quote! { StructureProcessorHeightmap::OceanFloorWg } + } + } +} + +pub(crate) fn generate_processor_kind(data: &StructureProcessorKind) -> TokenStream { match data { StructureProcessorKind::BlockRot { rottable_blocks, @@ -284,9 +373,25 @@ fn generate_processor_kind(data: &StructureProcessorKind) -> TokenStream { StructureProcessorKind::LavaSubmergedBlock => { quote! { StructureProcessorKind::LavaSubmergedBlock } } + StructureProcessorKind::JigsawReplacement => { + quote! { StructureProcessorKind::JigsawReplacement } + } StructureProcessorKind::BlackstoneReplace => { quote! { StructureProcessorKind::BlackstoneReplace } } + StructureProcessorKind::BlockIgnore { blocks } => { + let blocks = generate_vec(blocks, generate_block_state_data); + quote! { StructureProcessorKind::BlockIgnore { blocks: #blocks } } + } + StructureProcessorKind::Gravity { heightmap, offset } => { + let heightmap = generate_processor_heightmap(*heightmap); + quote! { + StructureProcessorKind::Gravity { + heightmap: #heightmap, + offset: #offset, + } + } + } StructureProcessorKind::Capped { delegate, limit } => { let delegate = generate_box(delegate.as_ref(), generate_processor_kind); let limit = generate_int_provider(limit); @@ -329,13 +434,14 @@ pub(crate) fn build() -> TokenStream { }); let mut register = TokenStream::new(); - for (name, data) in &entries { - let ident = Ident::new(&name.to_shouty_snake_case(), Span::call_site()); + for (registry_id, data) in &entries { + let ident = registry_entry_ident(registry_id); + let key = generate_registry_key(registry_id); let data = generate_processor_list_data(data); stream.extend(quote! { pub static #ident: LazyLock = LazyLock::new(|| { StructureProcessorList { - key: Identifier::vanilla_static(#name), + key: #key, data: #data, id: OnceLock::new(), } From cd227bf19d12dea441f5f0c05db54a9d9a4349ae Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 03:53:53 +0200 Subject: [PATCH 39/54] Update structure_sets.rs --- steel-registry/build/structure_sets.rs | 232 ++++++++++++++++--------- 1 file changed, 149 insertions(+), 83 deletions(-) diff --git a/steel-registry/build/structure_sets.rs b/steel-registry/build/structure_sets.rs index 40e2303cc276..4d025411a9b8 100644 --- a/steel-registry/build/structure_sets.rs +++ b/steel-registry/build/structure_sets.rs @@ -2,6 +2,7 @@ use std::fs; use rustc_hash::FxHashMap as HashMap; +use crate::generator_functions::generate_owned_identifier_from_str; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use serde::Deserialize; @@ -22,7 +23,7 @@ struct StructureEntryJson { struct PlacementJson { #[serde(rename = "type")] placement_type: String, - salt: i32, + salt: i64, #[serde(default = "default_frequency")] frequency: f32, #[serde(default)] @@ -62,7 +63,7 @@ struct ExclusionZoneJson { /// Structure JSON — we need biomes, type, and height config. #[derive(Deserialize, Debug)] struct StructureJson { - biomes: String, + biomes: BiomeSelectorJson, #[serde(rename = "type")] structure_type: String, #[serde(default)] @@ -107,6 +108,36 @@ struct StructureJson { height: Option, } +#[derive(Deserialize, Debug)] +#[serde(untagged)] +enum BiomeSelectorJson { + Tag(String), + List(Vec), +} + +fn resolve_structure_biomes( + biomes: BiomeSelectorJson, + biome_tags: &HashMap>, + full_name: &str, +) -> Vec { + match biomes { + BiomeSelectorJson::List(list) => list, + BiomeSelectorJson::Tag(tag) => { + if let Some(tag_name) = tag.strip_prefix('#') { + let tag_name = normalize_tag_key(tag_name); + biome_tags + .get(&tag_name) + .unwrap_or_else(|| { + panic!("Missing biome tag {tag_name} referenced by structure {full_name}") + }) + .clone() + } else { + vec![tag] + } + } + } +} + #[derive(Deserialize, Debug)] struct SpawnOverrideJson { bounding_box: String, @@ -139,7 +170,14 @@ struct RuinedPortalSetupJson { /// Biome tag JSON. #[derive(Deserialize, Debug)] struct TagJson { - values: Vec, + values: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +enum TagValueJson { + Plain(String), + Optional { id: String, required: Option }, } /// Loads all biome tags from the worldgen/biome tags directory, @@ -284,8 +322,11 @@ struct JigsawConfigData { } enum StartHeightData { - Constant(i32), - Uniform { min: i32, max: i32 }, + Constant(VerticalAnchorData), + Uniform { + min: VerticalAnchorData, + max: VerticalAnchorData, + }, } enum VerticalAnchorData { @@ -354,25 +395,23 @@ fn non_negative_i32(value: i32, context: &str) -> i32 { value } -fn parse_absolute_start_anchor(value: &serde_json::Value, context: &str) -> i32 { - if let Some(n) = value.as_i64() { - return i64_to_i32(n, context); - } - if let Some(n) = value.get("absolute").and_then(|v| v.as_i64()) { - return i64_to_i32(n, context); - } - panic!("Unsupported jigsaw start_height anchor in {context}: {value}"); -} - fn parse_start_height_full(value: &serde_json::Value, context: &str) -> StartHeightData { // {"absolute": N} if let Some(n) = value.get("absolute").and_then(|v| v.as_i64()) { - return StartHeightData::Constant(i64_to_i32(n, context)); + return StartHeightData::Constant(VerticalAnchorData::Absolute(i64_to_i32(n, context))); } - match value.get("type").and_then(|v| v.as_str()) { + let provider_type = value.get("type").and_then(|v| v.as_str()).map(|raw| { + crate::generator_functions::parse_loose_identifier(raw) + .unwrap_or_else(|error| { + panic!("invalid jigsaw start_height provider {raw} in {context}: {error}") + }) + .to_string() + }); + + match provider_type.as_deref() { Some("minecraft:uniform") => { - let min = parse_absolute_start_anchor( + let min = parse_vertical_anchor( required_value( value.get("min_inclusive"), context, @@ -380,7 +419,7 @@ fn parse_start_height_full(value: &serde_json::Value, context: &str) -> StartHei ), context, ); - let max = parse_absolute_start_anchor( + let max = parse_vertical_anchor( required_value( value.get("max_inclusive"), context, @@ -390,17 +429,15 @@ fn parse_start_height_full(value: &serde_json::Value, context: &str) -> StartHei ); StartHeightData::Uniform { min, max } } - Some("minecraft:constant") => StartHeightData::Constant(parse_absolute_start_anchor( + Some("minecraft:constant") => StartHeightData::Constant(parse_vertical_anchor( required_value(value.get("value"), context, "start_height.value"), context, )), - None if value.get("value").is_some() => { - StartHeightData::Constant(parse_absolute_start_anchor( - required_value(value.get("value"), context, "start_height.value"), - context, - )) - } - None => panic!("Unsupported jigsaw start_height shape in {context}: {value}"), + None if value.get("value").is_some() => StartHeightData::Constant(parse_vertical_anchor( + required_value(value.get("value"), context, "start_height.value"), + context, + )), + None => StartHeightData::Constant(parse_vertical_anchor(value, context)), Some(other) => panic!("Unsupported jigsaw start_height provider {other} in {context}"), } } @@ -422,7 +459,13 @@ fn parse_vertical_anchor(value: &serde_json::Value, context: &str) -> VerticalAn } fn parse_height_provider(value: &serde_json::Value, context: &str) -> HeightProviderData { - match value.get("type").and_then(|v| v.as_str()) { + let provider_type = value.get("type").and_then(|v| v.as_str()).map(|raw| { + crate::generator_functions::parse_loose_identifier(raw) + .unwrap_or_else(|error| panic!("invalid height provider {raw} in {context}: {error}")) + .to_string() + }); + + match provider_type.as_deref() { Some("minecraft:uniform") => { let min = parse_vertical_anchor( required_value(value.get("min_inclusive"), context, "height.min_inclusive"), @@ -694,9 +737,17 @@ fn structure_static_ident(key: &str) -> proc_macro2::Ident { fn generate_generation_step(step: &str) -> TokenStream { match step { - "surface_structures" => quote! { StructureGenerationStep::SurfaceStructures }, + "raw_generation" => quote! { StructureGenerationStep::RawGeneration }, + "lakes" => quote! { StructureGenerationStep::Lakes }, + "local_modifications" => quote! { StructureGenerationStep::LocalModifications }, "underground_structures" => quote! { StructureGenerationStep::UndergroundStructures }, + "surface_structures" => quote! { StructureGenerationStep::SurfaceStructures }, + "strongholds" => quote! { StructureGenerationStep::Strongholds }, + "underground_ores" => quote! { StructureGenerationStep::UndergroundOres }, "underground_decoration" => quote! { StructureGenerationStep::UndergroundDecoration }, + "fluid_springs" => quote! { StructureGenerationStep::FluidSprings }, + "vegetal_decoration" => quote! { StructureGenerationStep::VegetalDecoration }, + "top_layer_modification" => quote! { StructureGenerationStep::TopLayerModification }, other => panic!("Unknown structure generation step: {other}"), } } @@ -730,7 +781,8 @@ fn generate_spawn_overrides(overrides: &[SpawnOverrideData]) -> Vec .spawns .iter() .map(|spawn| { - let entity_type = generate_identifier(&spawn.entity_type); + let entity_type = + generate_owned_identifier_from_str(&spawn.entity_type, "structure"); let weight = spawn.weight; let min_count = spawn.min_count; let max_count = spawn.max_count; @@ -757,8 +809,13 @@ fn generate_spawn_overrides(overrides: &[SpawnOverrideData]) -> Vec fn generate_start_height(height: &StartHeightData) -> TokenStream { match height { - StartHeightData::Constant(y) => quote! { StartHeight::Constant(#y) }, + StartHeightData::Constant(anchor) => { + let anchor = generate_vertical_anchor(anchor); + quote! { StartHeight::Constant(#anchor) } + } StartHeightData::Uniform { min, max } => { + let min = generate_vertical_anchor(min); + let max = generate_vertical_anchor(max); quote! { StartHeight::Uniform { min: #min, max: #max } } } } @@ -798,12 +855,21 @@ fn generate_pool_aliases(aliases: &[serde_json::Value], context: &str) -> Vec { - let a = generate_identifier(required_str(alias, &alias_context, "alias")); - let t = generate_identifier(required_str(alias, &alias_context, "target")); + let a = generate_owned_identifier_from_str( + required_str(alias, &alias_context, "alias"), + "structure", + ); + let t = generate_owned_identifier_from_str( + required_str(alias, &alias_context, "target"), + "structure", + ); quote! { PoolAlias::Direct { alias: #a, target: #t } } } "minecraft:random" => { - let a = generate_identifier(required_str(alias, &alias_context, "alias")); + let a = generate_owned_identifier_from_str( + required_str(alias, &alias_context, "alias"), + "structure", + ); let target_values = required_array(alias, &alias_context, "targets"); if target_values.is_empty() { panic!("Field targets must be non-empty in {alias_context}"); @@ -814,8 +880,10 @@ fn generate_pool_aliases(aliases: &[serde_json::Value], context: &str) -> Vec Vec) -> TokenStream { } fn generate_jigsaw_config(config: &JigsawConfigData, context: &str) -> TokenStream { - let start_pool = generate_identifier(&config.start_pool); + let start_pool = generate_owned_identifier_from_str(&config.start_pool, "structure"); let max_depth = config.max_depth; let use_expansion_hack = config.use_expansion_hack; let heightmap_token = match &config.project_start_to_heightmap { @@ -908,7 +974,7 @@ fn generate_jigsaw_config(config: &JigsawConfigData, context: &str) -> TokenStre let max_distance_from_center = config.max_distance_from_center; let start_jigsaw_name = match &config.start_jigsaw_name { Some(name) => { - let id = generate_identifier(name); + let id = generate_owned_identifier_from_str(name, "structure"); quote! { Some(#id) } } None => quote! { None }, @@ -1042,12 +1108,13 @@ pub(crate) fn build_structures() -> TokenStream { for (key, structure) in structures { let static_ident = structure_static_ident(&key); - let key_token = generate_identifier(&key); - let structure_type = generate_identifier(&structure.structure_type); + let key_token = generate_owned_identifier_from_str(&key, "structure"); + let structure_type = + generate_owned_identifier_from_str(&structure.structure_type, "structure"); let biomes: Vec = structure .allowed_biomes .iter() - .map(|b| generate_identifier(b)) + .map(|b| generate_owned_identifier_from_str(b, "structure")) .collect(); let spawn_overrides = generate_spawn_overrides(&structure.spawn_overrides); let step = generate_generation_step(&structure.step); @@ -1127,10 +1194,10 @@ pub(crate) fn build() -> TokenStream { let mut entries = TokenStream::new(); - for (set_name, set) in &sets { - let key = generate_identifier(&format!("minecraft:{set_name}")); + for (set_id, set) in &sets { + let key = generate_owned_identifier_from_str(set_id, "structure"); if set.structures.is_empty() { - panic!("Structure set {set_name} must have at least one structure"); + panic!("Structure set {set_id} must have at least one structure"); } let structures: Vec = set @@ -1139,9 +1206,9 @@ pub(crate) fn build() -> TokenStream { .enumerate() .map(|(entry_index, entry)| { if entry.weight <= 0 { - panic!("Structure set {set_name} entry {entry_index} has non-positive weight"); + panic!("Structure set {set_id} entry {entry_index} has non-positive weight"); } - let structure = generate_identifier(&entry.structure); + let structure = generate_owned_identifier_from_str(&entry.structure, "structure"); let weight = entry.weight; quote! { @@ -1155,25 +1222,24 @@ pub(crate) fn build() -> TokenStream { let freq = set.placement.frequency; if !freq.is_finite() || !(0.0..=1.0).contains(&freq) { - panic!("Structure set {set_name} has invalid placement frequency {freq}"); + panic!("Structure set {set_id} has invalid placement frequency {freq}"); } let freq_method = generate_frequency_method(&set.placement.frequency_reduction_method); let [locate_x, locate_y, locate_z] = set.placement.locate_offset.unwrap_or([0, 0, 0]); let placement = match set.placement.placement_type.as_str() { "minecraft:random_spread" => { - let spacing = required(set.placement.spacing, set_name, "placement.spacing"); - let separation = - required(set.placement.separation, set_name, "placement.separation"); + let spacing = required(set.placement.spacing, set_id, "placement.spacing"); + let separation = required(set.placement.separation, set_id, "placement.separation"); if spacing <= 0 { - panic!("Structure set {set_name} has non-positive spacing {spacing}"); + panic!("Structure set {set_id} has non-positive spacing {spacing}"); } if separation < 0 { - panic!("Structure set {set_name} has negative separation {separation}"); + panic!("Structure set {set_id} has negative separation {separation}"); } if spacing <= separation { panic!( - "Structure set {set_name} has spacing {spacing} <= separation {separation}" + "Structure set {set_id} has spacing {spacing} <= separation {separation}" ); } let salt = set.placement.salt; @@ -1182,11 +1248,11 @@ pub(crate) fn build() -> TokenStream { let exclusion = if let Some(ez) = &set.placement.exclusion_zone { if ez.chunk_count < 0 { panic!( - "Structure set {set_name} has negative exclusion chunk_count {}", + "Structure set {set_id} has negative exclusion chunk_count {}", ez.chunk_count ); } - let other = generate_identifier(&ez.other_set); + let other = generate_owned_identifier_from_str(&ez.other_set, "structure"); let count = ez.chunk_count; quote! { Some(ExclusionZoneData { @@ -1212,44 +1278,44 @@ pub(crate) fn build() -> TokenStream { } } "minecraft:concentric_rings" => { - let distance = required(set.placement.distance, set_name, "placement.distance"); - let spread = required(set.placement.spread, set_name, "placement.spread"); - let count = required(set.placement.count, set_name, "placement.count"); + let distance = required(set.placement.distance, set_id, "placement.distance"); + let spread = required(set.placement.spread, set_id, "placement.spread"); + let count = required(set.placement.count, set_id, "placement.count"); if distance <= 0 { - panic!("Structure set {set_name} has non-positive ring distance {distance}"); + panic!("Structure set {set_id} has non-positive ring distance {distance}"); } if spread <= 0 { - panic!("Structure set {set_name} has non-positive ring spread {spread}"); + panic!("Structure set {set_id} has non-positive ring spread {spread}"); } if count < 0 { - panic!("Structure set {set_name} has negative ring count {count}"); + panic!("Structure set {set_id} has negative ring count {count}"); } let salt = set.placement.salt; // Resolve preferred biomes from tag reference (e.g., "#minecraft:stronghold_biased_to") let tag_ref = required( set.placement.preferred_biomes.clone(), - set_name, + set_id, "placement.preferred_biomes", ); - let preferred_biomes: Vec = if let Some(tag_name) = - tag_ref.strip_prefix('#') - { - biome_tags - .get(tag_name) + let preferred_biomes: Vec = + if let Some(tag_name) = tag_ref.strip_prefix('#') { + let tag_name = normalize_tag_key(tag_name); + biome_tags + .get(&tag_name) .unwrap_or_else(|| { panic!( - "Missing biome tag {tag_name} referenced by structure set {set_name}" + "Missing biome tag {tag_name} referenced by structure set {set_id}" ) }) .clone() - } else { - // Direct biome identifier - vec![tag_ref.clone()] - }; + } else { + // Direct biome identifier + vec![tag_ref.clone()] + }; let biome_tokens: Vec = preferred_biomes .iter() - .map(|b| generate_identifier(b)) + .map(|b| generate_owned_identifier_from_str(b, "structure")) .collect(); quote! { From 12ae71a2eda7bd1df7ed08730c0fcb63e6c7a320 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 03:54:02 +0200 Subject: [PATCH 40/54] Update structure_sets.rs --- steel-registry/build/structure_sets.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/steel-registry/build/structure_sets.rs b/steel-registry/build/structure_sets.rs index 4d025411a9b8..b8c54b94d689 100644 --- a/steel-registry/build/structure_sets.rs +++ b/steel-registry/build/structure_sets.rs @@ -705,20 +705,6 @@ fn generate_spread_type(spread: &Option) -> TokenStream { } } -fn generate_identifier(id: &str) -> TokenStream { - if id.is_empty() { - panic!("Cannot generate an empty identifier"); - } - if let Some((namespace, path)) = id.split_once(':') { - if namespace.is_empty() || path.is_empty() { - panic!("Invalid identifier {id}"); - } - quote! { Identifier::new(#namespace, #path) } - } else { - quote! { Identifier::vanilla(#id.to_string()) } - } -} - fn structure_static_ident(key: &str) -> proc_macro2::Ident { let name = key .strip_prefix("minecraft:") From 0ffb2b998741988e9ba0305afc3af8ed06902219 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 03:54:17 +0200 Subject: [PATCH 41/54] Update structure_sets.rs --- steel-registry/build/structure_sets.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/steel-registry/build/structure_sets.rs b/steel-registry/build/structure_sets.rs index b8c54b94d689..c0b502b4798f 100644 --- a/steel-registry/build/structure_sets.rs +++ b/steel-registry/build/structure_sets.rs @@ -572,7 +572,18 @@ fn load_structure_data( }) .collect(); - let config = match structure.structure_type.as_str() { + let structure_type = + crate::generator_functions::parse_loose_identifier(&structure.structure_type) + .unwrap_or_else(|error| { + panic!( + "invalid structure type {} in {full_name}: {error}", + structure.structure_type + ) + }); + let structure_type = structure_type.to_string(); + structure.structure_type = structure_type.clone(); + + let config = match structure_type.as_str() { "minecraft:jigsaw" => { let start_pool = required(structure.start_pool.clone(), &full_name, "start_pool"); let max_depth = required(structure.max_depth, &full_name, "size"); From 8a0acb885bd9583cabfd9d87102ef26c93c0de09 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 03:54:33 +0200 Subject: [PATCH 42/54] Update structure_sets.rs --- steel-registry/build/structure_sets.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/steel-registry/build/structure_sets.rs b/steel-registry/build/structure_sets.rs index c0b502b4798f..daac99f984b2 100644 --- a/steel-registry/build/structure_sets.rs +++ b/steel-registry/build/structure_sets.rs @@ -551,16 +551,7 @@ fn load_structure_data( let structure: StructureJson = serde_json::from_str(&content) .unwrap_or_else(|e| panic!("Failed to parse structure {full_name}: {e}")); - let allowed_biomes = if let Some(tag_name) = structure.biomes.strip_prefix('#') { - biome_tags - .get(tag_name) - .unwrap_or_else(|| { - panic!("Missing biome tag {tag_name} referenced by structure {full_name}") - }) - .clone() - } else { - vec![structure.biomes.clone()] - }; + let allowed_biomes = resolve_structure_biomes(structure.biomes, biome_tags, &full_name); let spawn_overrides = structure .spawn_overrides From 2d99879a6b0dd57edc59cf09d3d93c70c77aed17 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:56:51 +0200 Subject: [PATCH 43/54] Update loot_tables.rs --- steel-registry/build/loot_tables.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/steel-registry/build/loot_tables.rs b/steel-registry/build/loot_tables.rs index f30eb89d9635..7ff6ef565e4c 100644 --- a/steel-registry/build/loot_tables.rs +++ b/steel-registry/build/loot_tables.rs @@ -27,10 +27,8 @@ enum NumberProviderJson { #[serde(default)] value: Option, #[serde(default)] - min: Option, min: Option>, #[serde(default)] - max: Option, max: Option>, #[serde(default)] n: Option, // Can be float in JSON, convert to i32 later From 4715f6bc868b5390dd45f19cc4fc6e4a41a4e11d Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:00:27 +0200 Subject: [PATCH 44/54] Update template_pools.rs --- steel-registry/build/template_pools.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/steel-registry/build/template_pools.rs b/steel-registry/build/template_pools.rs index 5f97cac11e37..c7453869cf5c 100644 --- a/steel-registry/build/template_pools.rs +++ b/steel-registry/build/template_pools.rs @@ -231,13 +231,23 @@ fn gen_identifier(id: &str) -> TokenStream { if id.is_empty() { panic!("Cannot generate an empty identifier"); } - if let Some((namespace, path)) = id.split_once(':') { - quote! { Identifier::new(#namespace, #path) } + let id = Identifier::parse_or_vanilla(id) + .unwrap_or_else(|error| panic!("invalid template pool identifier {id}: {error}")); + let namespace = id.namespace.as_ref(); + let path = id.path.as_ref(); + if namespace == Identifier::VANILLA_NAMESPACE { + quote! { Identifier::vanilla_static(#path) } } else { - quote! { Identifier::vanilla(#id.to_string()) } + quote! { Identifier::new_static(#namespace, #path) } } } +fn identifier_parts(id: &str, context: &str) -> (String, String) { + let id = Identifier::parse_or_vanilla(id) + .unwrap_or_else(|error| panic!("invalid {context} identifier {id}: {error}")); + (id.namespace.into_owned(), id.path.into_owned()) +} + fn required(value: Option, context: &str, field: &str) -> T { value.unwrap_or_else(|| panic!("Missing required field {field} in {context}")) } From 444483ae382d15dd961d0b8e177dc99b0eb46660 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:56:39 +0200 Subject: [PATCH 45/54] revert ProcessorList related --- steel-core/src/worldgen/feature/features/fossil.rs | 1 - steel-core/src/worldgen/structure_piece_placer/pool_element.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/steel-core/src/worldgen/feature/features/fossil.rs b/steel-core/src/worldgen/feature/features/fossil.rs index ccc7f3f5ab22..30c3f7b1d4b4 100644 --- a/steel-core/src/worldgen/feature/features/fossil.rs +++ b/steel-core/src/worldgen/feature/features/fossil.rs @@ -135,7 +135,6 @@ impl FeatureDecorationRunner { ) -> &'a [StructureProcessorKind] { match processors { ProcessorList::Empty => &[], - ProcessorList::Direct(processors) => processors, ProcessorList::Registry(key) => { let Some(processor_list) = registry.structure_processors.by_key(key) else { panic!("{context} references unknown processor list {key}"); diff --git a/steel-core/src/worldgen/structure_piece_placer/pool_element.rs b/steel-core/src/worldgen/structure_piece_placer/pool_element.rs index 360ebc8c9c81..9d1d9a532c58 100644 --- a/steel-core/src/worldgen/structure_piece_placer/pool_element.rs +++ b/steel-core/src/worldgen/structure_piece_placer/pool_element.rs @@ -158,7 +158,6 @@ impl StructurePiecePlacer { ) -> &'a [StructureProcessorKind] { match processors { ProcessorList::Empty => &[], - ProcessorList::Direct(processors) => processors, ProcessorList::Registry(key) => { let Some(processor_list) = registry.structure_processors.by_key(key) else { panic!("template pool references unknown processor list {key}"); From 9bd54475f66e69804c9d99716dd2242175b19b72 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:04:31 +0200 Subject: [PATCH 46/54] Update loot_table.rs --- steel-registry/src/loot_table.rs | 59 ++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/steel-registry/src/loot_table.rs b/steel-registry/src/loot_table.rs index dc73bd937aad..25a4ca9fa165 100644 --- a/steel-registry/src/loot_table.rs +++ b/steel-registry/src/loot_table.rs @@ -69,6 +69,10 @@ pub enum NumberProvider { min: f32, max: f32, }, + UniformProvider { + min: &'static NumberProvider, + max: &'static NumberProvider, + }, Binomial { n: i32, p: f32, @@ -111,6 +115,11 @@ impl NumberProvider { match self { Self::Constant(v) => *v, Self::Uniform { min, max } => rng.random_range(*min..=*max), + Self::UniformProvider { min, max } => { + let min = min.get(rng, ctx); + let max = max.get(rng, ctx); + rng.random_range(min..=max) + } Self::Binomial { n, p } => { let mut count = 0; for _ in 0..*n { @@ -142,6 +151,11 @@ impl NumberProvider { match self { Self::Constant(v) => *v, Self::Uniform { min, max } => rng.random_range(*min..=*max), + Self::UniformProvider { min, max } => { + let min = min.get_simple(rng); + let max = max.get_simple(rng); + rng.random_range(min..=max) + } Self::Binomial { n, p } => { let mut count = 0; for _ in 0..*n { @@ -582,6 +596,8 @@ pub enum ToolPredicate { }, /// Match items in a tag. Tag(Identifier), + /// Match items whose custom_data component contains the expected NBT subset. + CustomData { tag: &'static str }, /// Always matches (no predicate specified). Any, } @@ -789,6 +805,7 @@ impl ToolPredicate { // Check if the tool's item is in the specified tag REGISTRY.items.is_in_tag(tool.item, tag) } + ToolPredicate::CustomData { tag: _tag } => false, ToolPredicate::Any => true, } } @@ -1000,7 +1017,10 @@ pub enum LootFunction { /// Set the damage of the item (0.0 = broken, 1.0 = full durability). SetDamage { damage: NumberProvider, add: bool }, /// Enchant the item randomly with enchantments from options. - EnchantRandomly { options: EnchantmentOptions }, + EnchantRandomly { + options: EnchantmentOptions, + only_compatible: bool, + }, /// Enchant the item as if using an enchanting table at the specified level. EnchantWithLevels { levels: NumberProvider, @@ -1028,6 +1048,7 @@ pub enum LootFunction { decoration: Identifier, zoom: i32, skip_existing_chunks: bool, + search_radius: i32, }, /// Set the custom name of the item. SetName { @@ -1265,6 +1286,7 @@ pub enum LootEntry { /// Inline loot table (embedded pools directly in entry). InlineLootTable { pools: &'static [LootPool], + table_functions: &'static [ConditionalLootFunction], weight: i32, quality: i32, conditions: &'static [LootCondition], @@ -1405,7 +1427,7 @@ impl LootEntry { #[derive(Debug, Clone)] pub struct LootPool { pub rolls: NumberProvider, - pub bonus_rolls: f32, + pub bonus_rolls: NumberProvider, pub entries: &'static [LootEntry], pub conditions: &'static [LootCondition], pub functions: &'static [ConditionalLootFunction], @@ -1475,7 +1497,9 @@ impl LootPool { let start_index = result.len(); // Calculate number of rolls - let roll_count = self.rolls.get_int(ctx.rng) + (self.bonus_rolls * ctx.luck).floor() as i32; + let ctx_ref = LootContextRef { tool: ctx.tool }; + let bonus = self.bonus_rolls.get(ctx.rng, Some(&ctx_ref)); + let roll_count = self.rolls.get_int(ctx.rng) + (bonus * ctx.luck).floor() as i32; for _ in 0..roll_count { self.add_random_item(ctx, result); @@ -1589,14 +1613,25 @@ impl LootEntry { } } LootEntry::InlineLootTable { - pools, functions, .. + pools, + table_functions, + functions, + .. } => { // Process inline loot table pools directly let mut items = Vec::new(); for pool in *pools { pool.add_random_items(ctx, &mut items); } - // Apply functions to all items from the inline table + // Apply inline table-level functions + for item in &mut items { + for cond_func in *table_functions { + if cond_func.conditions.iter().all(|c| c.test(ctx)) { + cond_func.function.apply(item, ctx); + } + } + } + // Apply entry-level functions to all items from the inline table for item in &mut items { for cond_func in *functions { if cond_func.conditions.iter().all(|c| c.test(ctx)) { @@ -1779,7 +1814,10 @@ impl LootFunction { LootFunction::SetDamage { damage, add } => { item.set_damage_fraction(damage.get_simple(ctx.rng), *add); } - LootFunction::EnchantRandomly { options } => { + LootFunction::EnchantRandomly { + options, + only_compatible: _, + } => { // TODO: Implement when enchantment system is ready item.enchant_randomly(options, ctx.rng); } @@ -1811,9 +1849,16 @@ impl LootFunction { decoration, zoom, skip_existing_chunks, + search_radius, } => { // TODO: Implement exploration map creation - item.create_exploration_map(destination, decoration, *zoom, *skip_existing_chunks); + item.create_exploration_map( + destination, + decoration, + *zoom, + *skip_existing_chunks, + *search_radius, + ); } LootFunction::SetName { name, target } => { // TODO: Implement name setting From 5700a1be0aadd239f7ba616425fd398e1ba9aa12 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:04:59 +0200 Subject: [PATCH 47/54] Update types.rs --- steel-utils/src/types.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/steel-utils/src/types.rs b/steel-utils/src/types.rs index 4a23c1d577f9..e2685f7d4110 100644 --- a/steel-utils/src/types.rs +++ b/steel-utils/src/types.rs @@ -1308,6 +1308,31 @@ impl Identifier { } } + /// Parses an identifier, using the vanilla namespace when no namespace is present. + /// + /// Minecraft datapack JSON frequently omits `minecraft:` for vanilla ids. + pub fn parse_or_vanilla(s: &str) -> Result { + let (namespace, path) = s + .split_once(':') + .map_or((Self::VANILLA_NAMESPACE, s), |(namespace, path)| { + (namespace, path) + }); + + Self::new_validated(namespace, path) + } + + fn new_validated(namespace: &str, path: &str) -> Result { + if !Self::validate_namespace(namespace) { + return Err("Invalid namespace"); + } + + if !Self::validate_path(path) { + return Err("Invalid path"); + } + + Ok(Self::new(namespace.to_string(), path.to_string())) + } + /// Returns whether the character is a valid namespace character. #[must_use] pub const fn valid_namespace_char(char: char) -> bool { From c4ea6a877c1122acad16ea446059243de1086c58 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:16:46 +0200 Subject: [PATCH 48/54] fix around identifier --- steel-registry/build/biomes.rs | 2 +- steel-registry/build/carvers.rs | 2 +- steel-registry/build/feature_data.rs | 27 +++-- steel-registry/build/features.rs | 47 +++++--- steel-registry/build/generator_functions.rs | 54 +++++++-- steel-registry/build/items.rs | 6 +- steel-registry/build/loot_tables.rs | 113 ++++++++----------- steel-registry/build/structure_processors.rs | 8 +- steel-registry/build/structure_sets.rs | 76 +++++++++---- steel-registry/src/item_stack.rs | 1 + steel-registry/src/lib.rs | 26 ++--- steel-worldgen/build/density_functions.rs | 5 +- 12 files changed, 209 insertions(+), 158 deletions(-) diff --git a/steel-registry/build/biomes.rs b/steel-registry/build/biomes.rs index 4cabbd2cd650..118d1c149de1 100644 --- a/steel-registry/build/biomes.rs +++ b/steel-registry/build/biomes.rs @@ -623,7 +623,7 @@ pub(crate) fn build() -> TokenStream { // Generate static biome definitions let mut register_stream = TokenStream::new(); for (biome_name, biome) in &biomes { - let identifier = crate::generator_functions::parse_loose_identifier(biome_name) + let identifier = Identifier::parse_or_vanilla(biome_name) .unwrap_or_else(|e| panic!("invalid biome name {biome_name}: {e}")); let key = crate::generator_functions::generate_static_identifier(&identifier); let biome_ident_str = if identifier.namespace == steel_utils::Identifier::VANILLA_NAMESPACE diff --git a/steel-registry/build/carvers.rs b/steel-registry/build/carvers.rs index f8a2a948f85b..b397d334d1c5 100644 --- a/steel-registry/build/carvers.rs +++ b/steel-registry/build/carvers.rs @@ -318,7 +318,7 @@ pub(crate) fn build() -> TokenStream { let mut register = TokenStream::new(); for (name, kind) in &entries { - let identifier = crate::generator_functions::parse_loose_identifier(name) + let identifier = Identifier::parse_or_vanilla(name) .unwrap_or_else(|e| panic!("invalid configured carver name {name}: {e}")); let key = crate::generator_functions::generate_static_identifier(&identifier); let ident_str = if identifier.namespace == steel_utils::Identifier::VANILLA_NAMESPACE { diff --git a/steel-registry/build/feature_data.rs b/steel-registry/build/feature_data.rs index 071bb4259a3b..c30507342ec9 100644 --- a/steel-registry/build/feature_data.rs +++ b/steel-registry/build/feature_data.rs @@ -5,7 +5,6 @@ //! data in `src/feature/data.rs` can use typed registry refs because this module //! owns the extracted JSON decoding step. -use crate::generator_functions::parse_loose_identifier; use crate::shared_structs::deserialize_tag_identifier; pub use crate::shared_structs::{BlockStateData, FluidStateData}; use crate::template_pools::ProcessorsJson; @@ -16,10 +15,6 @@ use steel_utils::{ value_providers::{FloatProvider, HeightProvider, IntProvider, UniformIntProvider}, }; -fn parse_loose_identifier_de(raw: &str) -> Result { - parse_loose_identifier(raw).map_err(E::custom) -} - fn deserialize_vanilla_i32<'de, D: Deserializer<'de>>(deserializer: D) -> Result { let value = Value::deserialize(deserializer)?; let Some(number) = value.as_f64() else { @@ -36,7 +31,7 @@ fn deserialize_vanilla_i32<'de, D: Deserializer<'de>>(deserializer: D) -> Result } fn normalize_loose_identifier(raw: &str) -> String { - parse_loose_identifier(raw) + Identifier::parse_or_vanilla(raw) .unwrap_or_else(|error| panic!("invalid loose feature identifier {raw}: {error}")) .to_string() } @@ -122,7 +117,9 @@ impl<'de> Deserialize<'de> for ConfiguredFeatureRef { fn deserialize>(deserializer: D) -> Result { let value = Value::deserialize(deserializer)?; if let Some(id) = value.as_str() { - return parse_loose_identifier_de(id).map(Self::Reference); + return Identifier::parse_or_vanilla(id) + .map(Self::Reference) + .map_err(D::Error::custom); } serde_json::from_value(value) .map(Box::new) @@ -143,7 +140,9 @@ impl<'de> Deserialize<'de> for PlacedFeatureRef { fn deserialize>(deserializer: D) -> Result { let value = Value::deserialize(deserializer)?; if let Some(id) = value.as_str() { - return parse_loose_identifier_de(id).map(Self::Reference); + return Identifier::parse_or_vanilla(id) + .map(Self::Reference) + .map_err(D::Error::custom); } serde_json::from_value(value) .map(Box::new) @@ -426,17 +425,17 @@ impl<'de> Deserialize<'de> for BlockHolderSet { match Raw::deserialize(deserializer)? { Raw::Single(value) => { if let Some(tag) = value.strip_prefix('#') { - let tag = parse_loose_identifier_de(tag)?; + let tag = Identifier::parse_or_vanilla(tag).map_err(D::Error::custom)?; Ok(Self::Tag(tag)) } else { - let entry = parse_loose_identifier_de(&value)?; + let entry = Identifier::parse_or_vanilla(&value).map_err(D::Error::custom)?; Ok(Self::Entries(vec![entry])) } } Raw::Many(values) => { let entries = values .iter() - .map(|value| parse_loose_identifier_de(value)) + .map(|value| Identifier::parse_or_vanilla(value).map_err(D::Error::custom)) .collect::>()?; Ok(Self::Entries(entries)) } @@ -456,17 +455,17 @@ impl<'de> Deserialize<'de> for FluidHolderSet { match Raw::deserialize(deserializer)? { Raw::Single(value) => { if let Some(tag) = value.strip_prefix('#') { - let tag = parse_loose_identifier_de(tag)?; + let tag = Identifier::parse_or_vanilla(tag).map_err(D::Error::custom)?; Ok(Self::Tag(tag)) } else { - let entry = parse_loose_identifier_de(&value)?; + let entry = Identifier::parse_or_vanilla(&value).map_err(D::Error::custom)?; Ok(Self::Entries(vec![entry])) } } Raw::Many(values) => { let entries = values .iter() - .map(|value| parse_loose_identifier_de(value)) + .map(|value| Identifier::parse_or_vanilla(value).map_err(D::Error::custom)) .collect::>()?; Ok(Self::Entries(entries)) } diff --git a/steel-registry/build/features.rs b/steel-registry/build/features.rs index c7227118ad5d..c5da036431e4 100644 --- a/steel-registry/build/features.rs +++ b/steel-registry/build/features.rs @@ -3,7 +3,8 @@ use std::fs; use crate::generator_functions::{ - generate_static_identifier as generate_identifier, registry_entry_ident, + generate_static_identifier as generate_identifier, registry_entry_ident, resource_name, + sorted_json_files, }; use heck::ToShoutySnakeCase; use proc_macro2::{Ident, Span, TokenStream}; @@ -19,13 +20,15 @@ mod feature_data; use feature_data::*; -fn sorted_json_registry_entries( - overlay: &DatapackOverlay, - path_suffix: &str, -) -> Vec<(String, String)> { - overlay - .list_json_registry_ids_with_suffix(path_suffix) +fn sorted_json_registry_entries(dir: &str) -> Vec<(String, String)> { + sorted_json_files(dir) .into_iter() + .map(|path| { + let name = resource_name(&path); + let content = fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("failed to read {name}: {err}")); + (name, content) + }) .collect() } @@ -47,15 +50,22 @@ fn generate_fluid_ref(identifier: &Identifier) -> TokenStream { quote! { &vanilla_fluids::#ident } } +fn feature_entry_ident(identifier: &Identifier) -> Ident { + let registry_id = if identifier.namespace == Identifier::VANILLA_NAMESPACE { + identifier.path.as_ref().to_string() + } else { + format!("{}:{}", identifier.namespace, identifier.path) + }; + registry_entry_ident(®istry_id) +} + fn generate_configured_feature_entry_ref(identifier: &Identifier) -> TokenStream { - let registry_id = format!("{}:{}", identifier.namespace, identifier.path); - let ident = registry_entry_ident(®istry_id); + let ident = feature_entry_ident(identifier); quote! { &crate::vanilla_configured_features::#ident } } fn generate_placed_feature_entry_ref(identifier: &Identifier) -> TokenStream { - let registry_id = format!("{}:{}", identifier.namespace, identifier.path); - let ident = registry_entry_ident(®istry_id); + let ident = feature_entry_ident(identifier); quote! { &crate::vanilla_placed_features::#ident } } @@ -1966,11 +1976,12 @@ fn generate_vegetation_patch_kind( } } -pub(crate) fn build_configured(overlay: &DatapackOverlay) -> TokenStream { +pub(crate) fn build_configured() -> TokenStream { + let dir = "../steel-utils/build_assets/builtin_datapacks/minecraft/worldgen/configured_feature"; + println!("cargo:rerun-if-changed={dir}"); + let mut entries = Vec::new(); - for (registry_id, content) in - sorted_json_registry_entries(overlay, "worldgen/configured_feature") - { + for (registry_id, content) in sorted_json_registry_entries(dir) { let kind = parse_configured_feature_json(®istry_id, &content); entries.push((registry_id, generate_configured_feature_kind(&kind))); } @@ -1996,7 +2007,7 @@ pub(crate) fn build_configured(overlay: &DatapackOverlay) -> TokenStream { let mut register = TokenStream::new(); for (registry_id, kind) in &entries { let ident = registry_entry_ident(registry_id); - let identifier = Identifier::from_str(registry_id).unwrap_or_else(|err| { + let identifier = Identifier::parse_or_vanilla(registry_id).unwrap_or_else(|err| { panic!("invalid configured feature registry id {registry_id}: {err}") }); let key = generate_identifier(&identifier); @@ -2030,7 +2041,7 @@ pub(crate) fn build_placed() -> TokenStream { let mut entries = Vec::new(); for entry in sorted_json_files(dir) { let name = resource_name(&entry); - let path = entry.path(); + let path = entry.as_path(); let content = fs::read_to_string(&path).unwrap_or_else(|err| panic!("failed to read {name}: {err}")); let data = serde_json::from_str::(&content) @@ -2053,7 +2064,7 @@ pub(crate) fn build_placed() -> TokenStream { let mut register = TokenStream::new(); for (registry_id, data) in &entries { let ident = registry_entry_ident(registry_id); - let identifier = Identifier::from_str(registry_id).unwrap_or_else(|err| { + let identifier = Identifier::parse_or_vanilla(registry_id).unwrap_or_else(|err| { panic!("invalid placed feature registry id {registry_id}: {err}") }); let key = generate_identifier(&identifier); diff --git a/steel-registry/build/generator_functions.rs b/steel-registry/build/generator_functions.rs index d2fe23dbd485..23dce340dfc3 100644 --- a/steel-registry/build/generator_functions.rs +++ b/steel-registry/build/generator_functions.rs @@ -5,7 +5,10 @@ use heck::ToShoutySnakeCase; use proc_macro2::TokenStream; use proc_macro2::{Ident, Span}; use quote::quote; -use std::fs; +use std::{ + fs, + path::{Path, PathBuf}, +}; use steel_utils::Identifier; pub fn read_json_asset(path: &str) -> T { @@ -14,6 +17,47 @@ pub fn read_json_asset(path: &str) -> T { serde_json::from_str(&content).unwrap_or_else(|e| panic!("Failed to parse {path}: {e}")) } +pub fn sorted_json_files(dir: &str) -> Vec { + let mut files = Vec::new(); + collect_json_files(Path::new(dir), &mut files); + files.sort(); + files +} + +fn collect_json_files(dir: &Path, files: &mut Vec) { + let entries = fs::read_dir(dir) + .unwrap_or_else(|error| panic!("Failed to read {}: {error}", dir.display())); + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_json_files(&path, files); + } else if path.extension().and_then(|extension| extension.to_str()) == Some("json") { + files.push(path); + } + } +} + +pub fn resource_name(path: &Path) -> String { + let path_without_extension = path.with_extension(""); + let components: Vec<_> = path_without_extension + .components() + .map(|component| component.as_os_str().to_string_lossy()) + .collect(); + if let Some(index) = components + .iter() + .position(|component| *component == "worldgen") + { + if index + 2 < components.len() { + return components[index + 2..].join("/"); + } + } + path_without_extension + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_else(|| panic!("invalid resource file name {}", path.display())) + .to_string() +} + pub fn sort_contiguous_registry_entries( entries: &mut [T], path: &str, @@ -35,10 +79,6 @@ pub fn generate_identifier(resource: &Identifier) -> TokenStream { quote! { Identifier { namespace: Cow::Borrowed(#namespace), path: Cow::Borrowed(#path) } } } -pub fn parse_loose_identifier(raw: &str) -> Result { - Identifier::parse_or_vanilla(raw).map_err(|error| format!("{error}: {raw}")) -} - pub fn generate_static_identifier(resource: &Identifier) -> TokenStream { let namespace = resource.namespace.as_ref(); let path = resource.path.as_ref(); @@ -50,13 +90,13 @@ pub fn generate_static_identifier(resource: &Identifier) -> TokenStream { } pub fn generate_static_identifier_from_str(raw: &str, context: &str) -> TokenStream { - let identifier = parse_loose_identifier(raw) + let identifier = Identifier::parse_or_vanilla(raw) .unwrap_or_else(|error| panic!("invalid {context} identifier {raw}: {error}")); generate_static_identifier(&identifier) } pub fn generate_owned_identifier_from_str(raw: &str, context: &str) -> TokenStream { - let identifier = parse_loose_identifier(raw) + let identifier = Identifier::parse_or_vanilla(raw) .unwrap_or_else(|error| panic!("invalid {context} identifier {raw}: {error}")); let namespace = identifier.namespace.as_ref(); let path = identifier.path.as_ref(); diff --git a/steel-registry/build/items.rs b/steel-registry/build/items.rs index 1ec103ec5ead..395bdb3ac847 100644 --- a/steel-registry/build/items.rs +++ b/steel-registry/build/items.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, fs, str::FromStr}; -use crate::generator_functions::{generate_sound_event_ref, parse_loose_identifier}; +use crate::generator_functions::generate_sound_event_ref; use heck::ToShoutySnakeCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -80,7 +80,7 @@ fn parse_block_or_tag(s: &str) -> TokenStream { (false, s) }; - let id = parse_loose_identifier(rest) + let id = Identifier::parse_or_vanilla(rest) .unwrap_or_else(|error| panic!("invalid item tool rule block/tag reference {s}: {error}")); let namespace = id.namespace.as_ref(); let path = id.path.as_ref(); @@ -95,7 +95,7 @@ fn parse_block_or_tag(s: &str) -> TokenStream { } fn parse_identifier_or_vanilla(s: &str) -> Identifier { - parse_loose_identifier(s) + Identifier::parse_or_vanilla(s) .unwrap_or_else(|error| panic!("invalid item build identifier {s}: {error}")) } diff --git a/steel-registry/build/loot_tables.rs b/steel-registry/build/loot_tables.rs index 7ff6ef565e4c..0cce43391bcc 100644 --- a/steel-registry/build/loot_tables.rs +++ b/steel-registry/build/loot_tables.rs @@ -1,12 +1,12 @@ //! Build script for generating vanilla loot table definitions. -use std::{fs, path::Path}; - +use crate::generator_functions::{generate_option, generate_static_identifier_from_str}; use heck::{ToShoutySnakeCase, ToSnakeCase}; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use rustc_hash::FxHashMap; use serde::Deserialize; +use std::{fs, path::Path}; use steel_utils::Identifier; /// A number provider can be a constant number or an object with type. @@ -2063,21 +2063,11 @@ struct LootTableData { random_sequence: Option, } -pub(crate) fn build() -> TokenStream { - let loot_table_dir = "../steel-utils/build_assets/builtin_datapacks/minecraft/loot_table"; - println!("cargo:rerun-if-changed={loot_table_dir}"); - let mut tables: Vec = Vec::new(); fn parsed_loot_table_id(registry_id: &str) -> Identifier { Identifier::parse_or_vanilla(registry_id) .unwrap_or_else(|error| panic!("invalid loot table identifier {registry_id}: {error}")) } - // Recursively read all loot table JSON files - fn read_loot_tables(dir: &Path, base_dir: &Path, tables: &mut Vec) { - let entries = match fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return, - }; fn generate_loot_table_key(registry_id: &str) -> TokenStream { let id = parsed_loot_table_id(registry_id); let namespace = id.namespace.as_ref(); @@ -2089,58 +2079,6 @@ fn generate_loot_table_key(registry_id: &str) -> TokenStream { } } - for entry in entries.flatten() { - let path = entry.path(); - - if path.is_dir() { - read_loot_tables(&path, base_dir, tables); - } else if path.extension().and_then(|s| s.to_str()) == Some("json") { - let relative_path = path - .strip_prefix(base_dir) - .unwrap_or(&path) - .with_extension(""); - let key = relative_path - .to_str() - .unwrap_or("unknown") - .replace('\\', "/"); - - let content = match fs::read_to_string(&path) { - Ok(c) => c, - Err(_) => continue, - }; - - let loot_table: LootTableJson = match serde_json::from_str(&content) { - Ok(t) => t, - Err(e) => { - panic!("Failed to parse loot table {}: {}", key, e); - } - }; - - // Generate const identifier from the key - let const_name = key.replace('/', "_").to_shouty_snake_case(); - let const_ident = Ident::new(&const_name, Span::call_site()); - - let pools: Vec = loot_table.pools.iter().map(generate_pool).collect(); - let functions: Vec = - loot_table.functions.iter().map(generate_function).collect(); - - let random_sequence = loot_table - .random_sequence - .as_ref() - .map(|s| s.strip_prefix("minecraft:").unwrap_or(s).to_string()); - - tables.push(LootTableData { - key, - const_ident, - loot_type: generate_loot_type( - loot_table.loot_type.as_deref().unwrap_or("minecraft:empty"), - ), - pools, - functions, - random_sequence, - }); - } - } fn loot_table_category_key(registry_id: &str) -> String { let id = parsed_loot_table_id(registry_id); let namespace = id.namespace.as_ref(); @@ -2153,11 +2091,6 @@ fn loot_table_category_key(registry_id: &str) -> String { } } - read_loot_tables( - Path::new(loot_table_dir), - Path::new(loot_table_dir), - &mut tables, - ); fn loot_table_field_name(registry_id: &str) -> String { let id = parsed_loot_table_id(registry_id); let namespace = id.namespace.as_ref(); @@ -2216,6 +2149,48 @@ fn parse_loot_table(registry_id: &str, content: &str) -> LootTableData { } } +fn read_loot_tables(dir: &Path, base_dir: &Path, tables: &mut Vec) { + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + Err(_) => return, + }; + + for entry in entries.flatten() { + let path = entry.path(); + + if path.is_dir() { + read_loot_tables(&path, base_dir, tables); + continue; + } + + if path.extension().and_then(|extension| extension.to_str()) != Some("json") { + continue; + } + + let relative_path = path + .strip_prefix(base_dir) + .unwrap_or(&path) + .with_extension(""); + let registry_id = relative_path + .to_str() + .unwrap_or("unknown") + .replace('\\', "/"); + let content = fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("Failed to read loot table {registry_id}: {err}")); + tables.push(parse_loot_table(®istry_id, &content)); + } +} + +pub(crate) fn build() -> TokenStream { + let loot_table_dir = "../steel-utils/build_assets/builtin_datapacks/minecraft/loot_table"; + println!("cargo:rerun-if-changed={loot_table_dir}"); + let mut tables: Vec = Vec::new(); + read_loot_tables( + Path::new(loot_table_dir), + Path::new(loot_table_dir), + &mut tables, + ); + tables.sort_by(|a, b| a.registry_id.cmp(&b.registry_id)); let mut stream = TokenStream::new(); diff --git a/steel-registry/build/structure_processors.rs b/steel-registry/build/structure_processors.rs index a95ea12027d2..6d3badf9edbf 100644 --- a/steel-registry/build/structure_processors.rs +++ b/steel-registry/build/structure_processors.rs @@ -2,14 +2,12 @@ use std::fs; -use heck::ToShoutySnakeCase; -use proc_macro2::{Ident, Span, TokenStream}; use crate::generator_functions::{ generate_static_identifier as generate_identifier, generate_static_identifier_from_str, - registry_entry_ident, + registry_entry_ident, resource_name, sorted_json_files, }; +use proc_macro2::TokenStream; use quote::quote; -use steel_utils::{Identifier, value_providers::IntProvider}; use simdnbt::owned::{NbtCompound, NbtList, NbtTag}; use steel_utils::value_providers::IntProvider; @@ -412,7 +410,7 @@ pub(crate) fn build() -> TokenStream { let mut entries = Vec::new(); for entry in sorted_json_files(dir) { let name = resource_name(&entry); - let path = entry.path(); + let path = entry.as_path(); let content = fs::read_to_string(&path).unwrap_or_else(|err| panic!("failed to read {name}: {err}")); let data = serde_json::from_str::(&content) diff --git a/steel-registry/build/structure_sets.rs b/steel-registry/build/structure_sets.rs index daac99f984b2..2d6d84633620 100644 --- a/steel-registry/build/structure_sets.rs +++ b/steel-registry/build/structure_sets.rs @@ -6,6 +6,7 @@ use crate::generator_functions::generate_owned_identifier_from_str; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use serde::Deserialize; +use steel_utils::Identifier; #[derive(Deserialize, Debug)] struct StructureSetJson { @@ -124,7 +125,11 @@ fn resolve_structure_biomes( BiomeSelectorJson::List(list) => list, BiomeSelectorJson::Tag(tag) => { if let Some(tag_name) = tag.strip_prefix('#') { - let tag_name = normalize_tag_key(tag_name); + let tag_name = Identifier::parse_or_vanilla(tag_name) + .unwrap_or_else(|error| { + panic!("invalid biome tag {tag_name} in structure {full_name}: {error}") + }) + .to_string(); biome_tags .get(&tag_name) .unwrap_or_else(|| { @@ -180,6 +185,22 @@ enum TagValueJson { Optional { id: String, required: Option }, } +fn tag_value_strings(values: Vec) -> Vec { + values + .into_iter() + .filter_map(|value| match value { + TagValueJson::Plain(id) => Some(id), + TagValueJson::Optional { id, required } => { + if required == Some(false) { + None + } else { + Some(id) + } + } + }) + .collect() +} + /// Loads all biome tags from the worldgen/biome tags directory, /// then recursively resolves tag references to flat biome lists. fn load_biome_tags() -> HashMap> { @@ -224,7 +245,7 @@ fn load_tags_from_dir(dir: &str, prefix: &str, tags: &mut HashMap StartHei } let provider_type = value.get("type").and_then(|v| v.as_str()).map(|raw| { - crate::generator_functions::parse_loose_identifier(raw) + Identifier::parse_or_vanilla(raw) .unwrap_or_else(|error| { panic!("invalid jigsaw start_height provider {raw} in {context}: {error}") }) @@ -460,7 +486,7 @@ fn parse_vertical_anchor(value: &serde_json::Value, context: &str) -> VerticalAn fn parse_height_provider(value: &serde_json::Value, context: &str) -> HeightProviderData { let provider_type = value.get("type").and_then(|v| v.as_str()).map(|raw| { - crate::generator_functions::parse_loose_identifier(raw) + Identifier::parse_or_vanilla(raw) .unwrap_or_else(|error| panic!("invalid height provider {raw} in {context}: {error}")) .to_string() }); @@ -548,7 +574,7 @@ fn load_structure_data( let name = path.file_stem().unwrap().to_str().unwrap(); let full_name = format!("minecraft:{name}"); let content = fs::read_to_string(&path).unwrap(); - let structure: StructureJson = serde_json::from_str(&content) + let mut structure: StructureJson = serde_json::from_str(&content) .unwrap_or_else(|e| panic!("Failed to parse structure {full_name}: {e}")); let allowed_biomes = resolve_structure_biomes(structure.biomes, biome_tags, &full_name); @@ -563,14 +589,13 @@ fn load_structure_data( }) .collect(); - let structure_type = - crate::generator_functions::parse_loose_identifier(&structure.structure_type) - .unwrap_or_else(|error| { - panic!( - "invalid structure type {} in {full_name}: {error}", - structure.structure_type - ) - }); + let structure_type = Identifier::parse_or_vanilla(&structure.structure_type) + .unwrap_or_else(|error| { + panic!( + "invalid structure type {} in {full_name}: {error}", + structure.structure_type + ) + }); let structure_type = structure_type.to_string(); structure.structure_type = structure_type.clone(); @@ -1286,10 +1311,17 @@ pub(crate) fn build() -> TokenStream { set_id, "placement.preferred_biomes", ); - let preferred_biomes: Vec = - if let Some(tag_name) = tag_ref.strip_prefix('#') { - let tag_name = normalize_tag_key(tag_name); - biome_tags + let preferred_biomes: Vec = if let Some(tag_name) = + tag_ref.strip_prefix('#') + { + let tag_name = Identifier::parse_or_vanilla(tag_name) + .unwrap_or_else(|error| { + panic!( + "invalid preferred biome tag {tag_name} in structure set {set_id}: {error}" + ) + }) + .to_string(); + biome_tags .get(&tag_name) .unwrap_or_else(|| { panic!( @@ -1297,10 +1329,10 @@ pub(crate) fn build() -> TokenStream { ) }) .clone() - } else { - // Direct biome identifier - vec![tag_ref.clone()] - }; + } else { + // Direct biome identifier + vec![tag_ref.clone()] + }; let biome_tokens: Vec = preferred_biomes .iter() .map(|b| generate_owned_identifier_from_str(b, "structure")) diff --git a/steel-registry/src/item_stack.rs b/steel-registry/src/item_stack.rs index 45ad9c29ddb8..6a6b368ce075 100644 --- a/steel-registry/src/item_stack.rs +++ b/steel-registry/src/item_stack.rs @@ -600,6 +600,7 @@ impl ItemStack { _decoration: &Identifier, _zoom: i32, _skip_existing_chunks: bool, + _search_radius: i32, ) { // TODO: Implement exploration map creation // 1. Change item to filled_map diff --git a/steel-registry/src/lib.rs b/steel-registry/src/lib.rs index 7c9efccdac42..516fb82c983b 100644 --- a/steel-registry/src/lib.rs +++ b/steel-registry/src/lib.rs @@ -45,6 +45,7 @@ use crate::{ sound_event::SoundEventRegistry, structure::StructureRegistry, structure_processor::StructureProcessorListRegistry, + template_pool::ProcessorList, timeline::TimelineRegistry, trim_material::TrimMaterialRegistry, trim_pattern::TrimPatternRegistry, @@ -891,20 +892,8 @@ impl Registry { self.validate_placed_feature_ref(&config.feature); } ConfiguredFeatureKind::Fossil(config) => { - assert!( - self.structure_processors - .by_key(&config.fossil_processors) - .is_some(), - "fossil configured feature references unknown processor list {}", - config.fossil_processors - ); - assert!( - self.structure_processors - .by_key(&config.overlay_processors) - .is_some(), - "fossil configured feature references unknown processor list {}", - config.overlay_processors - ); + self.validate_processor_list(&config.fossil_processors); + self.validate_processor_list(&config.overlay_processors); } ConfiguredFeatureKind::SimpleRandomSelector(config) => { for feature in &config.features { @@ -924,6 +913,15 @@ impl Registry { } } + fn validate_processor_list(&self, processors: &ProcessorList) { + if let ProcessorList::Registry(id) = processors { + assert!( + self.structure_processors.by_key(id).is_some(), + "configured feature references unknown processor list {id}" + ); + } + } + fn validate_template_pool_feature_refs(&self, element: &template_pool::PoolElement) { match element { template_pool::PoolElement::Feature { feature, .. } => { diff --git a/steel-worldgen/build/density_functions.rs b/steel-worldgen/build/density_functions.rs index cfad8c135934..80e458f81631 100644 --- a/steel-worldgen/build/density_functions.rs +++ b/steel-worldgen/build/density_functions.rs @@ -622,10 +622,7 @@ fn json_spline_to_cubic(spline: &SplineJson) -> CubicSpline { }], ), SplineJson::Multipoint { coordinate, points } => CubicSpline::new( - Arc::new(DensityFunction::Reference(Reference { - id: coordinate.clone(), - resolved: None, - })), + Arc::new(json_to_df(coordinate)), points.iter().map(json_spline_point).collect(), ), } From 1e54ebc88b1358790a2deab4c12beb2f6b33577b Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:16:53 +0200 Subject: [PATCH 49/54] Update template_pool.rs --- steel-registry/src/template_pool.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/steel-registry/src/template_pool.rs b/steel-registry/src/template_pool.rs index 2a3ee4b55eaa..43c51c14e1d7 100644 --- a/steel-registry/src/template_pool.rs +++ b/steel-registry/src/template_pool.rs @@ -136,8 +136,8 @@ pub struct JigsawBlock { pub pool: Identifier, /// Joint type. pub joint: JointType, - /// Block state to replace jigsaw with after placement. - pub final_state: Identifier, + /// Block state string to replace jigsaw with after placement. + pub final_state: String, /// Priority for selecting this jigsaw among siblings in a piece (higher = tried first). pub selection_priority: i32, /// Priority for BFS queue ordering when placing children (higher = processed first). From d727ed10da17ab646ccf8eef47c6eafdc1862b7d Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:17:01 +0200 Subject: [PATCH 50/54] fmt --- steel-registry/src/blocks/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/steel-registry/src/blocks/mod.rs b/steel-registry/src/blocks/mod.rs index c6cf77b1e588..c3c5065c4c7c 100644 --- a/steel-registry/src/blocks/mod.rs +++ b/steel-registry/src/blocks/mod.rs @@ -512,7 +512,10 @@ impl BlockRegistry { properties: impl IntoIterator, ) -> Option<()> { for (prop_name, prop_value) in properties { - let Some(prop_idx) = block.properties.iter().position(|p| p.get_name() == prop_name) + let Some(prop_idx) = block + .properties + .iter() + .position(|p| p.get_name() == prop_name) else { // Vanilla's block state codec is lenient and ignores unknown properties. continue; From df8e384685984e1b91a985544ddf8e3fbd4b23ae Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:17:13 +0200 Subject: [PATCH 51/54] Update template_pools.rs --- steel-registry/build/template_pools.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/steel-registry/build/template_pools.rs b/steel-registry/build/template_pools.rs index c7453869cf5c..20f385a64521 100644 --- a/steel-registry/build/template_pools.rs +++ b/steel-registry/build/template_pools.rs @@ -5,6 +5,7 @@ use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; use serde_json::Value; +use steel_utils::Identifier; // ── JSON structures ── @@ -35,9 +36,9 @@ struct ElementJson { elements: Option>, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] #[serde(untagged)] -enum ProcessorsJson { +pub(crate) enum ProcessorsJson { Registry(String), Direct { processors: Vec }, } @@ -261,7 +262,7 @@ fn gen_projection(proj: &Option, context: &str) -> TokenStream { } } -fn gen_processors(processors: Option<&ProcessorsJson>, context: &str) -> TokenStream { +pub(crate) fn gen_processors(processors: Option<&ProcessorsJson>, context: &str) -> TokenStream { match processors { Some(ProcessorsJson::Registry(id)) => { let id = gen_identifier(id); @@ -413,7 +414,7 @@ pub(crate) fn build() -> TokenStream { let target = gen_identifier(&j.target); let pool = gen_identifier(&j.pool); let joint = gen_joint(&j.joint); - let final_state = gen_identifier(&j.final_state); + let final_state = &j.final_state; let sel_pri = j.selection_priority; let plc_pri = j.placement_priority; @@ -425,7 +426,7 @@ pub(crate) fn build() -> TokenStream { target: #target, pool: #pool, joint: #joint, - final_state: #final_state, + final_state: #final_state.to_string(), selection_priority: #sel_pri, placement_priority: #plc_pri, } From 5a22e18e4d700937b5d24905f32084cb3b537f52 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:09:14 +0200 Subject: [PATCH 52/54] fix clippy --- steel-core/src/worldgen/feature/tests.rs | 23 ++------------- steel-core/src/worldgen/template.rs | 2 +- steel-core/tests/structure_starts.rs | 1 - steel-registry/build/biomes.rs | 10 +++---- steel-registry/build/feature_data.rs | 16 ++++------ steel-registry/build/features.rs | 2 +- steel-registry/build/generator_functions.rs | 5 ++-- steel-registry/build/loot_tables.rs | 4 +++ steel-registry/build/structure_processors.rs | 2 +- steel-registry/build/template_pools.rs | 6 ---- steel-registry/src/blocks/mod.rs | 7 ++--- steel-registry/src/feature/data.rs | 4 +++ .../src/structure_processor/data.rs | 2 +- steel-worldgen/benches/jigsaw_assembly.rs | 29 +++++++++++++++---- steel-worldgen/build/multi_noise.rs | 3 +- steel-worldgen/src/state_resolver.rs | 7 ++--- steel-worldgen/src/structure/jigsaw.rs | 4 +-- 17 files changed, 57 insertions(+), 70 deletions(-) diff --git a/steel-core/src/worldgen/feature/tests.rs b/steel-core/src/worldgen/feature/tests.rs index ba7484cf97bf..2ef75a005c3b 100644 --- a/steel-core/src/worldgen/feature/tests.rs +++ b/steel-core/src/worldgen/feature/tests.rs @@ -119,7 +119,7 @@ fn structures_for_decoration_step_use_registry_order_inside_vanilla_step() { .filter(|s| s.key.namespace == "minecraft") .map(|structure| structure.key.path.as_ref()) .collect(); - let mut expected_surface = vec![ + let expected_surface = vec![ "bastion_remnant", "desert_pyramid", "end_city", @@ -147,12 +147,6 @@ fn structures_for_decoration_step_use_registry_order_inside_vanilla_step() { "village_snowy", "village_taiga", ]; - if matches!( - steel_worldgen::multi_noise::END_BIOME_SOURCE_KIND, - steel_worldgen::multi_noise::EndBiomeSourceKind::MultiNoise - ) { - expected_surface.retain(|&s| s != "end_city"); - } assert_eq!(surface_paths, expected_surface); let underground_decoration_paths: Vec<_> = underground_decoration @@ -180,20 +174,7 @@ fn structures_for_decoration_step_use_registry_order_inside_vanilla_step() { .iter() .all(|structure| structure.step.decoration_ordinal() == 7) ); - if matches!( - steel_worldgen::multi_noise::END_BIOME_SOURCE_KIND, - steel_worldgen::multi_noise::EndBiomeSourceKind::MultiNoise - ) { - assert_eq!( - FeatureDecorationRunner::structures_for_decoration_step(®istry, 0) - .iter() - .map(|s| s.key.path.as_ref()) - .collect::>(), - vec!["end_city"] - ); - } else { - assert!(FeatureDecorationRunner::structures_for_decoration_step(®istry, 0).is_empty()); - } + assert!(FeatureDecorationRunner::structures_for_decoration_step(®istry, 0).is_empty()); } #[test] diff --git a/steel-core/src/worldgen/template.rs b/steel-core/src/worldgen/template.rs index 4b192f2ea993..1a42cb082814 100644 --- a/steel-core/src/worldgen/template.rs +++ b/steel-core/src/worldgen/template.rs @@ -1391,7 +1391,7 @@ impl StructureTemplate { } } - fn processor_heightmap_type(heightmap: StructureProcessorHeightmap) -> HeightmapType { + const fn processor_heightmap_type(heightmap: StructureProcessorHeightmap) -> HeightmapType { match heightmap { StructureProcessorHeightmap::WorldSurface => HeightmapType::WorldSurface, StructureProcessorHeightmap::MotionBlocking => HeightmapType::MotionBlocking, diff --git a/steel-core/tests/structure_starts.rs b/steel-core/tests/structure_starts.rs index eb928db7d986..77ac0c0d16b0 100644 --- a/steel-core/tests/structure_starts.rs +++ b/steel-core/tests/structure_starts.rs @@ -904,7 +904,6 @@ fn processors_to_value(processors: &ProcessorList) -> Value { match processors { ProcessorList::Empty => json!({ "processors": [] }), ProcessorList::Registry(id) => Value::String(id.to_string()), - ProcessorList::Direct(_) => json!({ "processors": "direct" }), } } diff --git a/steel-registry/build/biomes.rs b/steel-registry/build/biomes.rs index 118d1c149de1..279013f96db6 100644 --- a/steel-registry/build/biomes.rs +++ b/steel-registry/build/biomes.rs @@ -178,7 +178,7 @@ enum ColorJson { } impl ColorJson { - fn to_i32(self) -> i32 { + fn into_i32(self) -> i32 { match self { ColorJson::Int(value) => value, ColorJson::Hex(hex) => parse_hex_color(&hex), @@ -209,11 +209,11 @@ impl From for BiomeEffects { BiomeEffects { fog_color: 12638463, // Default value, will be overridden from attributes sky_color: 8103167, // Default value, will be overridden from attributes - water_color: json.water_color.to_i32(), + water_color: json.water_color.into_i32(), water_fog_color: 329011, // Default value, will be overridden from attributes - foliage_color: json.foliage_color.map(ColorJson::to_i32), - grass_color: json.grass_color.map(ColorJson::to_i32), - dry_foliage_color: json.dry_foliage_color.map(ColorJson::to_i32), + foliage_color: json.foliage_color.map(ColorJson::into_i32), + grass_color: json.grass_color.map(ColorJson::into_i32), + dry_foliage_color: json.dry_foliage_color.map(ColorJson::into_i32), grass_color_modifier: json.grass_color_modifier, music: None, // Will be populated from attributes ambient_sound: None, // Will be populated from attributes diff --git a/steel-registry/build/feature_data.rs b/steel-registry/build/feature_data.rs index c30507342ec9..f2ccc0877989 100644 --- a/steel-registry/build/feature_data.rs +++ b/steel-registry/build/feature_data.rs @@ -68,9 +68,7 @@ fn normalize_datapack_type_fields(value: &mut Value) { match value { Value::Object(map) => { for (key, child) in map { - if TYPE_KEYS.contains(&key.as_str()) { - normalize_loose_identifier_value(child); - } else if key == "Name" { + if TYPE_KEYS.contains(&key.as_str()) || key == "Name" { normalize_loose_identifier_value(child); } else if matches!(key.as_str(), "blocks" | "block") { normalize_block_reference(child); @@ -95,14 +93,6 @@ pub fn parse_configured_feature_json(registry_id: &str, content: &str) -> Config .unwrap_or_else(|err| panic!("failed to parse configured feature {registry_id}: {err}")) } -pub fn parse_placed_feature_json(registry_id: &str, content: &str) -> PlacedFeatureData { - let mut value: Value = serde_json::from_str(content) - .unwrap_or_else(|err| panic!("failed to parse placed feature {registry_id}: {err}")); - normalize_datapack_type_fields(&mut value); - serde_json::from_value(value) - .unwrap_or_else(|err| panic!("failed to parse placed feature {registry_id}: {err}")) -} - /// A configured feature reference, either a registry key or an inline configured feature. #[derive(Debug, Clone)] pub enum ConfiguredFeatureRef { @@ -1189,6 +1179,10 @@ pub struct OreTarget { #[derive(Debug, Clone, Deserialize)] #[serde(tag = "predicate_type")] +#[expect( + clippy::enum_variant_names, + reason = "variant names mirror vanilla rule test names" +)] pub enum RuleTest { #[serde(rename = "minecraft:block_match")] BlockMatch { block: Identifier }, diff --git a/steel-registry/build/features.rs b/steel-registry/build/features.rs index c5da036431e4..45bec522a1c9 100644 --- a/steel-registry/build/features.rs +++ b/steel-registry/build/features.rs @@ -2043,7 +2043,7 @@ pub(crate) fn build_placed() -> TokenStream { let name = resource_name(&entry); let path = entry.as_path(); let content = - fs::read_to_string(&path).unwrap_or_else(|err| panic!("failed to read {name}: {err}")); + fs::read_to_string(path).unwrap_or_else(|err| panic!("failed to read {name}: {err}")); let data = serde_json::from_str::(&content) .unwrap_or_else(|err| panic!("failed to parse placed feature {name}: {err}")); entries.push((name, generate_placed_feature_data(&data))); diff --git a/steel-registry/build/generator_functions.rs b/steel-registry/build/generator_functions.rs index 23dce340dfc3..3069e158e356 100644 --- a/steel-registry/build/generator_functions.rs +++ b/steel-registry/build/generator_functions.rs @@ -46,10 +46,9 @@ pub fn resource_name(path: &Path) -> String { if let Some(index) = components .iter() .position(|component| *component == "worldgen") + && index + 2 < components.len() { - if index + 2 < components.len() { - return components[index + 2..].join("/"); - } + return components[index + 2..].join("/"); } path_without_extension .file_name() diff --git a/steel-registry/build/loot_tables.rs b/steel-registry/build/loot_tables.rs index 0cce43391bcc..ba18dd1082fa 100644 --- a/steel-registry/build/loot_tables.rs +++ b/steel-registry/build/loot_tables.rs @@ -79,6 +79,10 @@ enum ScoreboardTargetJson { #[derive(Deserialize, Debug, Clone)] #[serde(untagged)] +#[expect( + clippy::large_enum_variant, + reason = "build-only JSON shape mirrors vanilla loot table number providers" +)] enum NumberProviderRangeJson { Exact(f32), Range { diff --git a/steel-registry/build/structure_processors.rs b/steel-registry/build/structure_processors.rs index 6d3badf9edbf..ad2c6731a001 100644 --- a/steel-registry/build/structure_processors.rs +++ b/steel-registry/build/structure_processors.rs @@ -412,7 +412,7 @@ pub(crate) fn build() -> TokenStream { let name = resource_name(&entry); let path = entry.as_path(); let content = - fs::read_to_string(&path).unwrap_or_else(|err| panic!("failed to read {name}: {err}")); + fs::read_to_string(path).unwrap_or_else(|err| panic!("failed to read {name}: {err}")); let data = serde_json::from_str::(&content) .unwrap_or_else(|err| panic!("failed to parse structure processor list {name}: {err}")); entries.push((name, data)); diff --git a/steel-registry/build/template_pools.rs b/steel-registry/build/template_pools.rs index 20f385a64521..5f5967d4f81f 100644 --- a/steel-registry/build/template_pools.rs +++ b/steel-registry/build/template_pools.rs @@ -243,12 +243,6 @@ fn gen_identifier(id: &str) -> TokenStream { } } -fn identifier_parts(id: &str, context: &str) -> (String, String) { - let id = Identifier::parse_or_vanilla(id) - .unwrap_or_else(|error| panic!("invalid {context} identifier {id}: {error}")); - (id.namespace.into_owned(), id.path.into_owned()) -} - fn required(value: Option, context: &str, field: &str) -> T { value.unwrap_or_else(|| panic!("Missing required field {field} in {context}")) } diff --git a/steel-registry/src/blocks/mod.rs b/steel-registry/src/blocks/mod.rs index c3c5065c4c7c..3a0b8e31a70b 100644 --- a/steel-registry/src/blocks/mod.rs +++ b/steel-registry/src/blocks/mod.rs @@ -522,13 +522,10 @@ impl BlockRegistry { }; let prop = block.properties[prop_idx]; - let Some(value_idx) = prop + let value_idx = prop .get_possible_values() .iter() - .position(|v| *v == prop_value) - else { - return None; - }; + .position(|v| *v == prop_value)?; property_indices[prop_idx] = value_idx; } diff --git a/steel-registry/src/feature/data.rs b/steel-registry/src/feature/data.rs index 1154e41d7f88..72b43d9aec96 100644 --- a/steel-registry/src/feature/data.rs +++ b/steel-registry/src/feature/data.rs @@ -631,6 +631,10 @@ pub struct OreTarget { } #[derive(Debug, Clone)] +#[expect( + clippy::enum_variant_names, + reason = "variant names mirror vanilla rule test names" +)] pub enum RuleTest { BlockMatch { block: BlockRef }, BlockStateMatch { block_state: BlockStateData }, diff --git a/steel-registry/src/structure_processor/data.rs b/steel-registry/src/structure_processor/data.rs index 5947ea5233ac..bb88c013a18f 100644 --- a/steel-registry/src/structure_processor/data.rs +++ b/steel-registry/src/structure_processor/data.rs @@ -242,7 +242,7 @@ fn json_number_to_nbt_tag(value: &serde_json::Number) -> Result if let Some(value) = value.as_i64() { return i32::try_from(value) .map(NbtTag::Int) - .or_else(|_| Ok(NbtTag::Long(value))); + .or(Ok(NbtTag::Long(value))); } if let Some(value) = value.as_u64() { diff --git a/steel-worldgen/benches/jigsaw_assembly.rs b/steel-worldgen/benches/jigsaw_assembly.rs index ce9a6a1dfd64..5d8d210638e3 100644 --- a/steel-worldgen/benches/jigsaw_assembly.rs +++ b/steel-worldgen/benches/jigsaw_assembly.rs @@ -9,7 +9,7 @@ use steel_registry::test_support::init_test_registry; use steel_registry::vanilla_template_pools::{vanilla_template_pools, vanilla_templates}; use steel_registry::{ REGISTRY, RegistryExt, - structure::{JigsawConfig, StartHeight, StructureData}, + structure::{JigsawConfig, StartHeight, StructureData, VerticalAnchorData}, }; use steel_utils::Identifier; use steel_utils::random::legacy_random::LegacyRandom; @@ -75,10 +75,27 @@ fn jigsaw_assets() -> ( (pools, templates) } -fn sample_start_height(config: &JigsawConfig, rng: &mut impl Random) -> i32 { - match config.start_height { - StartHeight::Constant(y) => y, - StartHeight::Uniform { min, max } => rng.next_i32_between(min, max), +const fn resolve_vertical_anchor(anchor: &VerticalAnchorData, min_y: i32, height: i32) -> i32 { + match anchor { + VerticalAnchorData::Absolute(y) => *y, + VerticalAnchorData::AboveBottom(offset) => min_y + *offset, + VerticalAnchorData::BelowTop(offset) => min_y + height - 1 - *offset, + } +} + +fn sample_start_height( + config: &JigsawConfig, + rng: &mut impl Random, + min_y: i32, + height: i32, +) -> i32 { + match &config.start_height { + StartHeight::Constant(anchor) => resolve_vertical_anchor(anchor, min_y, height), + StartHeight::Uniform { min, max } => { + let min = resolve_vertical_anchor(min, min_y, height); + let max = resolve_vertical_anchor(max, min_y, height); + rng.next_i32_between(min, max) + } } } @@ -100,7 +117,7 @@ fn run_assembly( let mut alias_position_rng = LegacyRandom::from_seed(0); alias_position_rng.set_large_feature_seed(SEED, case.chunk_x, case.chunk_z); - let start_y = sample_start_height(config, &mut alias_position_rng); + let start_y = sample_start_height(config, &mut alias_position_rng, min_y, max_y - min_y); let mut alias_source = LegacyRandom::from_seed(SEED as u64); let mut alias_rng = alias_source diff --git a/steel-worldgen/build/multi_noise.rs b/steel-worldgen/build/multi_noise.rs index 103d7e15c5fe..e905b3d3cd3f 100644 --- a/steel-worldgen/build/multi_noise.rs +++ b/steel-worldgen/build/multi_noise.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::fs; +use std::ops::Deref; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -34,7 +35,7 @@ impl<'de> Deserialize<'de> for ParameterRange { } } -impl std::ops::Deref for ParameterRange { +impl Deref for ParameterRange { type Target = [f64; 2]; fn deref(&self) -> &Self::Target { &self.0 diff --git a/steel-worldgen/src/state_resolver.rs b/steel-worldgen/src/state_resolver.rs index 116fcc725da8..f1f778414566 100644 --- a/steel-worldgen/src/state_resolver.rs +++ b/steel-worldgen/src/state_resolver.rs @@ -35,11 +35,8 @@ impl WorldgenStateResolver { ) -> BlockStateId { let name = map_block_name(&data.name); let Some(block) = registry.blocks.by_key(&name) else { - println!( - "CRITICAL: WorldgenStateResolver references unknown block: {:?}", - name - ); - panic!("{context} references unknown block {}", name); + println!("CRITICAL: WorldgenStateResolver references unknown block: {name:?}"); + panic!("{context} references unknown block {name}"); }; Self::block_state_from_parts( registry, diff --git a/steel-worldgen/src/structure/jigsaw.rs b/steel-worldgen/src/structure/jigsaw.rs index 563da29accbd..0201b65cde16 100644 --- a/steel-worldgen/src/structure/jigsaw.rs +++ b/steel-worldgen/src/structure/jigsaw.rs @@ -117,7 +117,7 @@ pub fn resolve_aliases( map } -fn resolve_vertical_anchor(anchor: &VerticalAnchorData, min_y: i32, height: i32) -> i32 { +const fn resolve_vertical_anchor(anchor: &VerticalAnchorData, min_y: i32, height: i32) -> i32 { match anchor { VerticalAnchorData::Absolute(y) => *y, VerticalAnchorData::AboveBottom(offset) => min_y + *offset, @@ -1512,7 +1512,7 @@ mod tests { max_depth: 0, use_expansion_hack: false, project_start_to_heightmap: None, - start_height: StartHeight::Constant(70), + start_height: StartHeight::Constant(VerticalAnchorData::Absolute(70)), max_distance_from_center: 80, start_jigsaw_name: Some(Identifier::vanilla_static("bottom")), dimension_padding: DimensionPadding { bottom: 0, top: 0 }, From ebc8922a0c178c6ea9327249f0376399c1f9133a Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:24:34 +0200 Subject: [PATCH 53/54] Update shared_structs.rs --- steel-registry/build/shared_structs.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/steel-registry/build/shared_structs.rs b/steel-registry/build/shared_structs.rs index f2f4bddabf5d..8b841c6e1603 100644 --- a/steel-registry/build/shared_structs.rs +++ b/steel-registry/build/shared_structs.rs @@ -11,8 +11,6 @@ pub struct BlockStateData { pub name: Identifier, #[serde(rename = "Properties", default)] pub properties: BTreeMap, - #[serde(default, rename = "Properites")] - pub _properites: Option, } #[derive(Deserialize, Debug, Clone)] From 1f4dbf33db8048f69651e927ba3228accd1337c5 Mon Sep 17 00:00:00 2001 From: coco875 <59367621+coco875@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:00:51 +0200 Subject: [PATCH 54/54] Merge Identifier parsing and IntProvider deserialization improvements from datapack-overlay --- steel-utils/src/types.rs | 18 +++--------------- steel-utils/src/value_providers.rs | 3 +++ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/steel-utils/src/types.rs b/steel-utils/src/types.rs index e2685f7d4110..4846d350794b 100644 --- a/steel-utils/src/types.rs +++ b/steel-utils/src/types.rs @@ -1376,23 +1376,11 @@ impl FromStr for Identifier { type Err = &'static str; fn from_str(s: &str) -> Result { - let parts: Vec<&str> = s.split(':').collect(); - if parts.len() != 2 { + let Some((namespace, path)) = s.split_once(':') else { return Err("Invalid resource location"); - } - - if !Identifier::validate_namespace(parts[0]) { - return Err("Invalid namespace"); - } - - if !Identifier::validate_path(parts[1]) { - return Err("Invalid path"); - } + }; - Ok(Identifier { - namespace: Cow::Owned(parts[0].to_string()), - path: Cow::Owned(parts[1].to_string()), - }) + Identifier::new_validated(namespace, path) } } impl Serialize for Identifier { diff --git a/steel-utils/src/value_providers.rs b/steel-utils/src/value_providers.rs index cd6ec41841b6..2d04ee8be99f 100644 --- a/steel-utils/src/value_providers.rs +++ b/steel-utils/src/value_providers.rs @@ -589,6 +589,8 @@ impl<'de> Deserialize<'de> for IntProvider { Uniform { min_inclusive: i32, max_inclusive: i32, + #[serde(default, rename = "value")] + _value: Option, }, #[serde(rename = "minecraft:biased_to_bottom")] BiasedToBottom { @@ -640,6 +642,7 @@ impl<'de> Deserialize<'de> for IntProvider { Tagged::Uniform { min_inclusive, max_inclusive, + _value: _, } => Self::Uniform { min_inclusive, max_inclusive,