The Void superflat preset gets its stone start platform

FlatLevelGeneratorSettings.adjustGenerationSettings decorates a flat
world with its biome's placed features when `features` is on and the
world isn't all air, or its biome is the_void. The Void preset (the_void,
features, one air layer) so places the_void's only feature,
void_start_platform: a 33x33 stone square with cobblestone at 8, -61, 8.
FlatTerrain placed only the layers, so players fell into the void.

Biomes now keep each biome's `features` by step, FlatTerrain works out
the adjusted list and places void_start_platform (the ported
VoidStartPlatformFeature) when it is there. The other features, lakes
and structures are still not placed (#2912).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
d3grandoandClaude Opus 5.5 committed 2026-10-30 06:28:52 +07:00
1 parent dbf445db2d
commit 8c1f50f616
2 files changed
+205 -3

No files matched your search

+16
View File
@@ -102,6 +102,9 @@ pub struct BiomeInfo {
/// The biome's own fog colour attribute; otherwise the dimension's applies.
pub fog: Option<u32>,
pub water_fog: u32,
/// `BiomeGenerationSettings.features`: the placed feature ids of each
/// `GenerationStep.Decoration` step, in order.
pub features: Vec<Vec<String>>,
}
impl BiomeInfo {
/// `Biome.getHeightAdjustedTemperature`: the frozen modifier, then cooler
@@ -235,6 +238,19 @@ impl Biomes {
fog: color(&attributes["minecraft:visual/fog_color"]),
water_fog: color(&attributes["minecraft:visual/water_fog_color"])
.unwrap_or(0x050533),
features: v["features"]
.as_array()
.into_iter()
.flatten()
.map(|step| {
step.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect()
})
.collect(),
name,
});
}
+189 -3
View File
@@ -1,17 +1,69 @@
//! FlatLevelSource: superflat worlds (`minecraft:flat` generator).
use crate::{chunk::Chunk, streaming::ChunkGenerator};
use horizon_world::{nbt::Tag, registry::BlockRegistry};
use horizon_world::{
nbt::Tag,
registry::{BlockRegistry, BlockStateId},
worldgen::feature::{BasicFeature, FeatureBlocks, FeatureWorld},
};
/// The layers of a flat generator, bottom up from the world's minimum Y.
pub struct FlatTerrain {
layers: Vec<u32>,
biome: u16,
surface: Option<crate::lod::Surface>,
/// The blocks of `minecraft:void_start_platform`, when the adjusted
/// generation settings place it (the Void preset).
void_platform: Option<FeatureBlocks>,
}
/// `GenerationStep.Decoration` ordinals `adjustGenerationSettings` leaves out.
const LAKES: usize = 1;
const UNDERGROUND_STRUCTURES: usize = 3;
const SURFACE_STRUCTURES: usize = 4;
/// FlatLevelGeneratorSettings.adjustGenerationSettings for the world's own
/// biome: its placed features, by step, when `features` is on and the world
/// isn't only air (`voidGen`) or its biome is the_void. The structure steps
/// are left out, and the lakes step when `lakes` puts the lava lakes there.
fn decoration_features(
biome_features: &[Vec<String>],
features: bool,
lakes: bool,
void_gen: bool,
the_void: bool,
) -> impl Iterator<Item = &str> {
let decorate = (!void_gen || the_void) && features;
biome_features
.iter()
.enumerate()
.filter(move |&(step, _)| {
decorate
&& step != UNDERGROUND_STRUCTURES
&& step != SURFACE_STRUCTURES
&& !(lakes && step == LAKES)
})
.flat_map(|(_, step)| step.iter().map(String::as_str))
}
/// The proto chunk as the world a feature writes in.
struct ChunkWorld<'a>(&'a mut Chunk);
impl FeatureWorld for ChunkWorld<'_> {
type Error = std::convert::Infallible;
fn block(&mut self, [x, y, z]: [i32; 3]) -> Result<BlockStateId, Self::Error> {
Ok(self.0.block(x, y, z))
}
fn set_block(
&mut self,
[x, y, z]: [i32; 3],
state: BlockStateId,
_: i32,
) -> Result<bool, Self::Error> {
self.0.set(x, y, z, state.0);
Ok(true)
}
}
impl FlatTerrain {
/// The overworld generator of `world_gen_settings` (`data.dimensions`), if it
/// is flat. Its `features` and `lakes` decorate the world; a world without
/// them (the default) gets only its layers, as FlatLevelSource places them.
/// is flat. Its layers, as FlatLevelSource places them, and of the features
/// its settings decorate the world with, `void_start_platform`; the other
/// features and the lakes are not placed yet.
pub fn from_settings(
settings: &Tag,
registry: &BlockRegistry,
@@ -36,6 +88,29 @@ impl FlatTerrain {
}
}
let biome = biomes.id(&biome);
// FlatLevelGeneratorSettings.updateLayers: every layer is Blocks.AIR.
let void_gen = layers.iter().all(|&id| {
registry
.state(BlockStateId(id))
.is_some_and(|s| s.name == "minecraft:air")
});
let flag = |key| crate::save::number(s.get(key)).is_some_and(|v| v != 0);
let info = biomes.get(biome);
let void_platform = if decoration_features(
&info.features,
flag("features"),
flag("lakes"),
void_gen,
info.name == "minecraft:the_void",
)
.any(|f| f == "minecraft:void_start_platform")
{
FeatureBlocks::from_registry(registry)
.inspect_err(|e| log::warn!("Horizon: no void_start_platform: {e}"))
.ok()
} else {
None
};
let mut water = None;
let mut surface = None;
for (i, &id) in layers
@@ -69,6 +144,7 @@ impl FlatTerrain {
layers,
biome,
surface,
void_platform,
})
}
/// FlatLevelSource.getSpawnHeight: the top of the layers.
@@ -207,6 +283,21 @@ impl ChunkGenerator for FlatTerrain {
}
}
}
// ChunkGenerator.applyBiomeDecoration at the chunk's section origin.
// VoidStartPlatformFeature writes only inside its own chunk and draws
// no random numbers, so placing it here gives the FEATURES result.
if let Some(blocks) = self.void_platform {
let origin = [cx * 16, crate::chunk::MIN_Y, cz * 16];
let mut random = horizon_core::random::LegacyRandom::new(0);
if let Err(e) = BasicFeature::VoidStartPlatform.place(
&mut ChunkWorld(&mut chunk),
&mut random,
origin,
blocks,
) {
log::warn!("Horizon: placing void_start_platform failed: {e}");
}
}
for s in &mut chunk.sections {
std::sync::Arc::make_mut(s).compact();
}
@@ -233,6 +324,101 @@ mod tests {
)
}
/// A world_gen_settings with a flat overworld of `layers` (block, height).
fn flat_world(biome: &str, features: bool, layers: &[(&str, i32)]) -> Tag {
let layers = layers
.iter()
.map(|&(block, height)| {
Tag::compound([
("block", Tag::String(block.into())),
("height", Tag::Int(height)),
])
})
.collect();
let settings = Tag::compound([
("biome", Tag::String(biome.into())),
("features", Tag::Byte(i8::from(features))),
("lakes", Tag::Byte(0)),
("layers", Tag::List(layers)),
]);
let generator = Tag::compound([
("type", Tag::String("minecraft:flat".into())),
("settings", settings),
]);
Tag::compound([(
"dimensions",
Tag::compound([(
"minecraft:overworld",
Tag::compound([("generator", generator)]),
)]),
)])
}
#[test]
fn the_void_preset_places_the_start_platform() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client")
.join(horizon_core::VANILLA_VERSION);
if !root.join("movement-properties.nbt").exists() {
return;
}
let assets = crate::assets::Assets::load(&root).unwrap();
let registry = assets.registry;
let biomes = crate::biome::Biomes::load(&root).unwrap();
let terrain = |biome, features, layers: &[(&str, i32)]| {
FlatTerrain::from_settings(&flat_world(biome, features, layers), registry, &biomes)
.unwrap()
};
let block = |terrain: &FlatTerrain, [x, y, z]: [i32; 3]| {
let chunk = terrain.finish(terrain.proto(&mut (), x >> 4, z >> 4));
registry.state(chunk.block(x, y, z)).unwrap().name.clone()
};
// The Void preset (flat_level_generator_preset/the_void): the_void's
// only feature, VoidStartPlatformFeature, at the minimum Y + 3: a
// 33×33 stone square around 8, 8 with cobblestone at its centre, in
// the chunks within one of chunk 0, 0.
let void = terrain("minecraft:the_void", true, &[("minecraft:air", 1)]);
for (pos, expected) in [
([8, -61, 8], "minecraft:cobblestone"),
([0, -61, 0], "minecraft:stone"),
([-8, -61, -8], "minecraft:stone"),
([24, -61, 24], "minecraft:stone"),
([24, -61, -8], "minecraft:stone"),
([-9, -61, 8], "minecraft:air"),
([25, -61, 8], "minecraft:air"),
([8, -60, 8], "minecraft:air"),
([8, -62, 8], "minecraft:air"),
([40, -61, 8], "minecraft:air"),
] {
assert_eq!(block(&void, pos), expected, "{pos:?}");
}
// The decoration is off without `features`.
let plain = terrain("minecraft:the_void", false, &[("minecraft:air", 1)]);
assert_eq!(block(&plain, [8, -61, 8]), "minecraft:air");
// the_void over solid layers still gets its platform.
let floor = terrain("minecraft:the_void", true, &[("minecraft:bedrock", 1)]);
assert_eq!(block(&floor, [8, -64, 8]), "minecraft:bedrock");
assert_eq!(block(&floor, [8, -61, 8]), "minecraft:cobblestone");
}
#[test]
fn adjusted_generation_settings_keep_the_biome_features_outside_structures() {
let steps: Vec<Vec<String>> = (0..11).map(|step| vec![format!("f{step}")]).collect();
let kept = |features, lakes, void_gen, the_void| {
decoration_features(&steps, features, lakes, void_gen, the_void)
.map(str::to_owned)
.collect::<Vec<_>>()
};
let all_but_structures = ["f0", "f1", "f2", "f5", "f6", "f7", "f8", "f9", "f10"];
assert_eq!(kept(true, false, false, false), all_but_structures);
// The lava lakes take the LAKES step's place.
assert_eq!(kept(true, true, false, false)[1..], all_but_structures[2..]);
// An all-air world is decorated only in the_void.
assert!(kept(true, false, true, false).is_empty());
assert_eq!(kept(true, false, true, true), all_but_structures);
assert!(kept(false, false, false, true).is_empty());
}
#[test]
fn generator_settings_read_as_flat_level_generator_settings() {
// server.properties' default: the preset's own layers, and the codec's error.