diff --git a/minecraft-entities-derive/examples/main.rs b/minecraft-entities-derive/examples/main.rs index c20ef6f4..d42f5e76 100644 --- a/minecraft-entities-derive/examples/main.rs +++ b/minecraft-entities-derive/examples/main.rs @@ -17,6 +17,7 @@ enum AnyEntity { Cow(Cow), } +#[derive(Debug)] pub struct Handler { uuid: Eid, world: Arc>, diff --git a/minecraft-positions/src/lib.rs b/minecraft-positions/src/lib.rs index e67cc314..08a147e4 100644 --- a/minecraft-positions/src/lib.rs +++ b/minecraft-positions/src/lib.rs @@ -1,5 +1,7 @@ mod shards; +use std::ops::AddAssign; + pub use minecraft_protocol::packets::Position as NetworkPosition; #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] @@ -183,4 +185,179 @@ impl ChunkColumnPosition { cz: self.cz, } } + + pub fn get_circle_from_center(&self, radius: i32) -> Vec { + let mut chunks = Vec::new(); + for x in 0..=radius { + for z in 0..=radius { + if x * x + z * z <= radius * radius { + chunks.push(ChunkColumnPosition { + cx: self.cx + x, + cz: self.cz + z, + }); + chunks.push(ChunkColumnPosition { + cx: self.cx + x, + cz: self.cz - z, + }); + chunks.push(ChunkColumnPosition { + cx: self.cx - x, + cz: self.cz + z, + }); + chunks.push(ChunkColumnPosition { + cx: self.cx - x, + cz: self.cz - z, + }); + } + } + } + chunks + } +} + +#[derive(Debug, Clone)] +pub struct LightPositionInChunkColumn { + pub bx: u8, + pub y: usize, + pub bz: u8, +} + +impl LightPositionInChunkColumn { + pub fn in_chunk(&self) -> BlockPositionInChunk { + BlockPositionInChunk { + bx: self.bx, + by: self.y.rem_euclid(16) as u8, + bz: self.bz, + } + } +} + + +impl From for LightPositionInChunkColumn { + fn from(val: BlockPositionInChunkColumn) -> Self { + Self { + bx: val.bx, + y: (val.y + 64 + 16) as usize, // TODO: Use the world config + bz: val.bz, + } + } +} + +#[derive(Debug, Clone)] +pub struct LightPosition { + pub x: i32, + pub y: usize, + pub z: i32, +} + +impl From for LightPositionInChunkColumn { + fn from(val: LightPosition) -> Self { + LightPositionInChunkColumn { + bx: val.x.rem_euclid(16) as u8, + y: val.y, + bz: val.z.rem_euclid(16) as u8, + } + } +} + +impl From for ChunkColumnPosition { + fn from(val: LightPosition) -> Self { + ChunkColumnPosition { + cx: val.x.div_euclid(16), + cz: val.z.div_euclid(16), + } + } +} + +impl From for LightPosition { + fn from(val: BlockPosition) -> Self { + Self { + x: val.x, + y: (val.y + 64 + 16) as usize, + z: val.z, + } + } +} + +impl From for BlockPosition { + fn from(val: LightPosition) -> Self { + Self { + x: val.x, + y: val.y as i32 - 64 - 16, + z: val.z + } + } +} + +impl LightPosition { + pub fn in_chunk(&self) -> BlockPositionInChunk { + BlockPositionInChunk { + bx: self.x.rem_euclid(16) as u8, + by: self.y.rem_euclid(16) as u8, + bz: self.z.rem_euclid(16) as u8, + } + } + + pub fn get_neighbors(&self, n_chunk: usize) -> Vec { + let mut neighbors = Vec::new(); + if self.y < ((n_chunk - 1) * 16) + 1 { // No block can be higher so no block can affect the light level + neighbors.push(LightPosition { x: self.x, y: self.y + 1, z: self.z }); + } + neighbors.push(LightPosition { x: self.x - 1, y: self.y, z: self.z }); + neighbors.push(LightPosition { x: self.x + 1, y: self.y, z: self.z }); + neighbors.push(LightPosition { x: self.x, y: self.y, z: self.z - 1 }); + neighbors.push(LightPosition { x: self.x, y: self.y, z: self.z + 1 }); + if self.y > 0 { + neighbors.push(LightPosition { x: self.x, y: self.y - 1, z: self.z }); + } + neighbors + } +} + +impl PartialEq for LightPosition { + fn eq(&self, other: &Self) -> bool { + self.y == other.y + } +} + +impl From for BlockPositionInChunkColumn { + fn from(val: LightPosition) -> Self { + BlockPositionInChunkColumn { + bx: val.x.rem_euclid(16) as u8, + y: val.y as i32 - 64 - 16, // TODO: Use the world config + bz: val.x.rem_euclid(16) as u8, + } + } +} + +impl AddAssign for LightPosition { + fn add_assign(&mut self, rhs: usize) { + self.y += rhs; + } +} + +impl std::cmp::Eq for LightPosition {} + +impl std::cmp::PartialOrd for LightPosition { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.y.cmp(&other.y)) + } +} + +impl std::cmp::Ord for LightPosition { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.y.cmp(&other.y) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_light_positions_conversions() { + let light_position = LightPosition { x: 1, y: 1, z: 1 }; + assert_eq!(light_position, LightPosition::from(BlockPosition::from(light_position.clone()))); + + let block_position = BlockPosition { x: 1, y: 1, z: 1 }; + assert_eq!(block_position, BlockPosition::from(LightPosition::from(block_position.clone()))); + } } diff --git a/minecraft-positions/src/shards.rs b/minecraft-positions/src/shards.rs index 3d042bb2..4ef0989d 100644 --- a/minecraft-positions/src/shards.rs +++ b/minecraft-positions/src/shards.rs @@ -2,6 +2,9 @@ use crate::*; impl ChunkColumnPosition { pub fn shard(&self, shard_count: usize) -> usize { - (self.cx + self.cz).unsigned_abs() as usize % shard_count + const REGION_SIZE: i32 = 8; + let region_x = self.cx.div_euclid(REGION_SIZE); + let region_z = self.cz.div_euclid(REGION_SIZE); + (region_x + region_z).unsigned_abs() as usize % shard_count } } diff --git a/minecraft-protocol/build/blocks.rs b/minecraft-protocol/build/blocks.rs index 5e559976..b738fb9a 100644 --- a/minecraft-protocol/build/blocks.rs +++ b/minecraft-protocol/build/blocks.rs @@ -146,9 +146,7 @@ pub fn generate_block_enum(data: serde_json::Value) { raw_harvest_tools.push( block .harvest_tools - .clone() - .into_iter() - .map(|(k, _v)| k) + .clone().into_keys() .collect(), ); let mut material = block @@ -427,7 +425,7 @@ const AIR_BLOCKS: [bool; {max_value}] = {air_blocks:?}; default_state_ids = blocks.iter().map(|b| b.default_state).collect::>(), item_ids = blocks .iter() - .map(|b| b.drops.get(0).copied().unwrap_or(0)) + .map(|b| b.drops.first().copied().unwrap_or(0)) .collect::>(), materials = materials, resistances = blocks.iter().map(|b| b.resistance).collect::>(), diff --git a/minecraft-protocol/build/items.rs b/minecraft-protocol/build/items.rs index b78bef0c..515f418d 100644 --- a/minecraft-protocol/build/items.rs +++ b/minecraft-protocol/build/items.rs @@ -60,6 +60,13 @@ pub enum Item {{ {variants} }} +impl Default for Item {{ + #[inline] + fn default() -> Self {{ + Item::Air + }} +}} + impl Item {{ #[inline] pub fn from_id(id: u32) -> Option {{ diff --git a/minecraft-protocol/src/components/chunk.rs b/minecraft-protocol/src/components/chunk.rs index 843e9d76..827313d3 100644 --- a/minecraft-protocol/src/components/chunk.rs +++ b/minecraft-protocol/src/components/chunk.rs @@ -1,4 +1,4 @@ -use crate::{nbt::NbtTag, *, components::blocks::BlockEntity}; +use crate::{nbt::NbtTag, *, components::blocks::BlockEntity, packets::serializer::BitSet}; /// A complex data structure including block data and optionally entities of a chunk. /// @@ -24,19 +24,19 @@ pub struct ChunkData<'a> { /// BitSet containing bits for each section in the world + 2. /// Each set bit indicates that the corresponding 16×16×16 chunk section has data in the Sky Light array below. /// The least significant bit is for blocks 16 blocks to 1 block below the min world height (one section below the world), while the most significant bit covers blocks 1 to 16 blocks above the max world height (one section above the world). - pub sky_light_mask: Array<'a, u64, VarInt>, + pub sky_light_mask: BitSet<'a>, /// BitSet containing bits for each section in the world + 2. /// Each set bit indicates that the corresponding 16×16×16 chunk section has data in the Block Light array below. /// The order of bits is the same as in Sky Light Mask. - pub block_light_mask: Array<'a, u64, VarInt>, + pub block_light_mask: BitSet<'a>, /// BitSet containing bits for each section in the world + 2. /// Each set bit indicates that the corresponding 16×16×16 chunk section has data in the Block Light array below. /// The order of bits is the same as in Sky Light Mask. - pub empty_sky_light_mask: Array<'a, u64, VarInt>, + pub empty_sky_light_mask: BitSet<'a>, /// BitSet containing bits for each section in the world + 2. /// Each set bit indicates that the corresponding 16×16×16 chunk section has data in the Block Light array below. /// The order of bits is the same as in Sky Light Mask. - pub empty_block_light_mask: Array<'a, u64, VarInt>, + pub empty_block_light_mask: BitSet<'a>, /// Length should match the number of bits set in Sky Light Mask. /// Each entry is an array of 2048 bytes. /// There is 1 array for each bit set to true in the sky light mask, starting with the lowest value. Half a byte per light value. Indexed ((y<<8) | (z<<4) | x) / 2 @@ -221,6 +221,7 @@ impl Chunk { Ok(chunks) } + #[cfg_attr(feature = "tracing", instrument(skip(output)))] pub fn into_data(chunks: Vec) -> Result, &'static str> { let mut output = Vec::new(); diff --git a/minecraft-protocol/src/nbt/mod.rs b/minecraft-protocol/src/nbt/mod.rs index 1bad2873..d4d6005c 100644 --- a/minecraft-protocol/src/nbt/mod.rs +++ b/minecraft-protocol/src/nbt/mod.rs @@ -7,8 +7,9 @@ use arrays::*; use compound::*; use numbers::*; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub enum NbtTag { + #[default] Null, Byte(i8), Short(i16), diff --git a/minecraft-protocol/src/packets/play_clientbound.rs b/minecraft-protocol/src/packets/play_clientbound.rs index c39d2c1d..0af87f5e 100644 --- a/minecraft-protocol/src/packets/play_clientbound.rs +++ b/minecraft-protocol/src/packets/play_clientbound.rs @@ -443,8 +443,20 @@ pub enum ClientboundPacket<'a> { /// Updates light levels for a chunk UpdateLight { - /// TODO: parse this - data: RawBytes<'a>, + /// Chunk coordinate (block coordinate divided by 16, rounded down) + cx: VarInt, + /// Chunk coordinate (block coordinate divided by 16, rounded down) + cz: VarInt, + /// BitSet containing bits for each section in the world + 2. Each set bit indicates that the corresponding 16×16×16 chunk section has data in the Sky Light array below. The least significant bit is for blocks 16 blocks to 1 block below the min world height (one section below the world), while the most significant bit covers blocks 1 to 16 blocks above the max world height (one section above the world). + sky_light_mask: BitSet<'a>, + /// BitSet containing bits for each section in the world + 2. Each set bit indicates that the corresponding 16×16×16 chunk section has data in the Block Light array below. The order of bits is the same as in Sky Light Mask. + block_light_mask: BitSet<'a>, + /// BitSet containing bits for each section in the world + 2. Each set bit indicates that the corresponding 16×16×16 chunk section has all zeros for its Sky Light data. The order of bits is the same as in Sky Light Mask. + empty_sky_light_mask: BitSet<'a>, + /// BitSet containing bits for each section in the world + 2. Each set bit indicates that the corresponding 16×16×16 chunk section has all zeros for its Block Light data. The order of bits is the same as in Sky Light Mask. + empty_block_light_mask: BitSet<'a>, + sky_light_arrays: Array<'a, Array<'a, u8, VarInt>, VarInt>, + block_light_arrays: Array<'a, Array<'a, u8, VarInt>, VarInt>, }, /// See [Protocol Encryption](https://wiki.vg/Protocol_Encryption) for information on logging in. diff --git a/minecraft-server/Cargo.toml b/minecraft-server/Cargo.toml index 2c37ee16..fa7cdd6e 100644 --- a/minecraft-server/Cargo.toml +++ b/minecraft-server/Cargo.toml @@ -7,10 +7,18 @@ edition = "2021" [dependencies] env_logger = "0.10.0" -log = "0.4.20" tokio = { version = "1.33.0", features = ["full"] } futures = "0.3.29" -minecraft-protocol = { path="../minecraft-protocol" } +minecraft-protocol = { path="../minecraft-protocol"} minecraft-positions = { path="../minecraft-positions" } minecraft-entities-derive = { path="../minecraft-entities-derive" } rand = "0.8.4" +tracy-client = { version = "0.16.4", features = ["enable"], optional = true} +tracing-tracy = { version = "0.10.4", features = ["enable", "system-tracing"], optional = true} +tracing-subscriber = "0.3.18" +log = "0.4.17" +tracing = { version = "0.1", features = ["attributes"] } + +[features] +default = [] +trace = ["tracy-client", "tracing-tracy" ] diff --git a/minecraft-server/src/entities/entity.rs b/minecraft-server/src/entities/entity.rs index 59da82ac..606a1cbd 100644 --- a/minecraft-server/src/entities/entity.rs +++ b/minecraft-server/src/entities/entity.rs @@ -7,6 +7,8 @@ use super::*; init(self, server_msg_rcvr: BroadcastReceiver); } )] + +#[derive(Debug)] pub struct Entity { pub position: Position, pub velocity: Translation, diff --git a/minecraft-server/src/entities/monsters/zombies.rs b/minecraft-server/src/entities/monsters/zombies.rs index 65e8ad17..a50735bd 100644 --- a/minecraft-server/src/entities/monsters/zombies.rs +++ b/minecraft-server/src/entities/monsters/zombies.rs @@ -1,5 +1,3 @@ -use minecraft_protocol::network; - use super::*; #[derive(Default)] diff --git a/minecraft-server/src/entities/player.rs b/minecraft-server/src/entities/player.rs index acf209b8..8e1426b8 100644 --- a/minecraft-server/src/entities/player.rs +++ b/minecraft-server/src/entities/player.rs @@ -87,6 +87,7 @@ impl Player { } impl Handler { + #[instrument(skip_all)] async fn update_center_chunk(self) { let Some((old_center_chunk, new_center_chunk, render_distance)) = self.mutate(|player| { let old_center_chunk = player.center_chunk.clone(); @@ -96,71 +97,48 @@ impl Handler { }).await else {return}; // Tell the client which chunk he is in - if new_center_chunk == old_center_chunk { return }; - self.send_packet(PlayClientbound::SetCenterChunk { chunk_x: VarInt(new_center_chunk.cx), chunk_z: VarInt(new_center_chunk.cz) }).await; + // Maybe the server didn't send all the chunks yet so we have to check if all chunks are loaded + if new_center_chunk != old_center_chunk { + self.send_packet(PlayClientbound::SetCenterChunk { chunk_x: VarInt(new_center_chunk.cx), chunk_z: VarInt(new_center_chunk.cz) }).await; + }; - // Find out which chunks should be loaded - if new_center_chunk.chunk_column() == old_center_chunk.chunk_column() { return }; - let mut loaded_chunks_after = HashSet::new(); - for cx in (new_center_chunk.cx - render_distance)..=(new_center_chunk.cx + render_distance) { - for cz in (new_center_chunk.cz - render_distance)..=(new_center_chunk.cz + render_distance) { - let dist = (((cx - new_center_chunk.cx).pow(2) + (cz - new_center_chunk.cz).pow(2)) as f32).sqrt(); - if dist > render_distance as f32 { continue }; - loaded_chunks_after.insert(ChunkColumnPosition { cx, cz }); - } - } + let mut loaded_chunks_after_ordered = new_center_chunk.chunk_column().get_circle_from_center(render_distance); + loaded_chunks_after_ordered.sort_by_key(|chunk| (chunk.cx - new_center_chunk.cx).abs() + (chunk.cz - new_center_chunk.cz).abs()); + let loaded_chunks_after: HashSet<_> = loaded_chunks_after_ordered.iter().collect(); + // TODO: Load n chunks per tick according to the server's TPS + const CHUNK_PER_REQUEST: usize = 5; - // Select chunks to load (max 50) and unload let Some((loaded_chunks_after, newly_loaded_chunks, unloaded_chunks, uuid)) = self.mutate(|player| { - if loaded_chunks_after == player.loaded_chunks { return (None, EntityChanges::nothing()) }; - let mut newly_loaded_chunks: Vec<_> = loaded_chunks_after.difference(&player.loaded_chunks).cloned().collect(); - let unloaded_chunks: Vec<_> = player.loaded_chunks.difference(&loaded_chunks_after).cloned().collect(); - for skipped in newly_loaded_chunks.iter().skip(50) { - loaded_chunks_after.remove(skipped); - } - newly_loaded_chunks.truncate(50); + let newly_loaded_chunks: Vec<_> = loaded_chunks_after_ordered.iter() + .filter(|chunk| !player.loaded_chunks.contains(chunk)) + .take(CHUNK_PER_REQUEST).cloned() + .collect(); + + let unloaded_chunks: Vec<_> = player.loaded_chunks.iter() + .filter(|chunk| !loaded_chunks_after.contains(chunk)) + .cloned() + .collect(); + + // loaded_chunks_after = loaded_chunks_before - unloaded_chunks + newly_loaded_chunks + let loaded_chunks_after: HashSet<_> = player.loaded_chunks.difference(&unloaded_chunks.iter().cloned().collect()).chain(newly_loaded_chunks.iter()).cloned().collect(); + player.loaded_chunks = loaded_chunks_after.clone(); + let uuid = player.info.uuid; + player.loaded_chunks = loaded_chunks_after.clone(); (Some((loaded_chunks_after, newly_loaded_chunks, unloaded_chunks, uuid)), EntityChanges::other()) }).await.flatten() else { return }; + - // Tell the world about the changes - self.world.update_loaded_chunks(uuid, loaded_chunks_after).await; + // TODO: optimize, don't wait to load all chunks before sending them to the client + self.world.ensure_loaded_chunks(uuid, loaded_chunks_after).await; // Send the chunks to the client - let mut heightmaps = HashMap::new(); - heightmaps.insert(String::from("MOTION_BLOCKING"), NbtTag::LongArray(vec![0; 37])); - let heightmaps = NbtTag::Compound(heightmaps); for newly_loaded_chunk in newly_loaded_chunks { - let mut column = Vec::new(); - for cy in -4..20 { - let chunk = self.world.get_network_chunk(newly_loaded_chunk.chunk(cy)).await.unwrap_or_else(|| { - error!("Chunk not loaded: {newly_loaded_chunk:?}"); - NetworkChunk { // TODO hard error - block_count: 0, - blocks: PalettedData::Single { value: 0 }, - biomes: PalettedData::Single { value: 4 }, - } - }); - column.push(chunk); - } - let serialized: Vec = NetworkChunk::into_data(column).unwrap(); - let chunk_data = PlayClientbound::ChunkData { - value: ChunkData { - chunk_x: newly_loaded_chunk.cx, - chunk_z: newly_loaded_chunk.cz, - heightmaps: heightmaps.clone(), - data: Array::from(serialized.clone()), - block_entities: Array::default(), - sky_light_mask: Array::default(), - block_light_mask: Array::default(), - empty_sky_light_mask: Array::default(), - empty_block_light_mask: Array::default(), - sky_light: Array::default(), - block_light: Array::default(), - } - }; - self.send_packet(chunk_data).await; + let chunk_column = self.world.get_network_chunk_column_data(newly_loaded_chunk.clone()).await.unwrap_or_else(|| { + panic!("Chunk not loaded: {:?}", newly_loaded_chunk); + }); + self.send_raw_packet(chunk_column).await; } // Tell the client to unload chunks @@ -172,6 +150,7 @@ impl Handler { } } + #[cfg_attr(feature = "tracing", instrument(skip_all))] async fn send_packet<'a>(&self, packet: PlayClientbound<'a>) { let packet = packet.serialize_minecraft_packet().unwrap(); let packets_sent = self.mutate(|player| { @@ -185,10 +164,29 @@ impl Handler { packet_sender.send(packet).await.unwrap(); } + #[cfg_attr(feature = "tracing", instrument(skip_all))] + async fn send_raw_packet(&self, packet: Vec) { + let packets_sent = self.mutate(|player| { + player.packets_sent += 1; + (player.packets_sent, EntityChanges::other()) + }).await.unwrap_or(0); + if packets_sent > 500 { + warn!("Many packets sent ({packets_sent})"); + } + let Some(packet_sender) = self.observe(|player| player.packet_sender.clone()).await else {return}; + packet_sender.send(packet).await.unwrap(); + } + + #[instrument(skip_all)] async fn on_server_message(self, message: ServerMessage) { use ServerMessage::*; match message { Tick(tick_id) => { + #[cfg(feature = "tracing")] { + let span = info_span!("player tick"); + let _enter: tracing::span::Entered<'_> = span.enter(); + } + if tick_id % (20*10) == 0 { self.send_packet(PlayClientbound::KeepAlive { keep_alive_id: tick_id as u64 }).await; } @@ -201,6 +199,7 @@ impl Handler { } } + #[instrument(skip_all)] async fn on_world_change(self, change: WorldChange) { match change { WorldChange::Block(position, block) => { @@ -266,7 +265,7 @@ impl Handler { pitch: (pitch * (256.0 / 360.0)) as u8, yaw: (yaw * (256.0 / 360.0)) as u8, head_yaw: (head_yaw * (256.0 / 360.0)) as u8, - data: VarInt(0 as i32), // TODO set data on entities + data: VarInt(0_i32), // TODO set data on entities velocity_x: (velocity.x * 8000.0) as i16, velocity_y: (velocity.y * 8000.0) as i16, velocity_z: (velocity.z * 8000.0) as i16, @@ -293,6 +292,7 @@ impl Handler { } } + #[instrument(skip_all)] async fn on_packet<'a>(mut self, packet: PlayServerbound<'a>) { use PlayServerbound::*; match packet { @@ -348,7 +348,7 @@ impl Handler { self.world.spawn_entity::(AnyEntity::Zombie(zombie)).await; } else if message == "stress" { tokio::spawn(async move { - for i in 0..1000 { + for _ in 0..1000 { let mut zombie = Zombie::default(); let Some(mut position) = self.observe(|player| player.get_entity().position.clone()).await else {return}; position.y += 20.0; @@ -361,12 +361,19 @@ impl Handler { } RequestPing { payload } => { self.send_packet(PlayClientbound::Ping { id: payload as i32 }).await; - } + }, + Pong { id } => { + self.send_packet(PlayClientbound::PingResponse { payload: id as i64 }).await; + }, + KeepAlive { keep_alive_id } => { + self.send_packet(PlayClientbound::KeepAlive { keep_alive_id }).await; + }, packet => warn!("Unsupported packet received: {packet:?}"), } } } +#[instrument(skip_all)] async fn handle_player(h: Handler, uuid: UUID, stream: TcpStream, packet_receiver: MpscReceiver>, server_msg_rcvr: BroadcastReceiver, change_receiver: MpscReceiver) { let r = handle_player_inner(h.clone(), stream, packet_receiver, server_msg_rcvr, change_receiver).await; match r { @@ -376,6 +383,7 @@ async fn handle_player(h: Handler, uuid: UUID, stream: TcpStream, packet h.world.remove_loader(uuid).await; } +#[instrument(skip_all)] async fn handle_player_inner(h: Handler, stream: TcpStream, mut packet_receiver: MpscReceiver>, mut server_msg_rcvr: BroadcastReceiver, mut change_receiver: MpscReceiver) -> Result<(), ()> { let (mut reader_stream, mut writer_stream) = stream.into_split(); diff --git a/minecraft-server/src/main.rs b/minecraft-server/src/main.rs index 9667ad48..5c3a3fc3 100644 --- a/minecraft-server/src/main.rs +++ b/minecraft-server/src/main.rs @@ -23,6 +23,22 @@ impl std::future::Future for ServerFuture { #[tokio::main] async fn main() { + #[cfg(feature = "trace")] + #[global_allocator] + static GLOBAL: tracy_client::ProfiledAllocator = + tracy_client::ProfiledAllocator::new(std::alloc::System, 100); + + use tracing_subscriber::{fmt, layer::SubscriberExt, Registry}; + + let subscriber = Registry::default() + .with(fmt::layer()); + #[cfg(feature = "trace")] + let subscriber = subscriber + .with(tracing_tracy::TracyLayer::new()); + + tracing::subscriber::set_global_default(subscriber) + .expect("setting up tracing"); + env_logger::init(); let server = ServerBehavior::init().await; diff --git a/minecraft-server/src/player_handler/connect.rs b/minecraft-server/src/player_handler/connect.rs index 87692566..39d23774 100644 --- a/minecraft-server/src/player_handler/connect.rs +++ b/minecraft-server/src/player_handler/connect.rs @@ -1,5 +1,7 @@ use super::*; + +#[cfg_attr(feature = "trace", instrument(skip_all))] pub async fn handle_connection( mut stream: TcpStream, addr: SocketAddr, diff --git a/minecraft-server/src/player_handler/handshake.rs b/minecraft-server/src/player_handler/handshake.rs index f8533e94..8c60243e 100644 --- a/minecraft-server/src/player_handler/handshake.rs +++ b/minecraft-server/src/player_handler/handshake.rs @@ -14,6 +14,8 @@ pub struct PlayerInfo { pub allow_server_listing: bool, } + +#[instrument(skip_all)] pub async fn handshake(stream: &mut TcpStream, logged_in_player_info: LoggedInPlayerInfo, world: &'static World) -> Result<(PlayerInfo, MpscReceiver), ()> { // Receive client informations let packet = receive_packet(stream).await?; @@ -313,60 +315,36 @@ pub async fn handshake(stream: &mut TcpStream, logged_in_player_info: LoggedInPl loaded_chunks.insert(ChunkColumnPosition { cx, cz }); } } - world.update_loaded_chunks(logged_in_player_info.uuid, loaded_chunks).await; - - let mut heightmaps = HashMap::new(); - heightmaps.insert(String::from("MOTION_BLOCKING"), NbtTag::LongArray(vec![0; 37])); - let heightmaps = NbtTag::Compound(heightmaps); + world.ensure_loaded_chunks(logged_in_player_info.uuid, loaded_chunks.clone()).await; for cx in -3..=3 { for cz in -3..=3 { - let mut column = Vec::new(); - for cy in -4..20 { - let chunk = world.get_network_chunk(ChunkPosition { cx, cy, cz }).await.unwrap_or_else(|| { - error!("Chunk not loaded: {cx} {cy} {cz}"); - NetworkChunk { // TODO hard error - block_count: 0, - blocks: PalettedData::Single { value: 0 }, - biomes: PalettedData::Single { value: 4 }, - } - }); - column.push(chunk); - } - let serialized: Vec = NetworkChunk::into_data(column).unwrap(); - let chunk_data = PlayClientbound::ChunkData { - value: ChunkData { - chunk_x: cx, - chunk_z: cz, - heightmaps: heightmaps.clone(), - data: Array::from(serialized.clone()), - block_entities: Array::default(), - sky_light_mask: Array::default(), - block_light_mask: Array::default(), - empty_sky_light_mask: Array::default(), - empty_block_light_mask: Array::default(), - sky_light: Array::default(), - block_light: Array::default(), - } - }; - send_packet(stream, chunk_data).await; + let chunk_column = world.get_network_chunk_column_data(ChunkColumnPosition { cx, cz }).await.unwrap_or_else(|| { + error!("Chunk not loaded: {cx} {cz}"); + panic!("Chunk not loaded: {cx} {cz}"); + }); + send_packet_raw(stream, chunk_column.as_slice()).await; } } debug!("ChunkData sent"); // Chunk batch end - let chunk_data = PlayClientbound::ChunkBatchFinished { batch_size: VarInt(49) }; + let chunk_data = PlayClientbound::ChunkBatchFinished { batch_size: VarInt(loaded_chunks.len() as i32) }; send_packet(stream, chunk_data).await; debug!("ChunkBatchFinished sent"); // Get chunk batch acknoledgement let packet = receive_packet(stream).await?; let packet = PlayServerbound::deserialize_uncompressed_minecraft_packet(packet.as_slice()).unwrap(); - let PlayServerbound::ChunkBatchReceived { chunks_per_tick } = packet else { + if let PlayServerbound::ChunkBatchReceived { chunks_per_tick } = packet { + debug!("ChunkBatchAcknoledgement received chunks per tick: {chunks_per_tick}"); + } else if let PlayServerbound::ConfirmTeleportation { teleport_id } = packet { + debug!("ConfirmTeleportation received {:?}", teleport_id); + } else { error!("Expected ChunkBatchAcknoledgement packet, got: {packet:?}"); return Err(()); - }; - debug!("ChunkBatchAcknoledgement received"); + + } Ok((PlayerInfo { addr: logged_in_player_info.addr, diff --git a/minecraft-server/src/player_handler/login.rs b/minecraft-server/src/player_handler/login.rs index 043ced41..60ca9ba1 100644 --- a/minecraft-server/src/player_handler/login.rs +++ b/minecraft-server/src/player_handler/login.rs @@ -6,6 +6,7 @@ pub struct LoggedInPlayerInfo { pub(super) uuid: u128, } +#[cfg_attr(feature = "trace", instrument(skip_all))] pub async fn login(stream: &mut TcpStream, addr: SocketAddr) -> Result { // Receive login start let packet = receive_packet(stream).await?; diff --git a/minecraft-server/src/player_handler/network.rs b/minecraft-server/src/player_handler/network.rs index 0b9e7227..d3a86317 100644 --- a/minecraft-server/src/player_handler/network.rs +++ b/minecraft-server/src/player_handler/network.rs @@ -48,6 +48,7 @@ pub async fn receive_packet_split(stream: &mut OwnedReadHalf) -> Result, Ok(data) } +#[cfg_attr(feature = "trace", instrument)] pub async fn send_packet_raw(stream: &mut TcpStream, packet: &[u8]) { let length = VarInt::from(packet.len()); stream.write_all(length.serialize_minecraft_packet().unwrap().as_slice()).await.unwrap(); @@ -55,6 +56,7 @@ pub async fn send_packet_raw(stream: &mut TcpStream, packet: &[u8]) { stream.flush().await.unwrap(); } +#[cfg_attr(feature = "trace", instrument)] pub async fn send_packet_raw_split(stream: &mut OwnedWriteHalf, packet: &[u8]) { let length = VarInt::from(packet.len()); stream.write_all(length.serialize_minecraft_packet().unwrap().as_slice()).await.unwrap(); @@ -62,6 +64,7 @@ pub async fn send_packet_raw_split(stream: &mut OwnedWriteHalf, packet: &[u8]) { stream.flush().await.unwrap(); } +#[cfg_attr(feature = "trace", instrument(skip_all))] pub async fn send_packet<'a, P: MinecraftPacketPart<'a>>(stream: &mut TcpStream, packet: P) { let packet = packet.serialize_minecraft_packet().unwrap(); send_packet_raw(stream, packet.as_slice()).await; diff --git a/minecraft-server/src/player_handler/status.rs b/minecraft-server/src/player_handler/status.rs index aa2f724c..7237089c 100644 --- a/minecraft-server/src/player_handler/status.rs +++ b/minecraft-server/src/player_handler/status.rs @@ -1,5 +1,6 @@ use super::*; +#[cfg_attr(feature = "trace", instrument(skip_all))] pub async fn status(stream: &mut TcpStream) -> Result<(), ()> { loop { let packet = receive_packet(stream).await?; diff --git a/minecraft-server/src/prelude.rs b/minecraft-server/src/prelude.rs index 67738215..46568f95 100644 --- a/minecraft-server/src/prelude.rs +++ b/minecraft-server/src/prelude.rs @@ -1,10 +1,10 @@ pub use crate::{entities::*, player_handler::*, server_behavior::*, world::*}; pub use futures::FutureExt; -pub use log::{debug, error, info, trace, warn}; +pub use tracing::{debug, error, info, trace, warn, instrument, info_span}; pub use minecraft_protocol::{ components::{ chat::ChatMode, - chunk::{Chunk as NetworkChunk, ChunkData, PalettedData}, + chunk::{Chunk as NetworkChunk, ChunkData as NetworkChunkColumnData, PalettedData}, difficulty::Difficulty, entity::{EntityAttribute, EntityMetadata, EntityMetadataValue}, gamemode::{Gamemode, PreviousGamemode}, diff --git a/minecraft-server/src/world/ecs.rs b/minecraft-server/src/world/ecs.rs index eb20c8b0..b07f217e 100644 --- a/minecraft-server/src/world/ecs.rs +++ b/minecraft-server/src/world/ecs.rs @@ -6,6 +6,7 @@ use tokio::sync::RwLock; pub type EntityTask = Pin + Send + Sync + 'static>>; pub type EntityTaskHandle = tokio::task::JoinHandle<()>; + pub struct Entities { eid_counter: std::sync::atomic::AtomicU32, uuid_counter: std::sync::atomic::AtomicU64, @@ -72,6 +73,7 @@ impl Entities { } } + #[cfg_attr(feature = "trace", instrument(skip_all))] pub(super) async fn spawn_entity(&self, entity: AnyEntity, world: &'static World, receiver: BroadcastReceiver) -> (Eid, UUID) where AnyEntity: TryAsEntityRef, Handler: EntityExt { @@ -90,7 +92,7 @@ impl Entities { h.init(receiver).await; (eid, uuid) } - + pub(super) async fn insert_entity_task(&self, eid: Eid, name: &'static str, handle: EntityTaskHandle) { let mut entity_tasks = self.entity_tasks.write().await; let old = entity_tasks.entry(eid).or_insert(HashMap::new()).insert(name, handle); @@ -100,6 +102,7 @@ impl Entities { } /// Remove an entity + #[cfg_attr(feature = "trace", instrument(skip_all))] pub(super) async fn remove_entity(&self, eid: Eid) -> Option { let entity = self.entities.write().await.remove(&eid); let mut chunks = self.chunks.write().await; diff --git a/minecraft-server/src/world/light.rs b/minecraft-server/src/world/light.rs new file mode 100644 index 00000000..c2a60f85 --- /dev/null +++ b/minecraft-server/src/world/light.rs @@ -0,0 +1,597 @@ +use std::collections::BinaryHeap; + +use minecraft_protocol::ids::blocks::Block; +use tokio::sync::OwnedRwLockWriteGuard; + +use crate::prelude::*; +use super::*; + +const MAX_LIGHT_LEVEL: u8 = 15; + +#[derive(Debug, Clone)] +struct SectionLightData(Vec); // TODO(optimization): Use simd + +impl SectionLightData { + pub fn new() -> SectionLightData { + SectionLightData(vec![0; 2048]) + } + + pub fn set_with(&mut self, level: u8) { + let level = level << 4 | level; + self.0.iter_mut().for_each(|v| *v = level); + + } + + + /// Get the light level at the given position. + pub fn get(&self, postion: BlockPositionInChunk) -> Result { + let (x, y, z) = (postion.bx as usize, postion.by as usize, postion.bz as usize); + let index = (y << 8) | (z << 4) | x; + let byte_index = index >> 1; + + if byte_index >= 2048 { + return Err(()); + } + + if index & 1 == 0 { + Ok(self.0[byte_index] & 0x0F) + } else { + Ok((self.0[byte_index] & 0xF0) >> 4) + } + } + + /// Set the light level at the given position. + pub fn set(&mut self, postion: BlockPositionInChunk, level: u8) -> Result<(), ()> { + if level > MAX_LIGHT_LEVEL { + return Err(()); + } + + let (x, y, z) = (postion.bx as usize, postion.by as usize, postion.bz as usize); + let index = (y << 8) | (z << 4) | x; + let byte_index = index >> 1; + + if byte_index >= 2048 { + return Err(()); + } + + if index & 1 == 0 { + self.0[byte_index] = (self.0[byte_index] & 0xF0) | (level & 0x0F); + } else { + self.0[byte_index] = (self.0[byte_index] & 0x0F) | ((level & 0x0F) << 4); + } + + Ok(()) + } + + /// Set the light level at the given layer to the given level. + pub(super) fn set_layer(&mut self, layer: u8 , level: u8) -> Result<(), ()> { + if level > MAX_LIGHT_LEVEL { + return Err(()); + } + + if layer > MAX_LIGHT_LEVEL { + return Err(()); + } + + let level = level << 4 | level; + let layer = layer as usize; + + // Because a layer is 16x16 blocks, we can just set 128 blocks at once and the y coordinate is the most significant bit of the index. + for i in layer*128..(layer+1)*128 { + self.0[i] = level; + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct LightSystem { + /// The level of the sky light, 15 is the maximum. + pub level: u8, + /// The sky light data for each section. + pub light_arrays: Vec, + /// The mask of sections that have sky light data. + pub light_mask: u64, + /// The mask of sections that don't have sky light data. + pub empty_light_mask: u64, +} + +impl LightSystem { + /// Get the light data as an array of arrays. + fn to_array<'a>(&self) -> Array<'a, Array<'a, u8, VarInt>, VarInt> { + let mut sections = Vec::new(); + for (i, section) in self.light_arrays.iter().enumerate() { + if self.light_mask & (1 << i) != 0 { + let mut data = Vec::new(); + for byte in section.0.iter() { + data.push(*byte); + } + sections.push(Array::from(data)); + } + } + Array::from(sections) + } + + /// Get the light mask and the empty light mask as bitsets. + /// return (light_mask, empty_light_mask) + fn masks_to_bitset<'a>(&self) -> (BitSet<'a>, BitSet<'a>) { + let light_mask = BitSet::from(vec![self.light_mask as i64]); + let empty_light_mask = BitSet::from(vec![self.empty_light_mask as i64]); + (light_mask, empty_light_mask) + } + + /// Set the section mask. + pub fn set_mask(&mut self, section: usize, has_light: bool) { + let mask = 1 << section; + if has_light { + self.light_mask |= mask; + self.empty_light_mask &= !mask; + } else { + self.light_mask &= !mask; + self.empty_light_mask |= mask; + } + } + + /// Update the light mask and the empty light mask. + pub fn update_masks(&mut self, section: usize) { + let mask = 1 << section; + if self.light_arrays[section].0.iter().any(|v| *v != 0) { + self.light_mask |= mask; + self.empty_light_mask &= !mask; + } else { + self.light_mask &= !mask; + self.empty_light_mask |= mask; + } + } + + /// Get the light data and the light mask and the empty light mask as bitsets. + /// return (light_data, light_mask, empty_light_mask) + pub fn get_packet_data<'a>(&self) -> (Array<'a, Array<'a, u8, VarInt>, VarInt>, BitSet<'a>, BitSet<'a>) { + let data = self.to_array(); + let (light_mask, empty_light_mask) = self.masks_to_bitset(); + (data, light_mask, empty_light_mask) + } + + /// Set the sky light in the given section. + pub fn set_region(&mut self, from_y: usize, to_y: usize, level: u8) -> Result<(), ()> { + if level > self.level { + return Err(()); + } + + // Get the range of sections to set. + let first_section = from_y.div_euclid(16); + let first_secion_offset = from_y.rem_euclid(16); + + let last_section = to_y.div_euclid(16); + let last_section_offset = to_y.rem_euclid(16); + + + for section in first_section..=last_section { + if section != first_section && section != last_section { + // Set the whole section + self.light_arrays[section].set_with(level); + self.set_mask(section, level > 0); + } else { + // Set the part of the section + let first_offset = if section == first_section { first_secion_offset } else { 0 }; + let last_offset = if section == last_section { last_section_offset } else { MAX_LIGHT_LEVEL as usize }; + for y in first_offset..=last_offset { + self.light_arrays[section].set_layer(y as u8, level)?; + } + + // Update the mask + self.update_masks(section); + } + } + + Ok(()) + } + + pub(super) fn get_level(&self, position: LightPositionInChunkColumn) -> Result { + let section = position.y.div_euclid(16); + self.light_arrays[section.max(0)].get(position.in_chunk()) + } + + pub(super) fn set_level(&mut self, position: LightPositionInChunkColumn, level: u8) -> Result<(), ()> { + let section = position.y.div_euclid(16); + // Update the mask + let mask = 1 << section; + if self.level > 0 { + self.empty_light_mask &= !mask; + self.light_mask |= mask; + } else { + // TODO: don't apply this if another block contains the light + self.empty_light_mask |= mask; + self.light_mask &= !mask; + } + self.light_arrays[section.max(0)].set(position.in_chunk(), level)?; + self.update_masks(section); + Ok(()) + } +} + +pub(super) struct Light { + sky_light: LightSystem, +} + +impl Light { + pub fn new() -> Self { + // TODO: Make this configurable with the world. + Self { + sky_light: LightSystem { + level: MAX_LIGHT_LEVEL, + light_arrays: vec![SectionLightData::new(); 24+2], + light_mask: !0, + empty_light_mask: 0, + }, + } + } + + pub fn get_packet(&self) -> (Array, VarInt>, BitSet, BitSet) { + self.sky_light.get_packet_data() + } + + pub fn get_skylight_level(&self, position: LightPositionInChunkColumn) -> u8 { + self.sky_light.get_level(position).unwrap_or_default() + } +} + +pub struct LightManager { + world_map: &'static WorldMap, + current_shard_id: Option, + current_shard: Option>>, +} + +impl LightManager { + pub fn new(world_map: &'static WorldMap) -> Self { + Self { + world_map, + current_shard: None, + current_shard_id: None, + } + } + + #[instrument(skip(world_map))] + pub async fn update_light(world_map: &'static WorldMap, block_position: BlockPosition, block: BlockWithState) { + let mut light_manager = Self::new(world_map); + let block = Block::from_state_id(block.block_id()).unwrap(); + + if block.is_transparent() { + + } else { + light_manager.set_block(block_position.clone(), block).await; + light_manager.set_light_level(LightPosition::from(block_position), 0).await; + } + + } + + async fn ensure_shard(&mut self, shard_id: usize) { + if let Some(current_shard_id) = self.current_shard_id { + if current_shard_id == shard_id { + return; + } + } + self.current_shard = Some(self.world_map.write_shard(shard_id).await); + self.current_shard_id = Some(shard_id); + } + + async fn get_chunk_column(&mut self, chunk_column_position: ChunkColumnPosition) -> Option<&mut ChunkColumn> { + let shard_id = chunk_column_position.shard(self.world_map.get_shard_count()); + + self.ensure_shard(shard_id).await; + + if let Some(shard) = &mut self.current_shard { + // Here, we use a reference to `shard` instead of trying to move it + shard.get_mut(&chunk_column_position) + } else { + unreachable!("ensure shard always sets to current_shard the requested shard") + } + } + + pub async fn set_light_level(&mut self, position: LightPosition, level: u8) { + let chunk_col_position = ChunkColumnPosition::from(position.clone()); + let shard_id = chunk_col_position.clone().shard(self.world_map.get_shard_count()); + self.ensure_shard(shard_id).await; + + if let Some(shard) = &mut self.current_shard { + // Here, we use a reference to `shard` instead of trying to move it + if let Some(col) = shard.get_mut(&chunk_col_position) { + if col.light.sky_light.set_level(LightPositionInChunkColumn::from(position), level).is_ok() { + } else { + error!("Chunk column found at {:?} in shard {} but light level not found", chunk_col_position, shard_id); + } + } else { + error!("Chunk column not found at {:?} in shard {}", chunk_col_position, shard_id); + } + + } else { + unreachable!("ensure shard always sets to current_shard the requested shard") + } + } + + pub async fn get_light_level(&mut self, position: LightPosition) -> u8 { + let chunk_col_position = ChunkColumnPosition::from(position.clone()); + let shard_id = chunk_col_position.clone().shard(self.world_map.get_shard_count()); + self.ensure_shard(shard_id).await; + + if let Some(shard) = &mut self.current_shard { + // Here, we use a reference to `shard` instead of trying to move it + if let Some(col) = shard.get_mut(&chunk_col_position) { + if let Ok(level) = col.light.sky_light.get_level(LightPositionInChunkColumn::from(position)) { + level + } else { + error!("Chunk column found at {:?} in shard {} but light level not found", chunk_col_position, shard_id); + 0 + } + } else { + error!("Chunk column not found at {:?} in shard {}", chunk_col_position, shard_id); + 0 + } + + } else { + unreachable!("ensure shard always sets to current_shard the requested shard") + } + } + + async fn get_block(&mut self, position: LightPosition) -> Block { + let chunk_col_position = ChunkColumnPosition::from(position.clone()); + let shard_id = chunk_col_position.clone().shard(self.world_map.get_shard_count()); + self.ensure_shard(shard_id).await; + + if let Some(shard) = &mut self.current_shard { + // Here, we use a reference to `shard` instead of trying to move it + if let Some(col) = shard.get_mut(&chunk_col_position) { + let block_position = BlockPositionInChunkColumn::from(position); + Block::from_id(col.get_block(block_position).block_id()).unwrap() + } else { + error!("Chunk column not found at {:?} in shard {}", chunk_col_position, shard_id); + Block::Air + } + + } else { + unreachable!("ensure shard always sets to current_shard the requested shard") + } + } + + // Light propagation when a block is placed + async fn set_block(&mut self, block_position: BlockPosition, block: Block) { + // Use only self.get_light_level() and self.set_light_level() to get and set light levels + // Use only self.get_block() to get the block at a position + // Don't use self.get_chunk_column() to get the chunk column, use self.ensure_shard() instead + + // Use the dijsktra algorithm to propagate the light + let mut heap = BinaryHeap::new(); + let mut visited = HashSet::new(); + heap.push(block_position.clone()); + + } + + + pub async fn init_chunk_column_light(world_map: &'static WorldMap, chunk_column_position: ChunkColumnPosition) { + + // Clear locked chubks + } +} + +impl ChunkColumn { + /// Init independant light means it will compute the light for all the chunk without considering the neighbour chunks. + pub(super) fn init_independant_light(&mut self) { + let _ = self.light.sky_light.set_region(self.get_highest_block() as usize + 1, ChunkColumn::MAX_HEIGHT as usize, self.light.sky_light.level); + + for x in 0..16 { + for z in 0..16 { + for y in self.get_highest_block_at(&BlockPositionInChunkColumn { + bx: x, + y: 0i32, + bz: z + })..(self.get_highest_block() as u16) { + let _ = self.light.sky_light.set_level( + LightPositionInChunkColumn { + bx: x, + y: y as usize, + bz: z + }, self.light.sky_light.level); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_section_light_data() { + let mut data = SectionLightData::new(); + + data.set(BlockPositionInChunk { bx: 0, by: 0, bz: 0 }, MAX_LIGHT_LEVEL).unwrap(); + assert_eq!(data.get(BlockPositionInChunk { bx: 0, by: 0, bz: 0 }).unwrap(), MAX_LIGHT_LEVEL); + + data.set(BlockPositionInChunk { bx: 0, by: 0, bz: 0 }, 0).unwrap(); + assert_eq!(data.get(BlockPositionInChunk { bx: 0, by: 0, bz: 0 }).unwrap(), 0); + + data.set(BlockPositionInChunk { bx: 0, by: 0, bz: 1 }, 1).unwrap(); + assert_eq!(data.get(BlockPositionInChunk { bx: 0, by: 0, bz: 1 }).unwrap(), 1); + + data.set(BlockPositionInChunk { bx: 0, by: 1, bz: 1 }, MAX_LIGHT_LEVEL).unwrap(); + assert_eq!(data.get(BlockPositionInChunk { bx: 0, by: 1, bz: 1 }).unwrap(), MAX_LIGHT_LEVEL); + + data.set(BlockPositionInChunk { bx: 1, by: 1, bz: 1 }, 1).unwrap(); + assert_eq!(data.get(BlockPositionInChunk { bx: 1, by: 1, bz: 1 }).unwrap(), 1); + + data.set(BlockPositionInChunk { bx: 2, by: 0, bz: 0 }, 1).unwrap(); + assert_eq!(data.get(BlockPositionInChunk { bx: 2, by: 0, bz: 0 }).unwrap(), 1); + + // Manual layer + for z in 0..16 { + for x in 0..16 { + data.set(BlockPositionInChunk { bx: x, by: 0, bz: z }, MAX_LIGHT_LEVEL).unwrap(); + } + } + + for z in 0..16 { + for x in 0..16 { + assert_eq!(data.get(BlockPositionInChunk { bx: x, by: 0, bz: z }).unwrap(), MAX_LIGHT_LEVEL, "x: {}, z: {}", x, z); + } + } + + // Test layer + data.set_layer(1, MAX_LIGHT_LEVEL).unwrap(); + for x in 0..16 { + for z in 0..16 { + assert_eq!(data.get(BlockPositionInChunk { bx: x, by: 1, bz: z }).unwrap(), MAX_LIGHT_LEVEL, "x: {}, z: {}", x, z); + } + } + } + + #[test] + fn test_set_region() { + let mut sky_light = LightSystem { + level: MAX_LIGHT_LEVEL, + light_arrays: vec![SectionLightData::new(); 16+2], + light_mask: 0, + empty_light_mask: !0, + }; + + sky_light.set_region(1, 33, MAX_LIGHT_LEVEL).unwrap(); + + // Test in + assert_eq!(sky_light.light_arrays[0].get(BlockPositionInChunk { bx: 0, by: 1, bz: 7 }).unwrap(), MAX_LIGHT_LEVEL); + assert_eq!(sky_light.light_arrays[1].get(BlockPositionInChunk { bx: 1, by: MAX_LIGHT_LEVEL, bz: 8 }).unwrap(), MAX_LIGHT_LEVEL); + assert_eq!(sky_light.light_arrays[2].get(BlockPositionInChunk { bx: 3, by: 0, bz: 0 }).unwrap(), MAX_LIGHT_LEVEL); + + // Test out + assert_eq!(sky_light.light_arrays[0].get(BlockPositionInChunk { bx: 4, by: 0, bz: 2 }).unwrap(), 0); + assert_eq!(sky_light.light_arrays[3].get(BlockPositionInChunk { bx: 0, by: 14, bz: 9 }).unwrap(), 0); + assert_eq!(sky_light.light_arrays[4].get(BlockPositionInChunk { bx: 9, by: 0, bz: 10 }).unwrap(), 0); + } + + #[test] + fn test_init_independant_light() { + let chunk_column = ChunkColumn::flat(); + + for x in 0..16 { + for z in 0..16 { + for y in 0..(chunk_column.get_highest_block() as u16) { + assert_eq!(chunk_column.light.sky_light.get_level(LightPositionInChunkColumn { bx: x, y: y as usize, bz: z }).unwrap(), 0); + } + for y in (chunk_column.get_highest_block() as u16 + 1)..255 { + assert_eq!(chunk_column.light.sky_light.get_level(LightPositionInChunkColumn { bx: x, y: y as usize, bz: z }).unwrap(), MAX_LIGHT_LEVEL); + } + } + } + } + + #[test] + fn test_get_light_level() { + let mut light_system = LightSystem { + level: 15, + light_arrays: vec![SectionLightData([0; 2048].to_vec()); 16], + light_mask: 0, + empty_light_mask: u64::MAX, + }; + + // Set light level at position (1, 2, 3) to 10 + let position = LightPositionInChunkColumn { bx: 1, y: 2, bz: 3 }; + light_system.set_level(position.clone(), 10).unwrap(); + + // Get light level at position (1, 2, 3) + let level = light_system.get_level(position).unwrap(); + assert_eq!(level, 10); + } + + #[test] + fn test_set_mask() { + let mut light_system = LightSystem { + level: 15, + light_arrays: vec![SectionLightData([0; 2048].to_vec()); 16], + light_mask: 0, + empty_light_mask: u64::MAX, + }; + + // Set mask for section 2 to true + light_system.set_mask(2, true); + + // Check if mask is set correctly + assert_eq!(light_system.light_mask, 4); + assert_eq!(light_system.empty_light_mask, u64::MAX - 4); + + // Set mask for section 2 to false + light_system.set_mask(2, false); + + // Check if mask is set correctly + assert_eq!(light_system.light_mask, 0); + assert_eq!(light_system.empty_light_mask, u64::MAX); + } + + #[test] + fn test_update_masks() { + let mut light_system = LightSystem { + level: 15, + light_arrays: vec![SectionLightData([0; 2048].to_vec()); 16], + light_mask: 0, + empty_light_mask: u64::MAX, + }; + + // Set light level at position (1, 2, 3) to 10 + let position = LightPositionInChunkColumn { bx: 1, y: 33, bz: 3 }; + light_system.set_level(position.clone(), 10).unwrap(); + + // Update masks for section 2 + light_system.update_masks(2); + + // Check if masks are updated correctly + assert_eq!(light_system.light_mask, 4); + assert_eq!(light_system.empty_light_mask, u64::MAX - 4); + + // Set light level at position (1, 2, 3) to 0 + light_system.set_level(position, 0).unwrap(); + + // Update masks for section 2 + light_system.update_masks(2); + + // Check if masks are updated correctly + assert_eq!(light_system.light_mask, 0); + assert_eq!(light_system.empty_light_mask, u64::MAX); + } + + #[test] + fn test_to_array() { + let light_system = LightSystem { + level: MAX_LIGHT_LEVEL, + light_arrays: vec![SectionLightData::new(); 24+2], + light_mask: !0, + empty_light_mask: 0, + }; + + // Convert light data to array + let light_array = light_system.to_array(); + + // Check if array is created correctly + assert_eq!(light_array.items.len(), 24+2); + assert_eq!(light_array.items[0].items.len(), 2048); + assert_eq!(light_array.items[1].items.len(), 2048); + assert_eq!(light_array.items[24].items[0], 0); + } + + #[test] + fn test_masks_to_bitset() { + let light_system = LightSystem { + level: 15, + light_arrays: vec![SectionLightData([0; 2048].to_vec()); 16], + light_mask: 5, + empty_light_mask: u64::MAX - 5, + }; + + // Convert masks to bitsets + let (light_mask, empty_light_mask) = light_system.masks_to_bitset(); + + // Check if bitsets are created correctly + assert_eq!(light_mask.items.len(), 1); + assert!(light_mask.items.contains(&5)); + assert_eq!(empty_light_mask.items.len(), 1); + assert!(empty_light_mask.items.contains(&((u64::MAX - 5) as i64))); + } +} diff --git a/minecraft-server/src/world/loading_manager.rs b/minecraft-server/src/world/loading_manager.rs index 3a5d2178..87953e59 100644 --- a/minecraft-server/src/world/loading_manager.rs +++ b/minecraft-server/src/world/loading_manager.rs @@ -1,12 +1,13 @@ use crate::prelude::*; -#[derive(Default)] +#[derive(Default, Debug)] pub(super) struct WorldLoadingManager { loaded_chunks: HashMap>, loader_entities: HashMap>, } impl WorldLoadingManager { + #[cfg_attr(feature = "trace", instrument(skip_all))] pub(super) fn update_loaded_chunks(&mut self, uuid: UUID, loaded_chunks: HashSet) { let loaded_before = self.loaded_chunks.entry(uuid).or_default(); for just_unloaded in loaded_before.difference(&loaded_chunks) { diff --git a/minecraft-server/src/world/map.rs b/minecraft-server/src/world/map.rs index 4c41c4dc..dd577c0c 100644 --- a/minecraft-server/src/world/map.rs +++ b/minecraft-server/src/world/map.rs @@ -1,7 +1,9 @@ -use std::collections::HashMap; -use minecraft_protocol::components::chunk::PalettedData; -use tokio::sync::RwLock; -use crate::prelude::*; +use std::{collections::HashMap, cmp::Ordering}; +use minecraft_protocol::{components::chunk::PalettedData, ids::blocks::Block}; +use tokio::sync::{RwLock, OwnedRwLockWriteGuard}; +use crate::{prelude::*, world::light::LightManager}; + +use super::light::Light; pub struct WorldMap { /// The map is divided in shards. @@ -9,11 +11,11 @@ pub struct WorldMap { /// The shards are locked independently. /// This allows high concurrency. shard_count: usize, - shards: Vec>>, + shards: Vec>>>, } #[derive(Clone)] -struct Chunk { +pub(super) struct Chunk { data: NetworkChunk, palette_block_counts: Vec, } @@ -52,7 +54,7 @@ impl Chunk { &self.data } - fn get_block(&self, position: BlockPositionInChunk) -> BlockWithState { + fn get_block(&self, position: &BlockPositionInChunk) -> BlockWithState { match &self.data.blocks { PalettedData::Paletted { palette, indexed } => { let data_position = position.by as usize * 16 * 16 + position.bz as usize * 16 + position.bx as usize; @@ -167,11 +169,197 @@ impl Chunk { } } -struct ChunkColumn { +struct HeightMap { + base: u8, + data: Vec, + max_height: Option, +} + +impl HeightMap { + pub fn new(base: u8) -> Self { + assert!(base <= 9, "base must be <= 9 because the max height is 320 + 64"); + Self { + base, + data: vec![0; ((16 * 16 * 9usize).div_euclid(base as usize) + 1) * base as usize ], + max_height: None + } + } + + pub fn to_tag(&self) -> NbtTag { + NbtTag::Compound( + HashMap::from_iter( + vec![ + (String::from("MOTION_BLOCKING"), NbtTag::LongArray(unsafe { + std::mem::transmute::, Vec>(self.data.clone()) + })), + ] + ) + ) + } + + /// Update the current base of the heightmap. + fn new_base(&mut self, new_base: u8) { + assert!(new_base <= 9, "base must be <= 9 because the max height is 320 + 64"); + + let old_base = self.base as usize; + + unimplemented!(); + + self.base = new_base as u8; + } + + + fn get_need_base(&self, height: u32) -> u8 { + 32 - ((height + 1).leading_zeros() as u8) + } + + /// Set the height of the highest block at the given position. + pub fn set(&mut self, position: &BlockPositionInChunkColumn, height: u32) { + let (x, z) = (position.bx, position.bz); + // Check if the height is higher than the current max height. + if let Some(max_height) = self.max_height { + if height < max_height { // Calculate the new base for the data. + let new_base = self.get_need_base(height); + // Update the base & max height. + self.max_height = Some(height); + } + } else { + // Set the max height. + self.max_height = Some(height); + } + + let index = (x * 16 + z) as usize; // assuming a 16x16 chunk column + let bits_per_entry = self.base as usize; + let bit_pos = index * bits_per_entry; + let data_index = bit_pos / 64; + let bit_offset = bit_pos % 64; + + // Ensure we don't shift beyond the limits of the data type. + if bits_per_entry >= 64 { + panic!("base too large for u64 storage"); + } + + // Cast the height to u64 + let height = height as u64; + + // Prepare the mask to clear the bits at the position. + let mask = ((1 << bits_per_entry) - 1) << bit_offset; + // Clear the bits at the target position. + self.data[data_index] &= !mask; + // Set the new height with the sign. + self.data[data_index] |= height << bit_offset; + // Check if the entry spills over to the next u64. + if bit_offset + bits_per_entry > 64 { + // Calculate how many bits spill over. + let spill_over_bits = bit_offset + bits_per_entry - 64; + // Prepare the mask to clear the spill over bits. + let spill_over_mask = (1 << spill_over_bits) - 1; + // Clear the spill over bits in the next entry. + self.data[data_index + 1] &= !spill_over_mask; + // Set the spill over bits. + self.data[data_index + 1] |= height >> (64 - bit_offset); + } + } + + /// Get the height of the highest block at the given position. + pub fn get(&self, position: &BlockPositionInChunkColumn) -> u16 { + let (x, z) = (position.bx, position.bz); + + let index = (x * 16 + z) as usize; // assuming a 16x16 chunk column + let bits_per_entry = self.base as usize; + let bit_pos = index * bits_per_entry; + let data_index = bit_pos / 64; + let bit_offset = bit_pos % 64; + + // Prepare the mask to get the bits at the position. + let mask = ((1u64 << bits_per_entry) - 1) << bit_offset; + // Retrieve the bits. + let mut value = (self.data[data_index] & mask) >> bit_offset; + + // Check if the entry spills over to the next u64 and retrieve the remaining bits. + if bit_offset + bits_per_entry > 64 { + // Calculate how many bits spill over. + let spill_over_bits = bit_offset + bits_per_entry - 64; + // Prepare the mask to get the spill over bits. + let spill_over_mask = (1u64 << spill_over_bits) - 1; + // Retrieve the spill over bits from the next entry. + value |= (self.data[data_index + 1] & spill_over_mask) << (64 - bit_offset); + } + + // Perform sign extension if the value is negative. + let sign_bit = 1u64 << (bits_per_entry - 1); + if value & sign_bit != 0 { + // If the sign bit is set, extend the sign to the rest of the i64. + value |= !((1u64 << bits_per_entry) - 1); + } + + // Cast to i32 with sign extension. + value as u16 + } +} + +pub(super) struct ChunkColumn { + pub(super) light: Light, + heightmap: HeightMap, chunks: Vec, } impl ChunkColumn { + pub const MAX_HEIGHT: u16 = 320 + 64; // TODO: adapt to the world height + pub const MIN_Y: i32 = -64; + + #[cfg_attr(feature = "trace", instrument(skip_all))] + fn init_chunk_heightmap(&mut self){ + if self.chunks.len() != 24 { + panic!("Chunk column must have 24 chunks (change it for other world heights)"); + } + + // Start from the higher chunk + for bx in 0..16 { + for bz in 0..16 { + let height = self.get_higher_skylight_filter_block(&BlockPositionInChunkColumn { bx, y: 0, bz }, Self::MAX_HEIGHT).into(); + self.heightmap.set(&BlockPositionInChunkColumn { bx, y: 0, bz }, height); + } + } + } + + fn get_higher_skylight_filter_block(&self, position: &BlockPositionInChunkColumn, current_height: u16) -> u16 { + let n_chunk_to_skip = self.chunks.len() - (current_height >> 4) as usize - ((current_height & 15) > 0) as usize; + let mut current_height = current_height - 1; + let mut block_position = BlockPositionInChunk { bx: position.bx, by: 0, bz: position.bz }; + // Downward propagation + for chunk in self.chunks.iter().rev().skip(n_chunk_to_skip) { + for by in (0..(((current_height & 15) + 1) as u8)).rev() { + block_position.by = by; + let block: BlockWithState = chunk.get_block(&block_position); + // SAFETY: fom_id will get a valid block necessarily + if !Block::from(block).is_transparent() { + return current_height + 1; + } + current_height = current_height.saturating_sub(1); + } + } + current_height + } + pub(super) fn get_highest_block(&self) -> u32 { + self.heightmap.max_height.unwrap_or(0) + } + + pub(super) fn get_highest_block_at(&self, position: &BlockPositionInChunkColumn) -> u16 { + self.heightmap.get(position) + } + + pub fn from(chunks: Vec) -> Self { + let mut column = Self { + chunks, + heightmap: HeightMap::new(8), + light: Light::new(), + }; + column.init_chunk_heightmap(); + column.init_independant_light(); + column + } + pub fn flat() -> Self { let empty_chunk = Chunk { data: NetworkChunk { @@ -194,30 +382,52 @@ impl ChunkColumn { for _ in 0..23 { chunks.push(empty_chunk.clone()); } - ChunkColumn { chunks } + Self::from(chunks) } - fn get_block(&self, position: BlockPositionInChunkColumn) -> BlockWithState { + pub(super) fn get_block(&self, position: BlockPositionInChunkColumn) -> BlockWithState { fn get_block_inner(s: &ChunkColumn, position: BlockPositionInChunkColumn) -> Option { let cy = position.cy(); let cy_in_vec: usize = cy.saturating_add(4).try_into().ok()?; let position = position.in_chunk(); let chunk = s.chunks.get(cy_in_vec)?; - Some(chunk.get_block(position)) + Some(chunk.get_block(&position)) } get_block_inner(self, position).unwrap_or(BlockWithState::Air) } + #[cfg(test)] + pub fn set_block_for_test(&mut self, position: BlockPositionInChunkColumn, block: BlockWithState) { + self.set_block(position, block); + } + fn set_block(&mut self, position: BlockPositionInChunkColumn, block: BlockWithState) { fn set_block_innter(s: &mut ChunkColumn, position: BlockPositionInChunkColumn, block: BlockWithState) -> Option<()> { let cy = position.cy(); let cy_in_vec: usize = cy.saturating_add(4).try_into().ok()?; let position = position.in_chunk(); let chunk = s.chunks.get_mut(cy_in_vec)?; - chunk.set_block(position, block); + chunk.set_block(position, block.clone()); Some(()) } - set_block_innter(self, position, block); + set_block_innter(self, position.clone(), block.clone()); + + let last_height = self.heightmap.get(&position); + let not_filter_sunlight = Block::from(block.clone()).is_transparent(); // TODO: check if the block is transparent + + // Get the height of the placed block + let block_height = (position.y - Self::MIN_Y + 1).max(0) as u16; + match block_height.cmp(&last_height) { + Ordering::Greater if !not_filter_sunlight => { + self.heightmap.set(&position, block_height.into()); + }, + Ordering::Equal if not_filter_sunlight => { + // Downward propagation + let new_height = self.get_higher_skylight_filter_block(&position, last_height).into(); + self.heightmap.set(&position, new_height); + }, + _ => {} + } } } @@ -225,9 +435,12 @@ impl WorldMap { pub fn new(shard_count: usize) -> WorldMap { let mut shards = Vec::new(); for _ in 0..shard_count { - shards.push(RwLock::new(HashMap::new())); + shards.push(Arc::new(RwLock::new(HashMap::new()))); } - WorldMap { shard_count, shards } + WorldMap { + shard_count, + shards + } } pub async fn get_block(&self, position: BlockPosition) -> BlockWithState { @@ -244,33 +457,25 @@ impl WorldMap { inner_get_block(self, position).await.unwrap_or(BlockWithState::Air) } - pub async fn get_network_chunk(&self, position: ChunkPosition) -> Option { - let chunk_column_position = position.chunk_column(); - let shard = chunk_column_position.shard(self.shard_count); - let cy_in_vec: usize = position.cy.saturating_add(4).try_into().ok()?; - - let shard = self.shards[shard].read().await; - let chunk_column = shard.get(&chunk_column_position)?; - let chunk = chunk_column.chunks.get(cy_in_vec)?; - - Some(chunk.as_network_chunk().clone()) - } - pub async fn set_block(&self, position: BlockPosition, block: BlockWithState) { - async fn inner_get_block(s: &WorldMap, position: BlockPosition, block: BlockWithState) -> Option<()> { + pub async fn set_block(&'static self, position: BlockPosition, block: BlockWithState) { + async fn inner_set_block(s: &'static WorldMap, position: BlockPosition, block: BlockWithState) -> Option<()> { let chunk_position = position.chunk(); let position_in_chunk_column = position.in_chunk_column(); let chunk_column_position = chunk_position.chunk_column(); let shard = chunk_column_position.shard(s.shard_count); - + let mut shard = s.shards[shard].write().await; let chunk_column = shard.get_mut(&chunk_column_position)?; - chunk_column.set_block(position_in_chunk_column, block); + chunk_column.set_block(position_in_chunk_column.clone(), block); Some(()) } - inner_get_block(self, position, block).await; + + inner_set_block(self, position.clone(), block.clone()).await; + LightManager::update_light(self, position, block).await; } + #[cfg_attr(feature = "trace", instrument(skip_all))] pub async fn try_move(&self, object: &CollisionShape, movement: &Translation) -> Translation { // TODO(perf): Optimize Map.try_move by preventing block double-checking // Also lock the map only once @@ -288,15 +493,52 @@ impl WorldMap { movement.clone() // Would be more logic if it returned validated, but this way we avoid precision errors } - pub async fn load(&self, position: ChunkColumnPosition) { + pub fn get_shard_count(&self) -> usize { + self.shard_count + } + + pub(super) async fn write_shard(&self, shard: usize) -> OwnedRwLockWriteGuard> { + self.shards[shard].clone().write_owned().await + } + + #[cfg_attr(feature = "trace", instrument(skip(self)))] + pub async fn load(&'static self, position: ChunkColumnPosition) { let chunk = ChunkColumn::flat(); // TODO: load from disk let shard = position.shard(self.shard_count); trace!("Loading chunk column at {:?}", position); let mut shard = self.shards[shard].write().await; - shard.entry(position).or_insert_with(|| chunk); + shard.entry(position.clone()).or_insert_with(|| chunk); + // init light because we don't really load the chunk but we generate it + LightManager::init_chunk_column_light(self, position).await; } + + pub async fn get_network_chunk_column_data(&self, position: ChunkColumnPosition) -> Option> { + let shard = position.shard(self.shard_count); + let shard = self.shards[shard].read().await; + let chunk_column = shard.get(&position)?; + + let serialized = NetworkChunk::into_data(chunk_column.chunks.iter().map(|c| c.data.clone()).collect()).unwrap(); + let (sky_light, sky_light_mask, empty_sky_light_mask) = chunk_column.light.get_packet(); + + let chunk_data = PlayClientbound::ChunkData { value: NetworkChunkColumnData { + chunk_x: position.cx, + chunk_z: position.cz, + heightmaps: chunk_column.heightmap.to_tag(), + data: Array::from(serialized.clone()), + block_entities: Array::default(), + sky_light_mask, + block_light_mask: Array::default(), + empty_sky_light_mask, + empty_block_light_mask: Array::default(), + sky_light, + block_light: Array::default(), + }}; + + let serialized = chunk_data.serialize_minecraft_packet().ok()?; + Some(serialized) + } pub async fn unload(&self, _position: ChunkColumnPosition) { // Note: these are not unloaded yet in order to preserve map data @@ -315,7 +557,7 @@ mod tests { #[test] fn test_get_block() { let chunk = Chunk::filled(BlockWithState::Dirt).unwrap(); - chunk.get_block(BlockPositionInChunk { bx: 0, by: 1, bz: 2 }); + chunk.get_block(&BlockPositionInChunk { bx: 0, by: 1, bz: 2 }); } #[test] @@ -331,7 +573,7 @@ mod tests { assert!(!chunk.palette_block_counts.is_empty()); let mut id = 1; for bx in 0..16 { - let got = chunk.get_block(BlockPositionInChunk { bx, by: 0, bz: 0 }).block_state_id().unwrap(); + let got = chunk.get_block(&BlockPositionInChunk { bx, by: 0, bz: 0 }).block_state_id().unwrap(); assert_eq!(id, got); id += 1; } @@ -356,7 +598,7 @@ mod tests { for bx in 0..16 { for by in 0..16 { for bz in 0..2 { - let got = chunk.get_block(BlockPositionInChunk { bx, by, bz }).block_state_id().unwrap(); + let got = chunk.get_block(&BlockPositionInChunk { bx, by, bz }).block_state_id().unwrap(); assert_eq!(id, got); id += 1; } @@ -410,27 +652,27 @@ mod tests { let high_block = flat_column.get_block(BlockPositionInChunkColumn { bx: 0, y: 120, bz: 0 }); assert_eq!(high_block.block_state_id().unwrap(), BlockWithState::Air.block_state_id().unwrap()); } - #[tokio::test] async fn test_world_map() { - let map = WorldMap::new(1); + let world = Box::leak(Box::new(World::new(broadcast_channel(100).1))); for cx in -3..=3 { for cz in -3..=3 { - map.load(ChunkColumnPosition { cx, cz }).await; + world.map.load(ChunkColumnPosition { cx, cz }).await; } } // Test single block - map.set_block(BlockPosition { x: -40, y: -40, z: -40 }, BlockWithState::RedstoneBlock).await; - let block = map.get_block(BlockPosition { x: -40, y: -40, z: -40 }).await; + world.set_block(BlockPosition { x: -40, y: -40, z: -40 }, BlockWithState::RedstoneBlock).await; + let block = world.map.get_block(BlockPosition { x: -40, y: -40, z: -40 }).await; assert_eq!(block.block_state_id().unwrap(), BlockWithState::RedstoneBlock.block_state_id().unwrap()); - + // Test no skylight at the same position + assert_eq!(world.get_light_level(BlockPosition { x: -40, y: -40, z: -40 }).await, 0); // Set blocks let mut id = 1; for x in (-40..40).step_by(9) { for y in (-40..200).step_by(15) { for z in (-40..40).step_by(9) { - map.set_block(BlockPosition { x, y, z }, BlockWithState::from_state_id(id).unwrap()).await; + world.set_block(BlockPosition { x, y, z }, BlockWithState::from_state_id(id).unwrap()).await; id += 1; } } @@ -441,7 +683,7 @@ mod tests { for x in (-40..40).step_by(9) { for y in (-40..200).step_by(15) { for z in (-40..40).step_by(9) { - let got = map.get_block(BlockPosition { x, y, z }).await.block_state_id().unwrap(); + let got = world.get_block(BlockPosition { x, y, z }).await.unwrap().block_state_id().unwrap(); assert_eq!(id, got); id += 1; } @@ -449,10 +691,62 @@ mod tests { } } + #[test] + fn test_heightmap_get_and_set() { + let mut heightmap = HeightMap::new(5); + heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }, 0); + heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: -2, bz: 1 }, 2); + heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: 3, bz: 2 }, 3); + heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: -4, bz: 3 }, 4); + heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: -4, bz: 7 }, 5); + + // Test get + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 0); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 1 }), 2); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 2 }), 3); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 3 }), 4); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 7 }), 5); + + // Test erase + heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: 12, bz: 0 }, 12); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 12, bz: 0 }), 12); + + // Test new base + //heightmap.new_base(8); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 12); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 1 }), 2); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 2 }), 3); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 3 }), 4); + assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 7 }), 5); + } + + #[test] + fn test_heightmap_auto_updates() { + let mut flat_column = ChunkColumn::flat(); + + // Check that the heightmap is correct + flat_column.set_block(BlockPositionInChunkColumn { bx: 0, y: 2, bz: 0 }, BlockWithState::GrassBlock { snowy: true }); + flat_column.init_chunk_heightmap(); + assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 67); + assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 1 }), 16); + + // Now check that the heightmap is correct after setting a block + flat_column.set_block(BlockPositionInChunkColumn { bx: 0, y: 10, bz: 0 }, BlockWithState::GrassBlock { snowy: false }); + assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 75); + + // Check that the heightmap is correct after setting a block to air under the highest block + flat_column.set_block(BlockPositionInChunkColumn { bx: 0, y: 8, bz: 0 }, BlockWithState::Air); + assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 75); + + // Check that the heightmap is correct after setting the highest block to air + flat_column.set_block(BlockPositionInChunkColumn { bx: 0, y: 10, bz: 0 }, BlockWithState::Air); + assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 67); + } + #[tokio::test] async fn test_try_move() { - let map = WorldMap::new(1); - map.load(ChunkColumnPosition { cx: 0, cz: 0 }).await; + let world = Box::leak(Box::new(World::new(broadcast_channel(100).1))); + world.map.load(ChunkColumnPosition { cx: 0, cz: 0 }).await; let bounding_box = CollisionShape { x1: 0.0, y1: 0.0, @@ -465,19 +759,32 @@ mod tests { // Position on ground and try to go through it let positionned_box = bounding_box.clone() + &Translation { x: 0.0, y: -3.0*16.0, z: 0.0 }; let movement = Translation { x: 0.0, y: -10.0, z: 0.0 }; - let movement = map.try_move(&positionned_box, &movement).await; + let movement = world.map.try_move(&positionned_box, &movement).await; assert_eq!(movement, Translation { x: 0.0, y: 0.0, z: 0.0 }); // It doesn't get through // Place it a little above ground let positionned_box = bounding_box.clone() + &Translation { x: 0.0, y: -3.0*16.0 + 1.0, z: 0.0 }; let movement = Translation { x: 0.0, y: -10.0, z: 0.0 }; - let movement = map.try_move(&positionned_box, &movement).await; + let movement = world.map.try_move(&positionned_box, &movement).await; assert_eq!(movement, Translation { x: 0.0, y: -1.0, z: 0.0 }); // It falls down but doesn't get through // Place it above but not on round coordinates let positionned_box = bounding_box.clone() + &Translation { x: 0.0, y: -3.0*16.0 + 1.1, z: 0.2 }; let movement = Translation { x: 2.0, y: -10.0, z: 0.0 }; - let movement = map.try_move(&positionned_box, &movement).await; + let movement = world.map.try_move(&positionned_box, &movement).await; assert_eq!(movement, Translation { x: 0.2200000000000003, y: -1.1000000000000014, z: 0.0 }); // It falls down but doesn't get through } + + #[tokio::test] + async fn test_light_getters_and_setters() { + let world = Box::leak(Box::new(World::new(broadcast_channel(100).1))); + world.map.load(ChunkColumnPosition { cx: 0, cz: 0 }).await; + + // Test set_light_level + world.set_light_level(BlockPosition { x: 0, y: 0, z: 0 }, 15).await; + assert_eq!(world.get_light_level(BlockPosition { x: 0, y: 0, z: 0 }).await, 15); + + world.set_light_level(BlockPosition { x: 0, y: 10, z: 0 }, 0).await; + assert_eq!(world.get_light_level(BlockPosition { x: 0, y: 10, z: 0 }).await, 0); + } } diff --git a/minecraft-server/src/world/mod.rs b/minecraft-server/src/world/mod.rs index 3a2ed2dd..957f1ec5 100644 --- a/minecraft-server/src/world/mod.rs +++ b/minecraft-server/src/world/mod.rs @@ -6,6 +6,8 @@ mod loading_manager; use loading_manager::*; mod map; use map::*; +mod light; +pub use light::*; mod ecs; use ecs::*; mod collisions; @@ -38,11 +40,7 @@ impl World { Some(self.map.get_block(position).await) } - pub async fn get_network_chunk(&self, position: ChunkPosition) -> Option { - self.map.get_network_chunk(position).await - } - - pub async fn set_block(&self, position: BlockPosition, block: BlockWithState) { + pub async fn set_block(&'static self, position: BlockPosition, block: BlockWithState) { self.map.set_block(position.clone(), block.clone()).await; self.notify(&position.chunk_column(), WorldChange::Block(position, block)).await; } @@ -61,7 +59,7 @@ impl World { self.change_senders.write().await.remove(&uuid); } - pub async fn update_loaded_chunks(&self, uuid: UUID, loaded_chunks: HashSet) { + pub async fn ensure_loaded_chunks(&'static self, uuid: UUID, loaded_chunks: HashSet) { let mut loading_manager = self.loading_manager.write().await; let loaded_chunks_before = loading_manager.get_loaded_chunks(); loading_manager.update_loaded_chunks(uuid, loaded_chunks); @@ -158,6 +156,20 @@ impl World { } } } + + #[cfg_attr(feature = "trace", instrument(skip(self)))] + pub async fn get_network_chunk_column_data<'a>(&self, position: ChunkColumnPosition) -> Option> { + self.map.get_network_chunk_column_data(position).await + } + + pub async fn get_light_level(&'static self, position: BlockPosition) -> u8 { + LightManager::new(&self.map).get_light_level(LightPosition::from(position)).await + } + + pub async fn set_light_level(&'static self, position: BlockPosition, light_level: u8) { + LightManager::new(&self.map).set_light_level(LightPosition::from(position), light_level).await; + } + } #[cfg(test)] @@ -167,12 +179,12 @@ mod tests { #[tokio::test] async fn test_world_notifications() { - let world = World::new(broadcast_channel(100).1); + let world = Box::leak(Box::new(World::new(broadcast_channel(100).1))); let mut receiver1 = world.add_loader(1).await; let mut receiver2 = world.add_loader(2).await; - world.update_loaded_chunks(1, vec![ChunkColumnPosition{cx: 0, cz: 0}].into_iter().collect()).await; - world.update_loaded_chunks(2, vec![ChunkColumnPosition{cx: 1, cz: 1}].into_iter().collect()).await; + world.ensure_loaded_chunks(1, vec![ChunkColumnPosition{cx: 0, cz: 0}].into_iter().collect()).await; + world.ensure_loaded_chunks(2, vec![ChunkColumnPosition{cx: 1, cz: 1}].into_iter().collect()).await; world.set_block(BlockPosition{x: 1, y: 1, z: 1}, BlockWithState::Air).await; assert!(matches!(receiver1.try_recv(), Ok(WorldChange::Block(BlockPosition{x: 1, y: 1, z: 1}, BlockWithState::Air)))); diff --git a/tools/tracy-p64 b/tools/tracy-p64 new file mode 100755 index 00000000..e81feee2 Binary files /dev/null and b/tools/tracy-p64 differ