mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
1 parent
dbf445db2d
commit
8c1f50f616
2 files changed
+205
-3
No files matched your search
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user