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..c018942b 100644 --- a/minecraft-positions/src/lib.rs +++ b/minecraft-positions/src/lib.rs @@ -1,5 +1,7 @@ mod shards; +use std::ops::{AddAssign, Add}; + pub use minecraft_protocol::packets::Position as NetworkPosition; #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] @@ -175,6 +177,17 @@ pub struct ChunkColumnPosition { pub cz: i32, } +impl Add for ChunkColumnPosition { + type Output = ChunkColumnPosition; + + fn add(self, rhs: ChunkColumnPosition) -> Self::Output { + ChunkColumnPosition { + cx: self.cx + rhs.cx, + cz: self.cz + rhs.cz, + } + } +} + impl ChunkColumnPosition { pub fn chunk(&self, cy: i32) -> ChunkPosition { ChunkPosition { 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/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..97f1b8d8 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 diff --git a/minecraft-protocol/src/components/mod.rs b/minecraft-protocol/src/components/mod.rs index 05fe9874..abea70e2 100644 --- a/minecraft-protocol/src/components/mod.rs +++ b/minecraft-protocol/src/components/mod.rs @@ -21,4 +21,4 @@ pub mod slots; pub mod sound; pub mod tags; pub mod teams; -pub mod trades; +pub mod trades; \ No newline at end of file diff --git a/minecraft-protocol/src/components/slots.rs b/minecraft-protocol/src/components/slots.rs index abdaa336..38b68f34 100644 --- a/minecraft-protocol/src/components/slots.rs +++ b/minecraft-protocol/src/components/slots.rs @@ -9,7 +9,7 @@ pub struct Slot { } #[cfg_attr(test, derive(PartialEq))] -#[derive(Debug, Clone, MinecraftPacketPart)] +#[derive(Debug, Clone, Default, MinecraftPacketPart)] pub struct SlotItem { /// The [item](crate::ids::items::Item). /// Item IDs are distinct from [block IDs](crate::ids::blocks::Block); see [crate::ids] for more information. diff --git a/minecraft-protocol/src/nbt/mod.rs b/minecraft-protocol/src/nbt/mod.rs index 1bad2873..0df29168 100644 --- a/minecraft-protocol/src/nbt/mod.rs +++ b/minecraft-protocol/src/nbt/mod.rs @@ -25,6 +25,12 @@ pub enum NbtTag { RootCompound(String, HashMap), } +impl Default for NbtTag { + fn default() -> Self { + NbtTag::Null + } +} + impl NbtTag { #[inline] pub fn is_null(&self) -> bool { 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..b3fc1e4c 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-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 = [] +tracing = ["tracy-client", "tracing-tracy"] diff --git a/minecraft-server/src/entities/entity.rs b/minecraft-server/src/entities/entity.rs index 59da82ac..c62726c9 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, @@ -29,6 +31,7 @@ pub struct Entity { } impl Handler { + #[instrument(skip_all)] pub async fn init(self, server_msg_rcvr: BroadcastReceiver) { self.insert_task("newton", tokio::spawn(newton_task(self.clone(), server_msg_rcvr))).await; } diff --git a/minecraft-server/src/entities/player.rs b/minecraft-server/src/entities/player.rs index acf209b8..22cb3e44 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(); @@ -115,10 +116,10 @@ impl Handler { 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) { + for skipped in newly_loaded_chunks.iter().skip(2) { loaded_chunks_after.remove(skipped); } - newly_loaded_chunks.truncate(50); + newly_loaded_chunks.truncate(2); let uuid = player.info.uuid; player.loaded_chunks = loaded_chunks_after.clone(); (Some((loaded_chunks_after, newly_loaded_chunks, unloaded_chunks, uuid)), EntityChanges::other()) @@ -128,39 +129,17 @@ impl Handler { self.world.update_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); + let mut chunks = Vec::new(); 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 = self.world.get_network_chunk_column_data(newly_loaded_chunk.clone()).await.unwrap_or_else(|| { + error!("Chunk not loaded: {newly_loaded_chunk:?}"); + panic!("Chunk not loaded: {newly_loaded_chunk:?}"); + }); + chunks.push(chunk); + } + + for chunk in chunks { + self.send_raw_packet(chunk).await; } // Tell the client to unload chunks @@ -172,6 +151,7 @@ impl Handler { } } + #[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 +165,28 @@ impl Handler { packet_sender.send(packet).await.unwrap(); } + #[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(); + } + 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) => { @@ -293,6 +292,7 @@ impl Handler { } } + #[instrument(skip_all)] async fn on_packet<'a>(mut self, packet: PlayServerbound<'a>) { use PlayServerbound::*; match packet { @@ -367,6 +367,7 @@ impl Handler { } } +#[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 +377,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..d0f414d7 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 = "tracing")] + #[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 = "tracing")] + 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..3b206068 100644 --- a/minecraft-server/src/player_handler/connect.rs +++ b/minecraft-server/src/player_handler/connect.rs @@ -1,5 +1,7 @@ use super::*; + +#[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..59366e35 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?; @@ -307,6 +309,7 @@ pub async fn handshake(stream: &mut TcpStream, logged_in_player_info: LoggedInPl debug!("ChunkBatchStart sent"); let change_receiver = world.add_loader(logged_in_player_info.uuid).await; + // TODO: Move chunk loading let mut loaded_chunks = HashSet::new(); for cx in -3..=3 { for cz in -3..=3 { @@ -315,43 +318,18 @@ pub async fn handshake(stream: &mut TcpStream, logged_in_player_info: LoggedInPl } 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); - + 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).await; } } + debug!("ChunkData sent"); // Chunk batch end @@ -367,7 +345,6 @@ pub async fn handshake(stream: &mut TcpStream, logged_in_player_info: LoggedInPl return Err(()); }; debug!("ChunkBatchAcknoledgement received"); - Ok((PlayerInfo { addr: logged_in_player_info.addr, username: logged_in_player_info.username, diff --git a/minecraft-server/src/player_handler/login.rs b/minecraft-server/src/player_handler/login.rs index 043ced41..1a78f4cd 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, } +#[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..eb723853 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) } +#[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(); } +#[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(); } +#[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/play.rs b/minecraft-server/src/player_handler/play.rs new file mode 100644 index 00000000..f54278fb --- /dev/null +++ b/minecraft-server/src/player_handler/play.rs @@ -0,0 +1,230 @@ +use super::*; + +struct PlayerHandler { + world: Arc, + game_mode: Gamemode, + info: PlayerInfo, + position: Position, + yaw: f32, + pitch: f32, + on_ground: bool, + packet_sender: MpscSender>, + + render_distance: i32, + loaded_chunks: HashSet, + center_chunk: ChunkPosition, +} + +impl PlayerHandler { + async fn send_packet<'a>(&mut self, packet: PlayClientbound<'a>) { + let packet = packet.serialize_minecraft_packet().unwrap(); + self.packet_sender.send(packet).await.unwrap(); + } + + async fn send_packet_raw(&mut self, packet: Vec) { + self.packet_sender.send(packet).await.unwrap(); + } + + async fn on_server_message(&mut self, message: ServerMessage) { + use ServerMessage::*; + match message { + Tick => { + self.send_packet(PlayClientbound::BundleDelimiter).await; + } + } + } + + async fn on_block_change(&mut self, position: BlockPosition, block: BlockWithState) { + self.send_packet(PlayClientbound::BlockUpdate { + location: position.into(), + block_state: block, + }).await; + } + + async fn on_move(&mut self) { + let new_center_chunk = self.position.chunk(); + // Tell the client which chunk he is in + if new_center_chunk == self.center_chunk { return }; + 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() == self.center_chunk.chunk_column() { + self.center_chunk = new_center_chunk; + return; + }; + let mut loaded_chunks_after = HashSet::new(); + for cx in (new_center_chunk.cx - self.render_distance)..=(new_center_chunk.cx + self.render_distance) { + for cz in (new_center_chunk.cz - self.render_distance)..=(new_center_chunk.cz + self.render_distance) { + let dist = (cx - new_center_chunk.cx).abs() + (cz - new_center_chunk.cz).abs(); + if dist > self.render_distance { continue }; + loaded_chunks_after.insert(ChunkColumnPosition { cx, cz }); + } + } + + // Select chunks to load (max 50) and unload + if loaded_chunks_after == self.loaded_chunks { return }; + let mut newly_loaded_chunks: Vec<_> = loaded_chunks_after.difference(&self.loaded_chunks).cloned().collect(); + let unloaded_chunks: Vec<_> = self.loaded_chunks.difference(&loaded_chunks_after).cloned().collect(); + for skipped in newly_loaded_chunks.iter().skip(5) { + loaded_chunks_after.remove(skipped); + } + newly_loaded_chunks.truncate(5); + + // Tell the world about the changes + self.world.update_loaded_chunks(self.info.uuid, loaded_chunks_after.clone()).await; + + // Send the chunks to the client + for newly_loaded_chunk in newly_loaded_chunks { + + let chunk_column_data = self.world.get_network_chunk_column_data(newly_loaded_chunk.clone()).await.unwrap_or_else(|| { + error!("Chunk not loaded: {newly_loaded_chunk:?}"); + panic!("Chunk not loaded: {newly_loaded_chunk:?}"); + }); + self.send_packet_raw(chunk_column_data).await; + + info!("sent"); + } + + + // Tell the client to unload chunks + for unloaded_chunk in unloaded_chunks { + self.send_packet(PlayClientbound::UnloadChunk { + chunk_x: unloaded_chunk.cx, + chunk_z: unloaded_chunk.cz, + }).await; + } + + self.loaded_chunks = loaded_chunks_after; + + } + + async fn on_packet<'a>(&mut self, packet: PlayServerbound<'a>) { + use PlayServerbound::*; + match packet { + SetPlayerPosition { x, y, z, on_ground } => { + self.position.x = x; + self.position.y = y; + self.position.z = z; + self.on_ground = on_ground; + self.on_move().await; + // TODO: make sure the movement is allowed + }, + SetPlayerRotation { yaw, pitch, on_ground } => { + self.yaw = yaw; + self.pitch = pitch; + self.on_ground = on_ground; + } + SetPlayerPositionAndRotation { x, y, z, yaw, pitch, on_ground } => { + self.position.x = x; + self.position.y = y; + self.position.z = z; + self.yaw = yaw; + self.pitch = pitch; + self.on_ground = on_ground; + self.on_move().await; + // TODO: make sure the movement is allowed + }, + DigBlock { status, location, face: _, sequence: _ } => { + use minecraft_protocol::components::blocks::DiggingState; + // TODO: Check legitimacy + if self.game_mode == Gamemode::Creative || status == DiggingState::Finished { + self.world.set_block(location.into(), BlockWithState::Air).await; + } + } + packet => warn!("Unsupported packet received: {packet:?}"), + } + } +} + +pub async fn handle_player(stream: TcpStream, player_info: PlayerInfo, mut server_msg_rcvr: BroadcastReceiver, world: Arc, mut change_receiver: MpscReceiver) -> Result<(), ()> { + let (packet_sender, mut packet_receiver) = mpsc_channel(100); + + let mut handler = PlayerHandler { + world, + game_mode: Gamemode::Creative, + position: Position { x: 0.0, y: 60.0, z: 0.0 }, + yaw: 0.0, + pitch: 0.0, + on_ground: false, + packet_sender, + + center_chunk: ChunkPosition { cx: 0, cy: 11, cz: 0 }, + render_distance: player_info.render_distance.clamp(4, 15) as i32, + loaded_chunks: HashSet::new(), + + info: player_info, + }; + + for cx in -3..=3 { + for cz in -3..=3 { + handler.loaded_chunks.insert(ChunkColumnPosition { cx, cz }); + } + } + + let (mut reader_stream, mut writer_stream) = stream.into_split(); + + let mut receive_packet_fut = Box::pin(receive_packet_split(&mut reader_stream).fuse()); + let mut receive_clientbound_fut = Box::pin(packet_receiver.recv().fuse()); + let mut receive_server_message_fut = Box::pin(server_msg_rcvr.recv().fuse()); + let mut receive_change_fut = Box::pin(change_receiver.recv().fuse()); + loop { + // Select the first event that happens + enum Event { + PacketServerbound(Result, ()>), + PacketClientbound(Option>), + Message(Result), + WorldChange(Option), + } + let event = futures::select! { + packet_serverbound = receive_packet_fut => Event::PacketServerbound(packet_serverbound), + packet_clientbound = receive_clientbound_fut => Event::PacketClientbound(packet_clientbound), + message = receive_server_message_fut => Event::Message(message), + change = receive_change_fut => Event::WorldChange(change), + }; + match event { + Event::PacketServerbound(Ok(packet)) => { + drop(receive_packet_fut); + receive_packet_fut = Box::pin(receive_packet_split(&mut reader_stream).fuse()); + + let packet = PlayServerbound::deserialize_uncompressed_minecraft_packet(packet.as_slice()).unwrap(); + handler.on_packet(packet).await; + }, + Event::PacketClientbound(Some(packet)) => { + drop(receive_clientbound_fut); + receive_clientbound_fut = Box::pin(packet_receiver.recv().fuse()); + + send_packet_raw_split(&mut writer_stream, packet.as_slice()).await; + }, + Event::Message(Ok(message)) => { + drop(receive_server_message_fut); + receive_server_message_fut = Box::pin(server_msg_rcvr.recv().fuse()); + + handler.on_server_message(message).await; + }, + Event::WorldChange(Some(change)) => { + drop(receive_change_fut); + receive_change_fut = Box::pin(change_receiver.recv().fuse()); + + match change { + WorldChange::BlockChange(position, block) => handler.on_block_change(position, block).await, + } + }, + Event::Message(Err(recv_error)) => { + error!("Failed to receive message: {recv_error:?}"); + return Err(()); + } + Event::PacketClientbound(None) => { + error!("Failed to receive clientbound packet"); + return Err(()); + } + Event::PacketServerbound(Err(e)) => { + error!("Failed to receive serverbound packet: {e:?}"); + return Err(()); + } + Event::WorldChange(None) => { + error!("Failed to receive world change"); + return Err(()); + } + } + } +} diff --git a/minecraft-server/src/player_handler/status.rs b/minecraft-server/src/player_handler/status.rs index aa2f724c..0e6dd654 100644 --- a/minecraft-server/src/player_handler/status.rs +++ b/minecraft-server/src/player_handler/status.rs @@ -1,5 +1,6 @@ use super::*; +#[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..9c40c7f9 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, @@ -30,12 +31,14 @@ impl Entities { } /// Observe an entity through a closure + #[instrument(skip_all)] pub(super) async fn observe_entity(&self, eid: Eid, observer: impl FnOnce(&AnyEntity) -> R) -> Option { self.entities.read().await.get(&eid).map(observer) } /// Observe entities in a chunk through a closure /// That closure will be applied to each entity, and the results will be returned in a vector + #[instrument(skip_all)] pub(super) async fn observe_entities(&self, chunk: ChunkColumnPosition, mut observer: impl FnMut(&AnyEntity) -> Option) -> Vec { let entities = self.entities.read().await; let chunks = self.chunks.read().await; @@ -52,6 +55,7 @@ impl Entities { } /// Mutate an entity through a closure + #[instrument(skip_all)] pub(super) async fn mutate_entity(&self, eid: Eid, mutator: impl FnOnce(&mut AnyEntity) -> (R, EntityChanges)) -> Option<(R, EntityChanges)> { let mut entities = self.entities.write().await; @@ -72,6 +76,7 @@ impl Entities { } } + #[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 +95,8 @@ impl Entities { h.init(receiver).await; (eid, uuid) } - + + #[instrument(skip_all)] 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 +106,7 @@ impl Entities { } /// Remove an entity + #[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..54d685ad --- /dev/null +++ b/minecraft-server/src/world/light.rs @@ -0,0 +1,636 @@ +use std::{ops::AddAssign, collections::{BinaryHeap, hash_map::Entry}}; +use minecraft_protocol::ids::blocks::Block; +use tokio::sync::{RwLockWriteGuard, 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) + } + + /// 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); + } 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 + let mask = 1 << section; + if self.level > 0 { + self.empty_light_mask &= !mask; + self.light_mask |= mask; + } else { + self.empty_light_mask |= mask; + self.light_mask &= !mask; + } + } + + 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)?; + 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() + } +} + +#[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)] +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) + } +} + +pub struct LightManager { + world_map: &'static WorldMap, + current_shard_id: Option, + current_shard: Option>>, +} + +impl LightManager { + fn new(world_map: &'static WorldMap) -> Self { + Self { + world_map, + current_shard: None, + current_shard_id: None, + } + } + + pub async fn update_light(world_map: &'static WorldMap, block_position: BlockPosition, block: BlockWithState) { + let mut light_manager = Self::new(world_map); + + light_manager.set_block(block_position, block).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") + } + } + + + + async fn set_light_level(&mut self, position: LightPosition, level: u8) { + unimplemented!(); + } + + async fn get_light_level(&mut self, position: LightPosition) -> u8 { + unimplemented!(); + } + + pub async fn set_block(&mut self, block_position: BlockPosition, block: BlockWithState) { + let mut to_explore = BinaryHeap::new(); + let position = LightPosition::from(block_position.clone()); + to_explore.extend(position.get_neighbors(24)); + while let Some(postion) = to_explore.pop() { + + if let Some(column) = self.get_chunk_column(position.clone().into()).await { + let block = Block::from(column.get_block(position.clone().into())); + + if block.is_transparent() { + let highest_block = column.get_highest_block_at(&block_position.in_chunk_column()); + let is_inside = highest_block > postion.clone().y as u16 + 1; + let new_level = if is_inside { postion.clone().y as u8 - block.light_absorption() - 1 } else { MAX_LIGHT_LEVEL }; + let new_position = LightPositionInChunkColumn::from(postion.clone()); + + to_explore.extend(postion.clone().get_neighbors(24)); + } + } + } + + // Clear locked chunks + + } + + + 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); + } + } + } + } + + /*fn propagate_sky_light_inside(&mut self) -> Result { + let mut to_propagate = EdgesLightToPropagate::new(); + // Set all highest blocks to the highest block + let highest_blocks = self.get_highest_block(); + + let max_y = self.light.sky_light.light_arrays.len() * 16 - 1; + let mut to_explore: BinaryHeap = BinaryHeap::new(); + to_propagate.expand(self.light.sky_light.set_region(highest_blocks as usize + 16, max_y, self.light.sky_light.level)?); + + // Add all highest blocks to the queue + for x in 0..16 { + for z in 0..16 { + let position = LightPositionInChunkColumn { + bx: x, + y: self.get_highest_block_at(&BlockPositionInChunkColumn { bx: x, y: 0, bz: z }) as usize + 16 + 1, + bz: z, + }; + to_explore.push(position); + } + } + + to_propagate.expand(self.explore_sky_light_from_heap(&mut to_explore).map_err(|_| error!("Error while updating light"))?); + Ok(to_propagate) + }*/ + + /*fn explore_sky_light_from_heap(&mut self, to_explore: &mut BinaryHeap) -> Result { + // We get the neighbors and determine the light source from them + // The neighbor with the highest light level is the light source + // So we explore from it + let mut edges = EdgesLightToPropagate::new(); + while let Some(position) = to_explore.pop() { + let neighbors = position.get_neighbors(self.light.sky_light.light_arrays.len()); + let my_level = self.light.sky_light.get_level(position.clone())?; + + for neighbor in neighbors { + let neighbor_level = self.light.sky_light.get_level(neighbor.clone())?; + + let block = Block::from(self.get_block(neighbor.clone().into())); + if block.is_transparent() + && (neighbor_level < my_level.saturating_sub(1)) + { + let highest_block = self.get_highest_block_at(&neighbor.clone().into()) + 16; + let is_inside = highest_block > neighbor.y as u16 + 1; + to_explore.push(neighbor.clone()); + let new_level = if is_inside { my_level - 1 } else { self.light.sky_light.level }; + edges.expand(self.light.sky_light.set_level(neighbor, new_level)?); + } + } + } + Ok(edges) + } + + fn clear_lightsystem_from(&mut self, position: LightPositionInChunkColumn) -> Result { + let mut to_explore: BinaryHeap = BinaryHeap::new(); + let mut edges = EdgesLightToPropagate::new(); + // We get the neighbors and determine the light source from them + // The neighbor with the highest light level is the light source + // then we clear from the other neighbors + // if are equal, we have nothing to do + + let my_level = self.light.sky_light.get_level(position.clone())?; + self.light.sky_light.set_level(position.clone(), my_level.saturating_sub(1))?; + to_explore.push(position.clone()); + + while let Some(position) = to_explore.pop() { + let neighbors = position.get_neighbors(self.light.sky_light.light_arrays.len()); + let my_level = self.light.sky_light.get_level(position.clone())?; + let my_is_inside = self.get_highest_block_at(&position.clone().into()) + 16 > position.y as u16 + 1; + + for neighbor in neighbors { + let neighbor_level = self.light.sky_light.get_level(neighbor.clone()).unwrap(); + + let block = Block::from(self.get_block(neighbor.clone().into())); + + if block.is_transparent() + && ((my_is_inside && neighbor_level <= my_level) + || (!my_is_inside && neighbor_level < my_level)) + { + let highest_block = self.get_highest_block_at(&neighbor.clone().into()) + 16; + let is_inside = highest_block > neighbor.y as u16 + 1; + to_explore.push(neighbor.clone()); + let new_level = if is_inside { my_level - block.light_absorption() - 1 } else { self.light.sky_light.level }; + edges.expand(self.light.sky_light.set_level(neighbor, new_level)?); + } + } + } + Ok(edges) + } + + pub(super) fn update_light_as_block_changed_at(&mut self, position: BlockPositionInChunkColumn) -> Result { + let position = LightPositionInChunkColumn::from(position); + let blocking = !Block::from(self.get_block(position.clone().into())).is_transparent(); + let mut to_explore: BinaryHeap = BinaryHeap::from(position.get_neighbors(self.light.sky_light.light_arrays.len())); + let mut to_propagate = EdgesLightToPropagate::new(); + if blocking { + to_propagate.expand(self.clear_lightsystem_from(position.clone()).map_err(|_| error!("Error while updating light"))?); + } + to_propagate.expand(self.explore_sky_light_from_heap(&mut to_explore).map_err(|_| error!("Error while updating light"))?); + Ok(to_propagate) + }*/ +} + +#[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); + } + +} diff --git a/minecraft-server/src/world/loading_manager.rs b/minecraft-server/src/world/loading_manager.rs index 3a5d2178..b00e1259 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 { + #[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..2894a46b 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, vec}; +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,6 +54,7 @@ impl Chunk { &self.data } + #[instrument(skip_all)] fn get_block(&self, position: BlockPositionInChunk) -> BlockWithState { match &self.data.blocks { PalettedData::Paletted { palette, indexed } => { @@ -72,6 +75,7 @@ impl Chunk { } // TODO edit block_count in data + #[instrument(skip_all)] fn set_block(&mut self, position: BlockPositionInChunk, block: BlockWithState) { let block_state_id = block.block_state_id().unwrap_or_else(|| { error!("Tried to set block with invalid state {block:?}. Placing air"); 0 @@ -167,11 +171,198 @@ 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 { + heightmap: HeightMap, + pub(super) light: Light, chunks: Vec, } impl ChunkColumn { + pub const MAX_HEIGHT: u16 = 320 + 64; // TODO: adapt to the world height + pub const MIN_Y: i32 = -64; + + fn init_chunk_heightmap(&mut self){ + self.heightmap = HeightMap::new(9); + 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.div_euclid(16) as usize - (current_height.rem_euclid(16) > 0) as usize; + let mut current_height = current_height - 1; + // Downward propagation + for chunk in self.chunks.iter().rev().skip(n_chunk_to_skip) { + for by in (0..((((current_height) % 16) + 1) as u8)).rev() { + let block: BlockWithState = chunk.get_block(BlockPositionInChunk { bx: position.bx, by, bz: position.bz }); + // 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(9), + light: Light::new(), + }; + column.init_chunk_heightmap(); + column.init_independant_light(); + column + } + pub fn flat() -> Self { let empty_chunk = Chunk { data: NetworkChunk { @@ -194,10 +385,10 @@ 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()?; @@ -208,16 +399,42 @@ impl ChunkColumn { 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); + }, + _ => {} + } + } + + fn get_skylight(&self, position: BlockPositionInChunkColumn) -> u8 { + self.light.get_skylight_level(position.into()) } } @@ -225,15 +442,19 @@ 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 } } + #[instrument(skip_all)] pub async fn get_block(&self, position: BlockPosition) -> BlockWithState { async fn inner_get_block(s: &WorldMap, position: BlockPosition) -> Option { let chunk_position = position.chunk(); - let position_in_chunk_column = position.in_chunk_column(); + let position_in_chunk_column: BlockPositionInChunkColumn = position.in_chunk_column(); let chunk_column_position = chunk_position.chunk_column(); let shard = chunk_column_position.shard(s.shard_count); @@ -244,20 +465,38 @@ 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(super) async fn write_shard(&self, shard: usize) -> OwnedRwLockWriteGuard> { + self.shards[shard].clone().write_owned().await } - pub async fn set_block(&self, position: BlockPosition, block: BlockWithState) { - async fn inner_get_block(s: &WorldMap, position: BlockPosition, block: BlockWithState) -> Option<()> { + #[instrument(skip_all)] + 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 (skylight_array_data, skylight_mask, empty_skylight_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: skylight_mask, + block_light_mask: Array::default(), + empty_sky_light_mask: empty_skylight_mask, + empty_block_light_mask: Array::default(), + sky_light: skylight_array_data, + block_light: Array::default(), + }}; + let serialized = chunk_data.serialize_minecraft_packet().ok()?; + Some(serialized) + } + + 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(); @@ -265,12 +504,41 @@ impl WorldMap { 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; } + #[instrument(skip_all)] + pub async fn get_skylight(&self, position: BlockPosition) -> u8 { + async fn inner_get_skylight(s: &WorldMap, position: BlockPosition) -> Option { + let chunk_position = position.chunk(); + let chunk_column_position = chunk_position.chunk_column(); + let shard = chunk_column_position.shard(s.shard_count); + + let shard = s.shards[shard].read().await; + let chunk_column = shard.get(&chunk_column_position)?; + let level = chunk_column.get_skylight(position.in_chunk_column()); + Some(level) + } + inner_get_skylight(self, position).await.unwrap_or(0) + } + + /*async fn update_light_from_edge(&self, chunk_column_position: ChunkColumnPosition, to_propagate: BinaryHeap<(LightPositionInChunkColumn, u8)>) { + async fn inner_get_skylight(s: &WorldMap, chunk_column_position: ChunkColumnPosition, to_propagate: BinaryHeap<(LightPositionInChunkColumn, u8)>) -> Option<()> { + 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.update_from_edge(to_propagate).ok()?; + Some(()) + } + inner_get_skylight(self, chunk_column_position, to_propagate).await; + }*/ + 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,13 +556,15 @@ 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) { + #[instrument(skip_all)] + 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); + LightManager::init_chunk_column_light(self, position).await; } pub async fn unload(&self, _position: ChunkColumnPosition) { @@ -306,6 +576,10 @@ impl WorldMap { //shard.remove(&position); // TODO: write to disk } + + pub fn get_shard_count(&self) -> usize { + self.shard_count + } } #[cfg(test)] @@ -413,16 +687,16 @@ mod tests { #[tokio::test] async fn test_world_map() { - let map = WorldMap::new(1); + let world_map = Box::leak(Box::new(WorldMap::new(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_map.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()); // Set blocks @@ -430,7 +704,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) { - map.set_block(BlockPosition { x, y, z }, BlockWithState::from_state_id(id).unwrap()).await; + world_map.set_block(BlockPosition { x, y, z }, BlockWithState::from_state_id(id).unwrap()).await; id += 1; } } @@ -441,7 +715,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_map.get_block(BlockPosition { x, y, z }).await.block_state_id().unwrap(); assert_eq!(id, got); id += 1; } @@ -449,10 +723,74 @@ 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); + } + + #[test] + fn benchmark_get_block() { + let start_time = std::time::Instant::now(); + for _ in 0..441 { + let _column = ChunkColumn::flat(); + } + + let elapsed: Duration = start_time.elapsed(); + println!("All Elapsed: {:?}", elapsed); + println!("Elapsed: {:?}", elapsed / 441); + } + #[tokio::test] async fn test_try_move() { - let map = WorldMap::new(1); - map.load(ChunkColumnPosition { cx: 0, cz: 0 }).await; + let world_map: &mut WorldMap = Box::leak(Box::new(WorldMap::new(1))); + world_map.load(ChunkColumnPosition { cx: 0, cz: 0 }).await; let bounding_box = CollisionShape { x1: 0.0, y1: 0.0, @@ -465,19 +803,29 @@ 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_skylight() { + let world_map = Box::leak(Box::new(WorldMap::new(1))); + world_map.load(ChunkColumnPosition { cx: 0, cz: 0 }).await; + + // Test skylight initialisation for flat map + assert_eq!(world_map.get_skylight(BlockPosition { x: 8, y: 200, z: 8 }).await, 15, "The skylight is not valid for the blocks higher than the highest block"); + + } } diff --git a/minecraft-server/src/world/mod.rs b/minecraft-server/src/world/mod.rs index 3a2ed2dd..fcbf8ad5 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; +use light::*; mod ecs; use ecs::*; mod collisions; @@ -17,7 +19,6 @@ pub use collisions::*; pub struct World { map: WorldMap, entities: Entities, - loading_manager: RwLock, change_senders: RwLock>>, // TODO: Add a way to select events you want to subscribe to receiver: BroadcastReceiver, @@ -38,11 +39,11 @@ 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 get_network_chunk_column_data<'a>(&self, position: ChunkColumnPosition) -> Option> { + self.map.get_network_chunk_column_data(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 +62,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 update_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); @@ -167,7 +168,7 @@ 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; 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