Merge pull request #3485 from ThatCakeIsAPie/feat/level-shape-6

🤖 Agent: verified on main + this head. I merged main in (through the carried #3484); save.rs keeps main's mob imports. Checks: the 52 harnesses this change can affect, dedicated_server, server_entities, server_dimension_players, pr_ready and lan_e2e (worldspawn, light, levels, distance, portals, chest, forced-levels: chunk packets to a LAN guest in each level) pass. The changes are per chunk, not per block. Queued to merge; #3486 follows.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-10-30 10:21:17 +01:00
commit e84a47c069
4 files changed
+88 -39

No files matched your search

+19 -11
View File
@@ -4,7 +4,7 @@
//! order only inserts the result (`ClientPacketListener.handleLevelChunkWithLight`).
use super::Game;
use horizon_client::{
chunk::{BIOME_STRATEGY, Chunk, Light, MIN_Y, SECTIONS, Section, block_strategy},
chunk::{BIOME_STRATEGY, Chunk, ColumnLayout, Light, Section, block_strategy},
streaming::Streaming,
};
use horizon_core::paletted::{PalettedContainer, Strategy};
@@ -24,6 +24,8 @@ use std::sync::{
/// with an older one is decoded again.
#[derive(Default)]
pub(super) struct ChunkFormat {
/// The column decoded chunks hold: the client's for the level.
column: ColumnLayout,
min_y: i32,
sections: usize,
has_skylight: bool,
@@ -34,6 +36,7 @@ pub(super) struct ChunkFormat {
impl ChunkFormat {
pub(super) fn new(dimension: &DimensionType, biome_map: Vec<u16>, biome_bits: u8) -> Self {
Self {
column: ColumnLayout::VANILLA,
min_y: dimension.min_y,
sections: dimension.sections(),
has_skylight: dimension.has_skylight,
@@ -42,17 +45,21 @@ impl ChunkFormat {
}
}
pub(super) fn with_dimension(&self, dimension: &DimensionType) -> Self {
Self::new(dimension, self.biome_map.clone(), self.biome_bits)
Self {
column: self.column,
..Self::new(dimension, self.biome_map.clone(), self.biome_bits)
}
}
/// LevelChunkSection data into the client's fixed -64..320 column.
/// LevelChunkSection data into the client's column.
fn column(&self, p: &play::LevelChunkWithLight) -> Result<Chunk, String> {
let sections = p
.chunk
.sections(self.sections, 16, self.biome_bits)
.map_err(|e| e.to_string())?;
let mut chunk = Chunk::empty(p.x, p.z);
let offset = (self.min_y - MIN_Y).div_euclid(16);
let mut chunk = Chunk::with_layout(p.x, p.z, self.column);
let offset = (self.min_y - self.column.min_y()).div_euclid(16);
let column = self.column.sections() as i32;
// Biome table index per quart cell of the dimension's own sections.
let mut biome_cells: Vec<[u32; 64]> = Vec::with_capacity(sections.len());
for (i, s) in sections.iter().enumerate() {
@@ -67,7 +74,7 @@ impl ChunkFormat {
);
}
biome_cells.push(cells);
if !(0..SECTIONS as i32).contains(&target) {
if !(0..column).contains(&target) {
continue;
}
// A global section is as wide as the server's registry: 16 bits
@@ -107,7 +114,7 @@ impl ChunkFormat {
// (the column's top or bottom cell layer repeated).
if !biome_cells.is_empty() {
let last = biome_cells.len() - 1;
for si in 0..SECTIONS as i32 {
for si in 0..column {
let i = si - offset;
if (0..sections.len() as i32).contains(&i) {
continue;
@@ -133,16 +140,17 @@ impl ChunkFormat {
/// above the column's top section (from the light section just above
/// it, else open sky).
pub(super) fn light(&self, light: &LightData) -> (Vec<SectionLight>, [u8; 256]) {
let mut layers: Vec<SectionLight> = vec![(None, None); SECTIONS];
let column = self.column.sections() as i32;
let mut layers: Vec<SectionLight> = vec![(None, None); self.column.sections()];
let sky = self.has_skylight;
let mut above = [if sky { 15 } else { 0 }; 256];
// Light section b is the dimension's section b - 1.
let first = (self.min_y - MIN_Y).div_euclid(16) - 1;
let first = (self.min_y - self.column.min_y()).div_euclid(16) - 1;
for (b, layer) in light.layers(self.sections + 2).into_iter().enumerate() {
let si = first + b as i32;
if (0..SECTIONS as i32).contains(&si) {
if (0..column).contains(&si) {
layers[si as usize] = layer;
} else if si == SECTIONS as i32
} else if si == column
&& sky
&& let Some(l) = &layer.0
{
+51 -20
View File
@@ -2,7 +2,7 @@
//! (sections with their paletted containers, block entities and the sky and
//! block DataLayers) and the section block-change batches.
use crate::{
chunk::{Chunk, Light, MIN_Y, SECTIONS, Section},
chunk::{Chunk, ColumnLayout, LevelShape, Light, Section},
containers::BlockEntity,
fast_hash::FxHashMap,
mobs::spawner::{SpawnDataExt, SpawnerExt},
@@ -107,6 +107,8 @@ pub struct ChunkEncoder {
/// The level's sections among a column's: all of them in the Overworld,
/// y 0 to 255 in the Nether and the End (`DimensionType` min_y and height).
sections: std::ops::Range<usize>,
/// The column of the level's chunks, which `sections` counts in.
column: ColumnLayout,
/// Whether the level has sky light (`DimensionType.hasSkyLight`).
sky: bool,
}
@@ -306,8 +308,9 @@ const WARM_MIN: usize = 16;
/// varying light, its layers).
struct Warm {
coder: Arc<SectionCoder>,
/// The heightmaps' tables and the level's (min_y, height).
heightmaps: Option<(Arc<crate::save::SaveTables>, i32, i32)>,
/// The heightmaps' tables, the column's min_y and the level's (min_y,
/// height).
heightmaps: Option<(Arc<crate::save::SaveTables>, i32, i32, i32)>,
columns: Vec<((i32, i32), Vec<Arc<Section>>)>,
heights: Vec<OnceLock<ColumnHeightmaps>>,
sections: Vec<Arc<Section>>,
@@ -321,8 +324,14 @@ impl Warm {
loop {
let i = self.next.fetch_add(1, Ordering::Relaxed);
if let Some((_, sections)) = self.columns.get(i) {
if let Some((tables, min_y, height)) = &self.heightmaps {
let maps = crate::save::client_heightmaps_in(sections, tables, *min_y, *height);
if let Some((tables, column_min_y, min_y, height)) = &self.heightmaps {
let maps = crate::save::client_heightmaps_in(
sections,
*column_min_y,
tables,
*min_y,
*height,
);
let _ = self.heights[i].set(Arc::new(maps));
}
continue;
@@ -438,7 +447,8 @@ impl ChunkEncoder {
tables: None,
cache: Default::default(),
heights: Default::default(),
sections: 0..SECTIONS,
sections: 0..ColumnLayout::VANILLA.sections(),
column: ColumnLayout::VANILLA,
sky: true,
}
}
@@ -453,6 +463,7 @@ impl ChunkEncoder {
let (min_y, height) = self.level();
let maps = Arc::new(crate::save::client_heightmaps_in(
&chunk.sections,
chunk.min_y(),
tables,
min_y,
height,
@@ -464,7 +475,7 @@ impl ChunkEncoder {
/// The level's min_y and height (`DimensionType`), the heightmaps' range.
fn level(&self) -> (i32, i32) {
(
MIN_Y + 16 * self.sections.start as i32,
self.column.section_y(self.sections.start),
16 * self.sections.len() as i32,
)
}
@@ -520,15 +531,15 @@ impl ChunkEncoder {
self.cached(section, |e| e.wire = Some(wire.clone()));
wire
}
/// Encode for a level of `min_y` and `height` (multiples of 16 within
/// the column), with or without sky light.
pub fn set_dimension(&mut self, min_y: i32, height: i32, sky: bool) {
let lo = usize::try_from((min_y - MIN_Y) >> 4)
.unwrap_or(0)
.min(SECTIONS);
let n = usize::try_from(height >> 4).unwrap_or(SECTIONS);
self.sections = lo..(lo + n).min(SECTIONS);
self.sky = sky;
/// Encode for a level of `shape`: its chunks hold `shape.column()`, of
/// which the level's own sections go on the wire.
pub fn set_dimension(&mut self, shape: LevelShape) {
let column = shape.column();
let lo = usize::try_from((shape.min_y - column.min_y()) >> 4).unwrap_or(0);
let n = usize::try_from(shape.height >> 4).unwrap_or(0);
self.column = column;
self.sections = lo.min(column.sections())..(lo + n).min(column.sections());
self.sky = shape.sky_light;
// The heightmaps depend on the level's height.
self.heights.get_mut().unwrap().clear();
}
@@ -649,7 +660,10 @@ impl ChunkEncoder {
let (min_y, height) = self.level();
Some(Warm {
coder: self.coder.clone(),
heightmaps: self.tables.clone().map(|t| (t, min_y, height)),
heightmaps: self
.tables
.clone()
.map(|t| (t, self.column.min_y(), min_y, height)),
heights: stale.iter().map(|_| OnceLock::new()).collect(),
columns: stale,
done: sections.iter().map(|_| OnceLock::new()).collect(),
@@ -816,7 +830,7 @@ impl ChunkEncoder {
i32::from(index >> 8),
i32::from(index >> 4 & 15),
);
let wy = MIN_Y + (i as i32) * 16 + y;
let wy = chunk.min_y() + (i as i32) * 16 + y;
let pos = (chunk.x * 16 + x, wy, chunk.z * 16 + z);
entity_infos.push(BlockEntityInfo {
packed_xz: ((x << 4) | z) as i8,
@@ -1152,6 +1166,7 @@ fn write_light(lights: &[(usize, SectionLight)], sky: bool, w: &mut Writer) {
#[cfg(test)]
mod tests {
use super::*;
use crate::chunk::{MIN_Y, SECTIONS};
#[test]
fn block_entity_blocks_are_recognised() {
@@ -1396,7 +1411,7 @@ mod tests {
"around {around:?}"
);
}
encoder.set_dimension(0, 256, false);
encoder.set_dimension(LevelShape::NETHER);
assert_eq!(
encoder.body(&chunk, &entities, SectionMask::ALL),
packet(&encoder, SectionMask::ALL)
@@ -1495,7 +1510,7 @@ mod tests {
&[],
);
let overworld = encoder.encode(&chunk, &HashMap::new());
encoder.set_dimension(0, 256, false);
encoder.set_dimension(LevelShape::NETHER);
let nether = encoder.encode(&chunk, &HashMap::new());
// An all-air section is the same bytes wherever it is.
let per_section = overworld.chunk.buffer.len() / SECTIONS;
@@ -1511,4 +1526,20 @@ mod tests {
18
));
}
#[test]
fn a_tall_column_sends_its_whole_height() {
let shape = LevelShape::new(-64, 2096, 2096, true).unwrap();
let mut chunk = Chunk::with_layout(0, 0, shape.column());
chunk.set(3, 2000, 4, 1);
let mut encoder = encoder();
let vanilla = encoder.encode(&Chunk::empty(0, 0), &HashMap::new());
encoder.set_dimension(shape);
let tall = encoder.encode(&chunk, &HashMap::new());
// 131 sections, one of them holding stone instead of air: longer than
// 131 air sections, and the sky light layers reach one above the top.
let per_section = vanilla.chunk.buffer.len() / SECTIONS;
assert!(tall.chunk.buffer.len() > per_section * 131);
assert_eq!(tall.light.sky.len(), 132);
}
}
+12 -3
View File
@@ -358,7 +358,7 @@ impl Hub {
let clock_ids = (0..sync.dynamic.len("minecraft:world_clock") as i32).collect();
let mut encoder = encoder;
let d = horizon_protocol::registry::DimensionType::vanilla(&config.dimension);
encoder.set_dimension(d.min_y, d.height, d.has_skylight);
encoder.set_dimension(level_shape(&d));
Self {
lan,
config,
@@ -2752,8 +2752,7 @@ impl Hub {
.id("minecraft:dimension_type", dimension)
.unwrap_or(0);
let d = horizon_protocol::registry::DimensionType::vanilla(dimension);
self.encoder
.set_dimension(d.min_y, d.height, d.has_skylight);
self.encoder.set_dimension(level_shape(&d));
}
/// ServerPlayer.teleport(TeleportTransition) into the level the hub now
@@ -2943,6 +2942,16 @@ fn shared_flags(sneaking: bool, sprinting: bool, body: &BodyFlags) -> i8 {
shared
}
/// The shape a level of the dimension type `d` has.
fn level_shape(d: &horizon_protocol::registry::DimensionType) -> crate::chunk::LevelShape {
crate::chunk::LevelShape {
sky_light: d.has_skylight,
min_y: d.min_y,
height: d.height,
logical_height: d.height,
}
}
#[cfg(test)]
mod tests {
use super::*;
+6 -5
View File
@@ -17,7 +17,7 @@ use crate::{
Result,
beehive::BeehiveExt,
biome::Biomes,
chunk::{BIOME_STRATEGY, Chunk, ColumnLayout, Light, MIN_Y, Section, block_strategy},
chunk::{BIOME_STRATEGY, Chunk, ColumnLayout, Light, Section, block_strategy},
mobs::{spawner::SpawnerExt, trial_spawner::TrialSpawnerExt},
streaming::ChunkGenerator,
};
@@ -1018,10 +1018,11 @@ fn heightmaps(chunk: &Chunk, tables: &SaveTables, shape: LevelShape) -> Tag {
)
}))
}
/// The client heightmaps of a column's sections, in a level from `min_y`,
/// `height` blocks high.
/// The client heightmaps of a column's sections (from `column_min_y`), in a
/// level from `min_y`, `height` blocks high.
pub fn client_heightmaps_in(
sections: &[Arc<Section>],
column_min_y: i32,
tables: &SaveTables,
min_y: i32,
height: i32,
@@ -1037,7 +1038,7 @@ pub fn client_heightmaps_in(
.collect();
let view = HeightmapView {
sections,
column_min_y: MIN_Y,
column_min_y,
tables,
min_y,
height,
@@ -5009,7 +5010,7 @@ mod tests {
assert_eq!(back.components, banner.components);
}
use super::*;
use crate::chunk::SECTIONS;
use crate::chunk::{MIN_Y, SECTIONS};
#[test]
fn end_gateway_block_entity_round_trips() {