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::density_functions::{
21 end::EndNoiseSettings, nether::NetherNoiseSettings, overworld::OverworldNoiseSettings,
22};
23use steel_worldgen::structure::placement::load_vanilla_structure_sets;
24
25pub struct GeneratorOutput {
27 pub dimension_type: DimensionTypeRef,
29 pub config: toml::Value,
31 pub generator: ChunkGeneratorType,
33 pub is_flat: bool,
35 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#[derive(Debug, Clone)]
51pub struct ValidatedWorldGeneratorConfig {
52 generator: Identifier,
53 data: WorldGeneratorConfigData,
54}
55
56impl ValidatedWorldGeneratorConfig {
57 #[must_use]
59 pub const fn generator(&self) -> &Identifier {
60 &self.generator
61 }
62
63 #[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
85pub struct WorldGeneratorRegistry {
87 factories: FxHashMap<Identifier, WorldGeneratorFactory>,
88}
89
90impl WorldGeneratorRegistry {
91 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 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 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 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 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}