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