mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
4 files changed
+88
-39
No files matched your search
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user