mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-11 07:11:30 +11:00
Implement the block change packets
This commit is contained in:
parent
09b434f298
commit
69ba704352
|
@ -330,6 +330,11 @@ pub fn build() -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn from_raw_unchecked(id: u16) -> Self {
|
||||
debug_assert!(Self::from_raw(id).is_some());
|
||||
Self(id)
|
||||
}
|
||||
|
||||
/// Converts this block state to its underlying raw block state ID.
|
||||
///
|
||||
/// The original block state can be recovered with [`BlockState::from_raw`].
|
||||
|
@ -337,6 +342,11 @@ pub fn build() -> anyhow::Result<()> {
|
|||
self.0
|
||||
}
|
||||
|
||||
/// Returns the maximum block state ID.
|
||||
pub const fn max_raw() -> u16 {
|
||||
#max_block_state
|
||||
}
|
||||
|
||||
/// Gets the value of the property with the given name from this block.
|
||||
///
|
||||
/// If this block does not have the property, then `None` is returned.
|
||||
|
|
|
@ -938,7 +938,7 @@ const ENTITIES: &[Class] = &[
|
|||
Field {
|
||||
name: "global_position",
|
||||
typ: Type::OptGlobalPosition,
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
Class {
|
||||
|
@ -1232,7 +1232,7 @@ const ENTITIES: &[Class] = &[
|
|||
Field {
|
||||
name: "tongue_target",
|
||||
typ: Type::VarInt(0),
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
Class {
|
||||
|
@ -1399,7 +1399,7 @@ const ENTITIES: &[Class] = &[
|
|||
Field {
|
||||
name: "right_horn",
|
||||
typ: Type::Bool(true),
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
Class {
|
||||
|
|
|
@ -201,7 +201,7 @@ fn terrain_column(
|
|||
2.0,
|
||||
0.5,
|
||||
) * 6.0)
|
||||
.floor() as i64;
|
||||
.floor() as i64;
|
||||
|
||||
if *in_terrain {
|
||||
if *depth > 0 {
|
||||
|
|
|
@ -53,12 +53,24 @@ impl From<(i32, i32, i32)> for BlockPos {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<BlockPos> for (i32, i32, i32) {
|
||||
fn from(pos: BlockPos) -> Self {
|
||||
(pos.x, pos.y, pos.z)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[i32; 3]> for BlockPos {
|
||||
fn from([x, y, z]: [i32; 3]) -> Self {
|
||||
BlockPos::new(x, y, z)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockPos> for [i32; 3] {
|
||||
fn from(pos: BlockPos) -> Self {
|
||||
[pos.x, pos.y, pos.z]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
294
src/chunk.rs
294
src/chunk.rs
|
@ -15,20 +15,21 @@ use crate::packets::play::s2c::{
|
|||
};
|
||||
use crate::protocol::{Encode, Nbt};
|
||||
use crate::var_int::VarInt;
|
||||
use crate::{BiomeId, Server, Ticks};
|
||||
use crate::var_long::VarLong;
|
||||
use crate::{BiomeId, BlockPos, DimensionId, Server, Ticks};
|
||||
|
||||
pub struct Chunks {
|
||||
chunks: HashMap<ChunkPos, Chunk>,
|
||||
server: Server,
|
||||
section_count: u32,
|
||||
dimension: DimensionId,
|
||||
}
|
||||
|
||||
impl Chunks {
|
||||
pub(crate) fn new(server: Server, section_count: u32) -> Self {
|
||||
pub(crate) fn new(server: Server, dimension: DimensionId) -> Self {
|
||||
Self {
|
||||
chunks: HashMap::new(),
|
||||
server,
|
||||
section_count,
|
||||
dimension,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,8 +37,8 @@ impl Chunks {
|
|||
self.chunks.len()
|
||||
}
|
||||
|
||||
pub fn get(&self, pos: ChunkPos) -> Option<&Chunk> {
|
||||
self.chunks.get(&pos)
|
||||
pub fn get(&self, pos: impl Into<ChunkPos>) -> Option<&Chunk> {
|
||||
self.chunks.get(&pos.into())
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
|
@ -51,6 +52,27 @@ impl Chunks {
|
|||
pub fn par_iter(&self) -> impl ParallelIterator<Item = (ChunkPos, &Chunk)> + Clone + '_ {
|
||||
self.chunks.par_iter().map(|(&pos, chunk)| (pos, chunk))
|
||||
}
|
||||
|
||||
pub fn get_block_state(&self, pos: impl Into<BlockPos>) -> Option<BlockState> {
|
||||
let pos = pos.into();
|
||||
let chunk_pos = ChunkPos::new(pos.x / 16, pos.z / 16);
|
||||
|
||||
let chunk = self.get(chunk_pos)?;
|
||||
|
||||
let min_y = self.server.dimension(self.dimension).min_y;
|
||||
|
||||
let y = pos.y.checked_sub(min_y)?.try_into().ok()?;
|
||||
|
||||
if y < chunk.height() {
|
||||
Some(chunk.get_block_state(
|
||||
pos.x.rem_euclid(16) as usize,
|
||||
y,
|
||||
pos.z.rem_euclid(16) as usize,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ChunksMut<'a> {
|
||||
|
@ -59,7 +81,8 @@ impl<'a> ChunksMut<'a> {
|
|||
}
|
||||
|
||||
pub fn create(&mut self, pos: impl Into<ChunkPos>) -> bool {
|
||||
let chunk = Chunk::new(self.section_count, self.server.current_tick());
|
||||
let section_count = (self.server.dimension(self.dimension).height / 16) as u32;
|
||||
let chunk = Chunk::new(section_count, self.server.current_tick());
|
||||
self.0.chunks.insert(pos.into(), chunk).is_none()
|
||||
}
|
||||
|
||||
|
@ -67,8 +90,8 @@ impl<'a> ChunksMut<'a> {
|
|||
self.0.chunks.remove(&pos).is_some()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, pos: ChunkPos) -> Option<ChunkMut> {
|
||||
self.0.chunks.get_mut(&pos).map(ChunkMut)
|
||||
pub fn get_mut(&mut self, pos: impl Into<ChunkPos>) -> Option<ChunkMut> {
|
||||
self.0.chunks.get_mut(&pos.into()).map(ChunkMut)
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl FusedIterator<Item = (ChunkPos, ChunkMut)> + '_ {
|
||||
|
@ -84,6 +107,29 @@ impl<'a> ChunksMut<'a> {
|
|||
.par_iter_mut()
|
||||
.map(|(&pos, chunk)| (pos, ChunkMut(chunk)))
|
||||
}
|
||||
|
||||
pub fn set_block_state(&mut self, pos: impl Into<BlockPos>, block: BlockState) -> bool {
|
||||
let pos = pos.into();
|
||||
let chunk_pos = ChunkPos::new(pos.x / 16, pos.z / 16);
|
||||
|
||||
if let Some(chunk) = self.0.chunks.get_mut(&chunk_pos) {
|
||||
let min_y = self.0.server.dimension(self.0.dimension).min_y;
|
||||
|
||||
if let Some(y) = pos.y.checked_sub(min_y).and_then(|y| y.try_into().ok()) {
|
||||
if y < chunk.height() {
|
||||
ChunkMut(chunk).set_block_state(
|
||||
pos.x.rem_euclid(16) as usize,
|
||||
y,
|
||||
pos.z.rem_euclid(16) as usize,
|
||||
block,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChunksMut<'a>(&'a mut Chunks);
|
||||
|
@ -101,23 +147,21 @@ pub struct Chunk {
|
|||
// TODO block_entities: HashMap<u32, BlockEntity>,
|
||||
/// The MOTION_BLOCKING heightmap
|
||||
heightmap: Vec<i64>,
|
||||
modified: bool,
|
||||
created_tick: Ticks,
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
pub(crate) fn new(section_count: u32, current_tick: Ticks) -> Self {
|
||||
let sect = ChunkSection {
|
||||
blocks: [BlockState::default(); 4096],
|
||||
blocks: [BlockState::AIR.to_raw(); 4096],
|
||||
modified_count: 1, // Must be >0 so the chunk is initialized.
|
||||
biomes: [BiomeId::default(); 64],
|
||||
compact_data: Vec::new(),
|
||||
modified: true,
|
||||
};
|
||||
|
||||
let mut chunk = Self {
|
||||
sections: vec![sect; section_count as usize].into(),
|
||||
heightmap: Vec::new(),
|
||||
modified: true,
|
||||
created_tick: current_tick,
|
||||
};
|
||||
|
||||
|
@ -135,7 +179,9 @@ impl Chunk {
|
|||
|
||||
pub fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState {
|
||||
if x < 16 && y < self.height() && z < 16 {
|
||||
self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16]
|
||||
BlockState::from_raw_unchecked(
|
||||
self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] & BLOCK_STATE_MASK,
|
||||
)
|
||||
} else {
|
||||
BlockState::AIR
|
||||
}
|
||||
|
@ -178,14 +224,60 @@ impl Chunk {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets the unapplied changes to this chunk as a block change packet.
|
||||
pub(crate) fn block_change_packet(&self, pos: ChunkPos) -> Option<BlockChangePacket> {
|
||||
if !self.modified {
|
||||
return None;
|
||||
}
|
||||
/// Returns unapplied changes to this chunk as block change packets through
|
||||
/// the provided closure.
|
||||
pub(crate) fn block_change_packets(
|
||||
&self,
|
||||
pos: ChunkPos,
|
||||
min_y: i32,
|
||||
mut packet: impl FnMut(BlockChangePacket),
|
||||
) {
|
||||
for (sect_y, sect) in self.sections.iter().enumerate() {
|
||||
if sect.modified_count == 1 {
|
||||
let (idx, &block) = sect
|
||||
.blocks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|&(_, &b)| b & !BLOCK_STATE_MASK != 0)
|
||||
.expect("invalid modified count");
|
||||
|
||||
// TODO
|
||||
None
|
||||
let global_x = pos.x * 16 + (idx % 16) as i32;
|
||||
let global_y = sect_y as i32 * 16 + (idx / (16 * 16)) as i32 + min_y;
|
||||
let global_z = pos.z * 16 + (idx / 16 % 16) as i32;
|
||||
|
||||
packet(BlockChangePacket::Single(BlockChange {
|
||||
location: BlockPos::new(global_x, global_y, global_z),
|
||||
block_id: VarInt((block & BLOCK_STATE_MASK).into()),
|
||||
}));
|
||||
} else if sect.modified_count > 1 {
|
||||
let mut blocks = Vec::new();
|
||||
for y in 0..16 {
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
let block =
|
||||
sect.blocks[x as usize + z as usize * 16 + y as usize * 16 * 16];
|
||||
|
||||
if block & !BLOCK_STATE_MASK != 0 {
|
||||
blocks.push(VarLong(
|
||||
((block & BLOCK_STATE_MASK) as i64) << 12
|
||||
| (x << 8 | z << 4 | y),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let chunk_section_position = (pos.x as i64) << 42
|
||||
| (pos.z as i64 & 0x3fffff) << 20
|
||||
| (sect_y as i64) & 0xfffff;
|
||||
|
||||
packet(BlockChangePacket::Multi(MultiBlockChange {
|
||||
chunk_section_position,
|
||||
invert_trust_edges: false,
|
||||
blocks,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,62 +293,79 @@ impl<'a> Deref for ChunkMut<'a> {
|
|||
|
||||
impl<'a> ChunkMut<'a> {
|
||||
pub fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) {
|
||||
if x < 16 && y < self.height() && z < 16 {
|
||||
let sec = &mut self.0.sections[y / 16];
|
||||
let idx = x + z * 16 + y % 16 * 16 * 16;
|
||||
if block != sec.blocks[idx] {
|
||||
sec.blocks[idx] = block;
|
||||
// TODO: set the modified bit.
|
||||
sec.modified = true;
|
||||
self.0.modified = true;
|
||||
// TODO: update block entity if b could have block entity data.
|
||||
assert!(
|
||||
x < 16 && y < self.height() && z < 16,
|
||||
"the chunk block coordinates must be within bounds"
|
||||
);
|
||||
|
||||
let sect = &mut self.0.sections[y / 16];
|
||||
let idx = x + z * 16 + y % 16 * 16 * 16;
|
||||
|
||||
if block.to_raw() != sect.blocks[idx] & BLOCK_STATE_MASK {
|
||||
if sect.blocks[idx] & !BLOCK_STATE_MASK == 0 {
|
||||
sect.modified_count += 1;
|
||||
}
|
||||
sect.blocks[idx] = block.to_raw() | !BLOCK_STATE_MASK;
|
||||
|
||||
// TODO: if the block type was modified and the old block type
|
||||
// could be a block entity, then the block entity at this
|
||||
// position must be cleared.
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_biome(&mut self, x: usize, y: usize, z: usize, b: BiomeId) {
|
||||
if x < 4 && y < self.height() / 4 && z < 4 {
|
||||
self.0.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b;
|
||||
}
|
||||
assert!(
|
||||
x < 4 && y < self.height() / 4 && z < 4,
|
||||
"the chunk biome coordinates must be within bounds"
|
||||
);
|
||||
|
||||
self.0.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b;
|
||||
}
|
||||
|
||||
pub(crate) fn apply_modifications(&mut self) {
|
||||
if self.modified {
|
||||
self.0.modified = false;
|
||||
let mut any_modified = false;
|
||||
|
||||
for sect in self.0.sections.iter_mut() {
|
||||
if sect.modified {
|
||||
sect.modified = false;
|
||||
for sect in self.0.sections.iter_mut() {
|
||||
if sect.modified_count > 0 {
|
||||
sect.modified_count = 0;
|
||||
any_modified = true;
|
||||
|
||||
sect.compact_data.clear();
|
||||
sect.compact_data.clear();
|
||||
|
||||
let non_air_block_count = sect.blocks.iter().filter(|&&b| !b.is_air()).count();
|
||||
let mut non_air_block_count: i16 = 0;
|
||||
|
||||
(non_air_block_count as i16)
|
||||
.encode(&mut sect.compact_data)
|
||||
.unwrap();
|
||||
|
||||
encode_paletted_container(
|
||||
sect.blocks.iter().map(|b| b.to_raw()),
|
||||
4,
|
||||
9,
|
||||
15,
|
||||
&mut sect.compact_data,
|
||||
)
|
||||
.unwrap();
|
||||
// TODO: The direct bits per idx changes depending on the number of biomes in
|
||||
// the biome registry.
|
||||
encode_paletted_container(
|
||||
sect.biomes.iter().map(|b| b.0),
|
||||
0,
|
||||
4,
|
||||
6,
|
||||
&mut sect.compact_data,
|
||||
)
|
||||
.unwrap();
|
||||
for b in &mut sect.blocks {
|
||||
*b &= BLOCK_STATE_MASK;
|
||||
if !BlockState::from_raw_unchecked(*b).is_air() {
|
||||
non_air_block_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
non_air_block_count.encode(&mut sect.compact_data).unwrap();
|
||||
|
||||
encode_paletted_container(
|
||||
sect.blocks.iter().cloned(),
|
||||
4,
|
||||
9,
|
||||
log2_ceil((BlockState::max_raw() + 1) as usize),
|
||||
&mut sect.compact_data,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// TODO: The direct bits per idx changes depending on the number of biomes in
|
||||
// the biome registry.
|
||||
encode_paletted_container(
|
||||
sect.biomes.iter().map(|b| b.0),
|
||||
0,
|
||||
4,
|
||||
6,
|
||||
&mut sect.compact_data,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if any_modified {
|
||||
build_heightmap(&self.0.sections, &mut self.0.heightmap);
|
||||
}
|
||||
}
|
||||
|
@ -323,14 +432,25 @@ impl Into<[i32; 2]> for ChunkPos {
|
|||
/// A 16x16x16 section of blocks, biomes, and light in a chunk.
|
||||
#[derive(Clone)]
|
||||
struct ChunkSection {
|
||||
/// The blocks in this section, stored in x, z, y order.
|
||||
blocks: [BlockState; 4096],
|
||||
/// The block states in this section, stored in x, z, y order.
|
||||
/// The most significant bit is used to indicate if this block has been
|
||||
/// modified.
|
||||
blocks: [u16; 4096],
|
||||
/// The number of modified blocks
|
||||
modified_count: u16,
|
||||
biomes: [BiomeId; 64],
|
||||
compact_data: Vec<u8>,
|
||||
/// If the blocks or biomes were modified.
|
||||
modified: bool,
|
||||
}
|
||||
|
||||
const BLOCK_STATE_MASK: u16 = 0x7fff;
|
||||
|
||||
const _: () = assert!(
|
||||
BlockState::max_raw() <= BLOCK_STATE_MASK,
|
||||
"There is not enough space in the block state type to store the modified bit. A bit array \
|
||||
separate from the block state array should be created to keep track of modified blocks in \
|
||||
the chunk section."
|
||||
);
|
||||
|
||||
/// Builds the MOTION_BLOCKING heightmap.
|
||||
fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec<i64>) {
|
||||
let height = sections.len() * 16;
|
||||
|
@ -344,7 +464,10 @@ fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec<i64>) {
|
|||
for x in 0..16 {
|
||||
for z in 0..16 {
|
||||
for y in (0..height).rev() {
|
||||
let block = sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16];
|
||||
let block = BlockState::from_raw_unchecked(
|
||||
sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] & BLOCK_STATE_MASK,
|
||||
);
|
||||
|
||||
// TODO: is_solid || is_fluid heuristic for motion blocking.
|
||||
if !block.is_air() {
|
||||
let column_height = y as u64;
|
||||
|
@ -438,3 +561,38 @@ fn encode_paletted_container(
|
|||
fn log2_ceil(n: usize) -> usize {
|
||||
n.next_power_of_two().trailing_zeros() as usize
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn set_get() {
|
||||
let mut chunk = Chunk::new(16, 0);
|
||||
let mut chunk = ChunkMut(&mut chunk);
|
||||
|
||||
chunk.set_block_state(1, 2, 3, BlockState::CAKE);
|
||||
assert_eq!(chunk.get_block_state(1, 2, 3), BlockState::CAKE);
|
||||
|
||||
chunk.set_biome(1, 2, 3, BiomeId(7));
|
||||
assert_eq!(chunk.get_biome(1, 2, 3), BiomeId(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn block_state_oob() {
|
||||
let mut chunk = Chunk::new(16, 0);
|
||||
let mut chunk = ChunkMut(&mut chunk);
|
||||
|
||||
chunk.set_block_state(16, 0, 0, BlockState::CAKE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn biome_oob() {
|
||||
let mut chunk = Chunk::new(16, 0);
|
||||
let mut chunk = ChunkMut(&mut chunk);
|
||||
|
||||
chunk.set_biome(4, 0, 0, BiomeId(0));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -485,6 +485,8 @@ impl<'a> ClientMut<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
let dimension = server.dimension(meta.dimension());
|
||||
|
||||
// Update existing chunks and unload those outside the view distance. Chunks
|
||||
// that have been overwritten also need to be unloaded.
|
||||
self.0.loaded_chunks.retain(|&pos| {
|
||||
|
@ -496,9 +498,9 @@ impl<'a> ClientMut<'a> {
|
|||
if is_chunk_in_view_distance(center, pos, view_dist + cache)
|
||||
&& chunk.created_tick() != current_tick
|
||||
{
|
||||
if let Some(pkt) = chunk.block_change_packet(pos) {
|
||||
send_packet(&mut self.0.send, pkt);
|
||||
}
|
||||
chunk.block_change_packets(pos, dimension.min_y, |pkt| {
|
||||
send_packet(&mut self.0.send, pkt)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -518,9 +520,7 @@ impl<'a> ClientMut<'a> {
|
|||
if let Some(chunk) = chunks.get(pos) {
|
||||
if self.0.loaded_chunks.insert(pos) {
|
||||
self.send_packet(chunk.chunk_data_packet(pos));
|
||||
if let Some(pkt) = chunk.block_change_packet(pos) {
|
||||
self.send_packet(pkt);
|
||||
}
|
||||
chunk.block_change_packets(pos, dimension.min_y, |pkt| self.send_packet(pkt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1028,9 +1028,9 @@ pub mod play {
|
|||
|
||||
def_struct! {
|
||||
MultiBlockChange 0x3d {
|
||||
chunk_section_position: u64,
|
||||
chunk_section_position: i64,
|
||||
invert_trust_edges: bool,
|
||||
blocks: Vec<u64>,
|
||||
blocks: Vec<VarLong>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use byteorder::{ReadBytesExt, WriteBytesExt};
|
|||
use crate::protocol::{Decode, Encode};
|
||||
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub struct VarLong(i64);
|
||||
pub struct VarLong(pub(crate) i64);
|
||||
|
||||
impl VarLong {
|
||||
/// The maximum number of bytes a `VarLong` can occupy when read from and
|
||||
|
|
|
@ -62,10 +62,7 @@ impl<'a> WorldsMut<'a> {
|
|||
clients: Clients::new(),
|
||||
entities: Entities::new(),
|
||||
spatial_index: SpatialIndex::new(),
|
||||
chunks: Chunks::new(
|
||||
self.server.clone(),
|
||||
(self.server.dimension(dim).height / 16) as u32,
|
||||
),
|
||||
chunks: Chunks::new(self.server.clone(), dim),
|
||||
meta: WorldMeta {
|
||||
dimension: dim,
|
||||
is_flat: false,
|
||||
|
|
Loading…
Reference in a new issue