1use 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
22pub struct GeneratorOutput {
24 pub dimension_type: DimensionTypeRef,
26 pub config: toml::Value,
28 pub generator: ChunkGeneratorType,
30 pub is_flat: bool,
32 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#[derive(Debug, Clone)]
48pub struct ValidatedWorldGeneratorConfig {
49 generator: Identifier,
50 data: WorldGeneratorConfigData,
51}
52
53impl ValidatedWorldGeneratorConfig {
54 #[must_use]
56 pub const fn generator(&self) -> &Identifier {
57 &self.generator
58 }
59
60 #[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
82pub struct WorldGeneratorRegistry {
84 factories: FxHashMap<Identifier, WorldGeneratorFactory>,
85}
86
87impl WorldGeneratorRegistry {
88 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 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 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 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}