Skip to main content

steel_core/worldgen/generator/
registry.rs

1//! Runtime registry for world generator factories.
2
3use rustc_hash::{FxHashMap, FxHashSet};
4use serde::Deserialize;
5use std::iter::repeat_n;
6use std::path::Path;
7use std::sync::Arc;
8use steel_registry::dimension_type::DimensionTypeRef;
9use steel_registry::vanilla_biomes;
10use steel_registry::vanilla_dimension_types::{OVERWORLD, THE_END, THE_NETHER};
11use steel_registry::{REGISTRY, RegistryExt};
12use steel_utils::Identifier;
13use toml::map::Map;
14
15use crate::worldgen::structure::{FixedStructureBiomeProvider, StructureGenerator};
16use crate::worldgen::{
17    ChunkGeneratorType, EmptyChunkGenerator, FlatChunkGenerator, VanillaGenerator,
18};
19use steel_worldgen::biomes::BiomeSourceKind;
20use steel_worldgen::density_functions::{
21    end::EndNoiseSettings, nether::NetherNoiseSettings, overworld::OverworldNoiseSettings,
22};
23use steel_worldgen::structure::placement::load_vanilla_structure_sets;
24
25/// Fully constructed generator metadata for a world.
26pub struct GeneratorOutput {
27    /// Vanilla dimension type rules used by this loaded world.
28    pub dimension_type: DimensionTypeRef,
29    /// Generator config after applying generator defaults.
30    pub config: toml::Value,
31    /// Chunk generator instance.
32    pub generator: ChunkGeneratorType,
33    /// Whether the client should treat this as a flat world.
34    pub is_flat: bool,
35    /// Sea level sent in login/respawn packets.
36    pub sea_level: i32,
37}
38
39struct WorldGeneratorFactory {
40    validate: fn(&toml::Value) -> Result<WorldGeneratorConfigData, String>,
41    create: fn(
42        Option<&Path>,
43        &WorldGeneratorConfigData,
44        i64,
45        &rayon::ThreadPool,
46    ) -> Result<GeneratorOutput, String>,
47}
48
49/// Generator config after parsing, validation, and default application.
50#[derive(Debug, Clone)]
51pub struct ValidatedWorldGeneratorConfig {
52    generator: Identifier,
53    data: WorldGeneratorConfigData,
54}
55
56impl ValidatedWorldGeneratorConfig {
57    /// Generator factory identifier this config was validated for.
58    #[must_use]
59    pub const fn generator(&self) -> &Identifier {
60        &self.generator
61    }
62
63    /// Dimension type selected by this validated generator config.
64    #[must_use]
65    pub fn dimension_type(&self) -> DimensionTypeRef {
66        match &self.data {
67            WorldGeneratorConfigData::Empty => fixed_generator_dimension_type(&self.generator),
68            WorldGeneratorConfigData::EmptyWorld(config) => {
69                validated_dimension_type_by_key(&config.dimension_type)
70            }
71            WorldGeneratorConfigData::Flat(config) => {
72                validated_dimension_type_by_key(&config.dimension_type)
73            }
74        }
75    }
76}
77
78#[derive(Debug, Clone)]
79enum WorldGeneratorConfigData {
80    Empty,
81    EmptyWorld(DimensionTypeOnlyConfig),
82    Flat(FlatGeneratorConfig),
83}
84
85/// Registry of server-side world generator factories.
86pub struct WorldGeneratorRegistry {
87    factories: FxHashMap<Identifier, WorldGeneratorFactory>,
88}
89
90impl WorldGeneratorRegistry {
91    /// Creates a registry containing Steel's built-in generator factories.
92    pub fn new_with_builtins() -> Result<Self, String> {
93        let mut registry = Self {
94            factories: FxHashMap::default(),
95        };
96
97        registry.register(
98            Identifier::vanilla_static("overworld"),
99            WorldGeneratorFactory {
100                validate: validate_empty_config,
101                create: create_overworld,
102            },
103        )?;
104        registry.register(
105            Identifier::vanilla_static("the_nether"),
106            WorldGeneratorFactory {
107                validate: validate_empty_config,
108                create: create_nether,
109            },
110        )?;
111        registry.register(
112            Identifier::vanilla_static("the_end"),
113            WorldGeneratorFactory {
114                validate: validate_empty_config,
115                create: create_end,
116            },
117        )?;
118        registry.register(
119            Identifier::vanilla_static("flat"),
120            WorldGeneratorFactory {
121                validate: validate_flat_config,
122                create: create_flat,
123            },
124        )?;
125        registry.register(
126            Identifier::from_steel("empty"),
127            WorldGeneratorFactory {
128                validate: validate_empty_world_config,
129                create: create_empty,
130            },
131        )?;
132
133        Ok(registry)
134    }
135
136    fn register(&mut self, key: Identifier, factory: WorldGeneratorFactory) -> Result<(), String> {
137        if self.factories.insert(key.clone(), factory).is_some() {
138            return Err(format!("duplicate world generator registration {key}"));
139        }
140        Ok(())
141    }
142
143    /// Parses and validates config for a generator ID.
144    pub fn validate_config(
145        &self,
146        key: &Identifier,
147        config: &toml::Value,
148    ) -> Result<ValidatedWorldGeneratorConfig, String> {
149        let factory = self
150            .factories
151            .get(key)
152            .ok_or_else(|| format!("unknown world generator {key}"))?;
153        let data = (factory.validate)(config)?;
154        Ok(ValidatedWorldGeneratorConfig {
155            generator: key.clone(),
156            data,
157        })
158    }
159
160    /// Creates a generator from a validated generator ID and config.
161    pub fn create(
162        &self,
163        world_path: Option<&Path>,
164        config: &ValidatedWorldGeneratorConfig,
165        seed: i64,
166        thread_pool: Arc<rayon::ThreadPool>,
167    ) -> Result<GeneratorOutput, String> {
168        let factory = self
169            .factories
170            .get(&config.generator)
171            .ok_or_else(|| format!("unknown world generator {}", config.generator))?;
172        (factory.create)(world_path, &config.data, seed, &thread_pool)
173    }
174}
175
176#[derive(Debug, Clone, Deserialize)]
177#[serde(deny_unknown_fields)]
178struct DimensionTypeOnlyConfig {
179    dimension_type: Identifier,
180}
181
182#[derive(Debug, Clone, Deserialize)]
183#[serde(deny_unknown_fields)]
184struct FlatGeneratorConfig {
185    #[serde(default = "default_flat_dimension_type")]
186    dimension_type: Identifier,
187    #[serde(default = "default_flat_layers")]
188    layers: Vec<FlatLayerConfig>,
189    #[serde(default)]
190    features: bool,
191    #[serde(default)]
192    lakes: bool,
193    #[serde(default = "default_flat_structure_overrides")]
194    structure_overrides: Vec<Identifier>,
195}
196
197#[derive(Debug, Clone, Deserialize)]
198#[serde(deny_unknown_fields)]
199struct FlatLayerConfig {
200    block: Identifier,
201    height: usize,
202}
203
204const fn default_flat_dimension_type() -> Identifier {
205    Identifier::vanilla_static("overworld")
206}
207
208fn default_flat_layers() -> Vec<FlatLayerConfig> {
209    vec![
210        FlatLayerConfig {
211            block: Identifier::vanilla_static("bedrock"),
212            height: 1,
213        },
214        FlatLayerConfig {
215            block: Identifier::vanilla_static("dirt"),
216            height: 2,
217        },
218        FlatLayerConfig {
219            block: Identifier::vanilla_static("grass_block"),
220            height: 1,
221        },
222    ]
223}
224
225fn default_flat_structure_overrides() -> Vec<Identifier> {
226    vec![
227        Identifier::vanilla_static("strongholds"),
228        Identifier::vanilla_static("villages"),
229    ]
230}
231
232fn validate_empty_config(config: &toml::Value) -> Result<WorldGeneratorConfigData, String> {
233    let Some(table) = config.as_table() else {
234        return Err("generator config must be a table".to_owned());
235    };
236    if !table.is_empty() {
237        return Err("this generator does not accept config".to_owned());
238    }
239    Ok(WorldGeneratorConfigData::Empty)
240}
241
242fn validate_empty_world_config(config: &toml::Value) -> Result<WorldGeneratorConfigData, String> {
243    let parsed: DimensionTypeOnlyConfig = config
244        .clone()
245        .try_into()
246        .map_err(|e| format!("invalid steel:empty config: {e}"))?;
247    dimension_type_by_key(&parsed.dimension_type)?;
248    Ok(WorldGeneratorConfigData::EmptyWorld(parsed))
249}
250
251fn validate_flat_config(config: &toml::Value) -> Result<WorldGeneratorConfigData, String> {
252    let parsed = parse_flat_config(config)?;
253    if parsed.layers.is_empty() {
254        return Err("minecraft:flat requires at least one layer".to_owned());
255    }
256    // TODO: Implement vanilla FlatLevelGeneratorSettings::adjustGenerationSettings for these flags.
257    if parsed.features {
258        return Err("minecraft:flat features=true is not implemented yet".to_owned());
259    }
260    if parsed.lakes {
261        return Err("minecraft:flat lakes=true is not implemented yet".to_owned());
262    }
263    dimension_type_by_key(&parsed.dimension_type)?;
264    for layer in &parsed.layers {
265        if layer.height == 0 {
266            return Err("minecraft:flat layer height must be greater than zero".to_owned());
267        }
268        if REGISTRY.blocks.by_key(&layer.block).is_none() {
269            return Err(format!(
270                "unknown block {} in minecraft:flat layer",
271                layer.block
272            ));
273        }
274    }
275    let available_structure_sets: FxHashSet<_> = load_vanilla_structure_sets()
276        .into_iter()
277        .map(|(key, _)| key)
278        .collect();
279    for structure_set in &parsed.structure_overrides {
280        if !available_structure_sets.contains(structure_set) {
281            return Err(format!(
282                "unknown structure set {structure_set} in minecraft:flat structure_overrides"
283            ));
284        }
285    }
286    Ok(WorldGeneratorConfigData::Flat(parsed))
287}
288
289fn parse_flat_config(config: &toml::Value) -> Result<FlatGeneratorConfig, String> {
290    config
291        .clone()
292        .try_into()
293        .map_err(|e| format!("invalid minecraft:flat config: {e}"))
294}
295
296fn create_overworld(
297    world_path: Option<&Path>,
298    config: &WorldGeneratorConfigData,
299    seed: i64,
300    thread_pool: &rayon::ThreadPool,
301) -> Result<GeneratorOutput, String> {
302    let WorldGeneratorConfigData::Empty = config else {
303        return Err("validated config does not match minecraft:overworld".to_owned());
304    };
305    let seed = seed as u64;
306    Ok(GeneratorOutput {
307        dimension_type: &OVERWORLD,
308        config: empty_config(),
309        generator: ChunkGeneratorType::Overworld(VanillaGenerator::new(
310            world_path,
311            BiomeSourceKind::overworld(seed),
312            seed,
313            thread_pool,
314        )),
315        is_flat: false,
316        sea_level: sea_level_for_dimension_type(&OVERWORLD),
317    })
318}
319
320fn create_nether(
321    world_path: Option<&Path>,
322    config: &WorldGeneratorConfigData,
323    seed: i64,
324    thread_pool: &rayon::ThreadPool,
325) -> Result<GeneratorOutput, String> {
326    let WorldGeneratorConfigData::Empty = config else {
327        return Err("validated config does not match minecraft:the_nether".to_owned());
328    };
329    let seed = seed as u64;
330    Ok(GeneratorOutput {
331        dimension_type: &THE_NETHER,
332        config: empty_config(),
333        generator: ChunkGeneratorType::Nether(VanillaGenerator::new(
334            world_path,
335            BiomeSourceKind::nether(seed),
336            seed,
337            thread_pool,
338        )),
339        is_flat: false,
340        sea_level: sea_level_for_dimension_type(&THE_NETHER),
341    })
342}
343
344fn create_end(
345    world_path: Option<&Path>,
346    config: &WorldGeneratorConfigData,
347    seed: i64,
348    thread_pool: &rayon::ThreadPool,
349) -> Result<GeneratorOutput, String> {
350    let WorldGeneratorConfigData::Empty = config else {
351        return Err("validated config does not match minecraft:the_end".to_owned());
352    };
353    let seed = seed as u64;
354    Ok(GeneratorOutput {
355        dimension_type: &THE_END,
356        config: empty_config(),
357        generator: ChunkGeneratorType::End(VanillaGenerator::new(
358            world_path,
359            BiomeSourceKind::end(seed),
360            seed,
361            thread_pool,
362        )),
363        is_flat: false,
364        sea_level: sea_level_for_dimension_type(&THE_END),
365    })
366}
367
368fn create_flat(
369    world_path: Option<&Path>,
370    config: &WorldGeneratorConfigData,
371    seed: i64,
372    thread_pool: &rayon::ThreadPool,
373) -> Result<GeneratorOutput, String> {
374    let WorldGeneratorConfigData::Flat(parsed) = config else {
375        return Err("validated config does not match minecraft:flat".to_owned());
376    };
377    let dimension_type = dimension_type_by_key(&parsed.dimension_type)?;
378    let normalized_config = normalized_flat_config(parsed);
379    let mut layers = Vec::new();
380    for layer in &parsed.layers {
381        let block = REGISTRY
382            .blocks
383            .by_key(&layer.block)
384            .ok_or_else(|| format!("unknown block {} in minecraft:flat layer", layer.block))?;
385        let state = REGISTRY.blocks.get_default_state_id(block);
386        layers.extend(repeat_n(state, layer.height));
387    }
388
389    let structure_generator = if parsed.structure_overrides.is_empty() {
390        None
391    } else {
392        let structure_sets = load_vanilla_structure_sets()
393            .into_iter()
394            .filter(|(key, _)| parsed.structure_overrides.contains(key))
395            .collect();
396        let biome_provider = FixedStructureBiomeProvider::new(&vanilla_biomes::PLAINS);
397        Some(StructureGenerator::vanilla_flat_with_structure_sets(
398            seed,
399            world_path,
400            &biome_provider,
401            structure_sets,
402            thread_pool,
403        ))
404    };
405
406    Ok(GeneratorOutput {
407        dimension_type,
408        config: normalized_config,
409        generator: ChunkGeneratorType::Flat(FlatChunkGenerator::new_layers_with_structures(
410            layers,
411            seed,
412            FlatChunkGenerator::SEA_LEVEL,
413            structure_generator,
414        )),
415        is_flat: true,
416        sea_level: FlatChunkGenerator::SEA_LEVEL,
417    })
418}
419
420fn create_empty(
421    _world_path: Option<&Path>,
422    config: &WorldGeneratorConfigData,
423    _seed: i64,
424    _thread_pool: &rayon::ThreadPool,
425) -> Result<GeneratorOutput, String> {
426    let WorldGeneratorConfigData::EmptyWorld(parsed) = config else {
427        return Err("validated config does not match steel:empty".to_owned());
428    };
429    let dimension_type = dimension_type_by_key(&parsed.dimension_type)?;
430    Ok(GeneratorOutput {
431        dimension_type,
432        config: normalized_dimension_type_config(&parsed.dimension_type),
433        generator: ChunkGeneratorType::Empty(EmptyChunkGenerator::new()),
434        is_flat: false,
435        sea_level: sea_level_for_dimension_type(dimension_type),
436    })
437}
438
439fn empty_config() -> toml::Value {
440    toml::Value::Table(Map::new())
441}
442
443fn normalized_dimension_type_config(dimension_type: &Identifier) -> toml::Value {
444    toml::Value::Table(normalized_dimension_type_table(dimension_type))
445}
446
447fn normalized_dimension_type_table(dimension_type: &Identifier) -> Map<String, toml::Value> {
448    let mut table = Map::new();
449    table.insert(
450        "dimension_type".to_owned(),
451        toml::Value::String(dimension_type.to_string()),
452    );
453    table
454}
455
456fn normalized_flat_config(config: &FlatGeneratorConfig) -> toml::Value {
457    let mut table = normalized_dimension_type_table(&config.dimension_type);
458    let layers = config
459        .layers
460        .iter()
461        .map(|layer| {
462            let mut layer_table = Map::new();
463            layer_table.insert(
464                "block".to_owned(),
465                toml::Value::String(layer.block.to_string()),
466            );
467            layer_table.insert(
468                "height".to_owned(),
469                toml::Value::Integer(layer.height as i64),
470            );
471            toml::Value::Table(layer_table)
472        })
473        .collect();
474    table.insert("layers".to_owned(), toml::Value::Array(layers));
475    table.insert("features".to_owned(), toml::Value::Boolean(config.features));
476    table.insert("lakes".to_owned(), toml::Value::Boolean(config.lakes));
477    table.insert(
478        "structure_overrides".to_owned(),
479        toml::Value::Array(
480            config
481                .structure_overrides
482                .iter()
483                .map(|key| toml::Value::String(key.to_string()))
484                .collect(),
485        ),
486    );
487    toml::Value::Table(table)
488}
489
490fn dimension_type_by_key(key: &Identifier) -> Result<DimensionTypeRef, String> {
491    REGISTRY
492        .dimension_types
493        .by_key(key)
494        .ok_or_else(|| format!("unknown dimension type {key}"))
495}
496
497fn validated_dimension_type_by_key(key: &Identifier) -> DimensionTypeRef {
498    match dimension_type_by_key(key) {
499        Ok(dimension_type) => dimension_type,
500        Err(error) => panic!("validated generator config should have a dimension type: {error}"),
501    }
502}
503
504fn fixed_generator_dimension_type(generator: &Identifier) -> DimensionTypeRef {
505    if generator == &Identifier::vanilla_static("overworld") {
506        &OVERWORLD
507    } else if generator == &Identifier::vanilla_static("the_nether") {
508        &THE_NETHER
509    } else if generator == &Identifier::vanilla_static("the_end") {
510        &THE_END
511    } else {
512        panic!("validated empty config does not have a fixed dimension type for {generator}")
513    }
514}
515
516fn sea_level_for_dimension_type(dimension_type: DimensionTypeRef) -> i32 {
517    if dimension_type == &THE_NETHER {
518        NetherNoiseSettings::SEA_LEVEL
519    } else if dimension_type == &THE_END {
520        EndNoiseSettings::SEA_LEVEL
521    } else {
522        OverworldNoiseSettings::SEA_LEVEL
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use steel_registry::init_vanilla_registry;
530
531    #[test]
532    fn default_flat_config_matches_vanilla_superflat() {
533        use std::panic;
534        use std::thread;
535
536        // Larger stack to match chunk_stage_hashes.rs / structure_starts.rs —
537        // loading the default structure overrides (strongholds, villages)
538        // recurses deeply for jigsaw assembly.
539        let result = thread::Builder::new()
540            .stack_size(16 * 1024 * 1024)
541            .spawn(default_flat_config_matches_vanilla_superflat_inner)
542            .expect("Failed to spawn test thread")
543            .join();
544
545        if let Err(payload) = result {
546            panic::resume_unwind(payload);
547        }
548    }
549
550    fn default_flat_config_matches_vanilla_superflat_inner() {
551        init_vanilla_registry();
552
553        let registry = WorldGeneratorRegistry::new_with_builtins()
554            .expect("built-in generator registry should initialize");
555        let config = registry
556            .validate_config(
557                &Identifier::vanilla_static("flat"),
558                &toml::Value::Table(Map::new()),
559            )
560            .expect("default flat config should validate");
561        let thread_pool = Arc::new(
562            rayon::ThreadPoolBuilder::default()
563                .build()
564                .expect("Cannot create a thread pool."),
565        );
566        let output = registry
567            .create(None, &config, 0, thread_pool)
568            .expect("default flat config should create a generator");
569        let config = output
570            .config
571            .as_table()
572            .expect("normalized flat config should be a table");
573
574        assert_eq!(
575            config.get("dimension_type"),
576            Some(&toml::Value::String("minecraft:overworld".to_owned()))
577        );
578        assert_eq!(config.get("features"), Some(&toml::Value::Boolean(false)));
579        assert_eq!(config.get("lakes"), Some(&toml::Value::Boolean(false)));
580        assert_eq!(
581            config.get("structure_overrides"),
582            Some(&toml::Value::Array(vec![
583                toml::Value::String("minecraft:strongholds".to_owned()),
584                toml::Value::String("minecraft:villages".to_owned()),
585            ]))
586        );
587    }
588
589    #[test]
590    fn rejects_unimplemented_flat_decoration_options() {
591        let features_config = toml::Value::Table(Map::from_iter([(
592            "features".to_owned(),
593            toml::Value::Boolean(true),
594        )]));
595        let features_error = validate_flat_config(&features_config)
596            .expect_err("features=true should not use non-vanilla decoration");
597        assert!(features_error.contains("features=true"));
598
599        let lakes_config = toml::Value::Table(Map::from_iter([(
600            "lakes".to_owned(),
601            toml::Value::Boolean(true),
602        )]));
603        let lakes_error = validate_flat_config(&lakes_config)
604            .expect_err("lakes=true should not use non-vanilla decoration");
605        assert!(lakes_error.contains("lakes=true"));
606    }
607}