From 9eac0f01e1e56af66fef0acc390dc352e99ca138 Mon Sep 17 00:00:00 2001 From: kralverde <80051564+kralverde@users.noreply.github.com> Date: Tue, 22 Oct 2024 07:15:17 -0400 Subject: [PATCH] Fix chunk loading deadlock and dangling chunk memory (#159) * rebase to master * fix chunk unloading --- pumpkin-world/Cargo.toml | 1 + .../src/cylindrical_chunk_iterator.rs | 62 +++-- pumpkin-world/src/level.rs | 246 +++++++++--------- pumpkin/src/client/mod.rs | 10 +- pumpkin/src/entity/player.rs | 19 +- pumpkin/src/world/mod.rs | 53 ++-- pumpkin/src/world/player_chunker.rs | 69 +---- 7 files changed, 230 insertions(+), 230 deletions(-) diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index cf0953993..db8beb01f 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -19,6 +19,7 @@ parking_lot.workspace = true num-traits.workspace = true num-derive.workspace = true futures = "0.3" +dashmap = "6.1.0" # Compression flate2 = "1.0" diff --git a/pumpkin-world/src/cylindrical_chunk_iterator.rs b/pumpkin-world/src/cylindrical_chunk_iterator.rs index 0f08f4286..39a873018 100644 --- a/pumpkin-world/src/cylindrical_chunk_iterator.rs +++ b/pumpkin-world/src/cylindrical_chunk_iterator.rs @@ -14,31 +14,24 @@ impl Cylindrical { } } - #[allow(unused_variables)] pub fn for_each_changed_chunk( old_cylindrical: Cylindrical, new_cylindrical: Cylindrical, mut newly_included: impl FnMut(Vector2), mut just_removed: impl FnMut(Vector2), - ignore: bool, ) { let min_x = old_cylindrical.left().min(new_cylindrical.left()); let max_x = old_cylindrical.right().max(new_cylindrical.right()); let min_z = old_cylindrical.bottom().min(new_cylindrical.bottom()); let max_z = old_cylindrical.top().max(new_cylindrical.top()); + //log::debug!("{:?} {:?}", old_cylindrical, new_cylindrical); for x in min_x..=max_x { for z in min_z..=max_z { - let old_is_within = if ignore { - false - } else { - old_cylindrical.is_within_distance(x, z) - }; - let new_is_within = if ignore { - true - } else { - new_cylindrical.is_within_distance(x, z) - }; + let old_is_within = old_cylindrical.is_within_distance(x, z); + let new_is_within = new_cylindrical.is_within_distance(x, z); + + //log::debug!("{}, {}: {} {}", x, z, old_is_within, new_is_within); if old_is_within != new_is_within { if new_is_within { @@ -68,21 +61,23 @@ impl Cylindrical { } fn is_within_distance(&self, x: i32, z: i32) -> bool { - let max_dist_squared = self.view_distance * self.view_distance; - let max_dist = self.view_distance as i64; - let dist_x = (x - self.center.x).abs().max(0) - (1); - let dist_z = (z - self.center.z).abs().max(0) - (1); - let dist_squared = dist_x.pow(2) + (max_dist.min(dist_z as i64) as i32).pow(2); - dist_squared < max_dist_squared + let rel_x = ((x - self.center.x).abs() - 1).max(0); + let rel_z = ((z - self.center.z).abs() - 1).max(0); + + let max_leg = (rel_x.max(rel_z) - 1).max(0) as i64; + let min_leg = rel_x.min(rel_z) as i64; + + let hyp_sqr = max_leg * max_leg + min_leg * min_leg; + hyp_sqr < (self.view_distance * self.view_distance) as i64 } /// Returns an iterator of all chunks within this cylinder pub fn all_chunks_within(&self) -> Vec> { // This is a naive implementation: start with square and cut out ones that dont fit let mut all_chunks = Vec::new(); - for x in -self.view_distance..=self.view_distance { - for z in -self.view_distance..=self.view_distance { - all_chunks.push(Vector2::new(self.center.x + x, self.center.z + z)); + for x in self.left()..=self.right() { + for z in self.bottom()..=self.top() { + all_chunks.push(Vector2::new(x, z)); } } all_chunks @@ -91,3 +86,28 @@ impl Cylindrical { .collect() } } + +#[cfg(test)] +mod test { + + use super::Cylindrical; + use pumpkin_core::math::vector2::Vector2; + + #[test] + fn test_bounds() { + let cylinder = Cylindrical::new(Vector2::new(0, 0), 10); + for chunk in cylinder.all_chunks_within() { + assert!(chunk.x >= cylinder.left() && chunk.x <= cylinder.right()); + assert!(chunk.z >= cylinder.bottom() && chunk.z <= cylinder.top()); + } + + for x in (cylinder.left() - 2)..=(cylinder.right() + 2) { + for z in (cylinder.bottom() - 2)..=(cylinder.top() + 2) { + if cylinder.is_within_distance(x, z) { + assert!(x >= cylinder.left() && x <= cylinder.right()); + assert!(z >= cylinder.bottom() && z <= cylinder.top()); + } + } + } + } +} diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index b4207583a..f0b0b8d08 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -1,4 +1,9 @@ -use std::{collections::HashMap, path::PathBuf, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; + +use dashmap::{DashMap, Entry}; +use pumpkin_core::math::vector2::Vector2; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use tokio::sync::{mpsc, RwLock}; use crate::{ chunk::{ @@ -6,11 +11,6 @@ use crate::{ }, world_gen::{get_world_gen, Seed, WorldGenerator}, }; -use pumpkin_core::math::vector2::Vector2; -use tokio::sync::mpsc; -use tokio::sync::{Mutex, RwLock}; - -type RAMChunkStorage = Arc, Arc>>>>; /// The `Level` module provides functionality for working with chunks within or outside a Minecraft world. /// @@ -23,11 +23,12 @@ type RAMChunkStorage = Arc, Arc>>> /// For more details on world generation, refer to the `WorldGenerator` module. pub struct Level { save_file: Option, - loaded_chunks: RAMChunkStorage, - chunk_watchers: Arc, usize>>>, - chunk_reader: Arc>, - world_gen: Arc>, + loaded_chunks: Arc, Arc>>>, + chunk_watchers: Arc, usize>>, + chunk_reader: Arc, + world_gen: Arc, } + #[derive(Clone)] pub struct SaveFile { #[expect(dead_code)] @@ -38,7 +39,6 @@ pub struct SaveFile { impl Level { pub fn from_root_folder(root_folder: PathBuf) -> Self { let world_gen = get_world_gen(Seed(0)).into(); // TODO Read Seed from config. - if root_folder.exists() { let region_folder = root_folder.join("region"); assert!( @@ -52,9 +52,9 @@ impl Level { root_folder, region_folder, }), - chunk_reader: Arc::new(Box::new(AnvilChunkReader::new())), - loaded_chunks: Arc::new(RwLock::new(HashMap::new())), - chunk_watchers: Arc::new(Mutex::new(HashMap::new())), + chunk_reader: Arc::new(AnvilChunkReader::new()), + loaded_chunks: Arc::new(DashMap::new()), + chunk_watchers: Arc::new(DashMap::new()), } } else { log::warn!( @@ -64,150 +64,152 @@ impl Level { Self { world_gen, save_file: None, - chunk_reader: Arc::new(Box::new(AnvilChunkReader::new())), - loaded_chunks: Arc::new(RwLock::new(HashMap::new())), - chunk_watchers: Arc::new(Mutex::new(HashMap::new())), + chunk_reader: Arc::new(AnvilChunkReader::new()), + loaded_chunks: Arc::new(DashMap::new()), + chunk_watchers: Arc::new(DashMap::new()), } } } pub fn get_block() {} + pub fn loaded_chunk_count(&self) -> usize { + self.loaded_chunks.len() + } + /// Marks chunks as "watched" by a unique player. When no players are watching a chunk, /// it is removed from memory. Should only be called on chunks the player was not watching /// before - pub async fn mark_chunk_as_newly_watched(&self, chunks: &[Vector2]) { - let mut watchers = self.chunk_watchers.lock().await; - for chunk in chunks { - match watchers.entry(*chunk) { - std::collections::hash_map::Entry::Occupied(mut occupied) => { + pub fn mark_chunks_as_newly_watched(&self, chunks: &[Vector2]) { + chunks + .par_iter() + .for_each(|chunk| match self.chunk_watchers.entry(*chunk) { + Entry::Occupied(mut occupied) => { let value = occupied.get_mut(); - *value = value.saturating_add(1); + if let Some(new_value) = value.checked_add(1) { + *value = new_value; + //log::debug!("Watch value for {:?}: {}", chunk, value); + } else { + log::error!("Watching overflow on chunk {:?}", chunk); + } } - std::collections::hash_map::Entry::Vacant(vacant) => { + Entry::Vacant(vacant) => { vacant.insert(1); } - } - } + }); } /// Marks chunks no longer "watched" by a unique player. When no players are watching a chunk, /// it is removed from memory. Should only be called on chunks the player was watching before pub async fn mark_chunk_as_not_watched_and_clean(&self, chunks: &[Vector2]) { - let dropped_chunks = { - let mut watchers = self.chunk_watchers.lock().await; - chunks - .iter() - .filter(|chunk| match watchers.entry(**chunk) { - std::collections::hash_map::Entry::Occupied(mut occupied) => { - let value = occupied.get_mut(); - *value = value.saturating_sub(1); - if *value == 0 { - occupied.remove_entry(); - true - } else { - false - } - } - std::collections::hash_map::Entry::Vacant(_) => { - log::error!( - "Marking a chunk as not watched, but was vacant! ({:?})", - chunk - ); + chunks + .par_iter() + .filter(|chunk| match self.chunk_watchers.entry(**chunk) { + Entry::Occupied(mut occupied) => { + let value = occupied.get_mut(); + *value = value.saturating_sub(1); + if *value == 0 { + occupied.remove_entry(); + true + } else { false } - }) - .collect::>() - }; - let mut loaded_chunks = self.loaded_chunks.write().await; - let dropped_chunk_data = dropped_chunks - .iter() - .filter_map(|chunk| { - //log::debug!("Unloading chunk {:?}", chunk); - loaded_chunks.remove_entry(*chunk) + } + Entry::Vacant(_) => { + log::error!( + "Marking a chunk as not watched, but was vacant! ({:?})", + chunk + ); + false + } }) - .collect(); - self.write_chunks(dropped_chunk_data); + .for_each(|chunk_pos| { + //log::debug!("Unloading {:?}", chunk_pos); + if let Some(data) = self.loaded_chunks.remove(chunk_pos) { + self.write_chunk(data); + }; + }); } - pub fn write_chunks(&self, _chunks_to_write: Vec<(Vector2, Arc>)>) { + pub fn write_chunk(&self, _chunk_to_write: (Vector2, Arc>)) { //TODO } + fn load_chunk_from_save( + chunk_reader: Arc, + save_file: SaveFile, + chunk_pos: Vector2, + ) -> Result>>, ChunkReadingError> { + match chunk_reader.read_chunk(&save_file, &chunk_pos) { + Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))), + Err( + ChunkReadingError::ChunkNotExist + | ChunkReadingError::ParsingError(ChunkParsingError::ChunkNotGenerated), + ) => { + // This chunk was not generated yet. + Ok(None) + } + Err(err) => Err(err), + } + } + /// Reads/Generates many chunks in a world /// MUST be called from a tokio runtime thread /// /// Note: The order of the output chunks will almost never be in the same order as the order of input chunks - pub fn fetch_chunks( + pub async fn fetch_chunks( &self, chunks: &[Vector2], channel: mpsc::Sender>>, ) { - for chunk in chunks { - { - let chunk_location = *chunk; - let channel = channel.clone(); - let loaded_chunks = self.loaded_chunks.clone(); - let chunk_reader = self.chunk_reader.clone(); - let save_file = self.save_file.clone(); - let world_gen = self.world_gen.clone(); - tokio::spawn(async move { - let loaded_chunks_read = loaded_chunks.read().await; - let possibly_loaded_chunk = loaded_chunks_read.get(&chunk_location).cloned(); - drop(loaded_chunks_read); - match possibly_loaded_chunk { - Some(chunk) => { - let chunk = chunk.clone(); - channel.send(chunk).await.unwrap(); - } - None => { - let chunk_data = match save_file { - Some(save_file) => { - match chunk_reader.read_chunk(&save_file, &chunk_location) { - Ok(data) => Ok(Arc::new(RwLock::new(data))), - Err( - ChunkReadingError::ChunkNotExist - | ChunkReadingError::ParsingError( - ChunkParsingError::ChunkNotGenerated, - ), - ) => { - // This chunk was not generated yet. - let chunk = Arc::new(RwLock::new( - world_gen.generate_chunk(chunk_location), - )); - let mut loaded_chunks = loaded_chunks.write().await; - loaded_chunks.insert(chunk_location, chunk.clone()); - drop(loaded_chunks); - Ok(chunk) - } - Err(err) => Err(err), + chunks.iter().for_each(|at| { + let channel = channel.clone(); + let loaded_chunks = self.loaded_chunks.clone(); + let chunk_reader = self.chunk_reader.clone(); + let save_file = self.save_file.clone(); + let world_gen = self.world_gen.clone(); + let chunk_pos = *at; + + tokio::spawn(async move { + let chunk = loaded_chunks + .get(&chunk_pos) + .map(|entry| entry.value().clone()) + .unwrap_or_else(|| { + let loaded_chunk = save_file + .and_then(|save_file| { + match Self::load_chunk_from_save(chunk_reader, save_file, chunk_pos) + { + Ok(chunk) => chunk, + Err(err) => { + log::error!( + "Failed to read chunk (regenerating) {:?}: {:?}", + chunk_pos, + err + ); + None } } - None => { - // There is no savefile yet -> generate the chunks - let chunk = Arc::new(RwLock::new( - world_gen.generate_chunk(chunk_location), - )); - - let mut loaded_chunks = loaded_chunks.write().await; - loaded_chunks.insert(chunk_location, chunk.clone()); - Ok(chunk) - } - }; - match chunk_data { - Ok(data) => channel.send(data).await.unwrap(), - Err(err) => { - log::warn!( - "Failed to read chunk {:?}: {:?}", - chunk_location, - err - ); - } - } + }) + .unwrap_or_else(|| { + Arc::new(RwLock::new(world_gen.generate_chunk(chunk_pos))) + }); + + if let Some(data) = loaded_chunks.get(&chunk_pos) { + // Another thread populated in between the previous check and now + // We did work, but this is basically like a cache miss, not much we + // can do about it + data.value().clone() + } else { + loaded_chunks.insert(chunk_pos, loaded_chunk.clone()); + loaded_chunk } - } - }); - } - } + }); + + let _ = channel + .send(chunk) + .await + .inspect_err(|err| log::error!("unable to send chunk to channel: {}", err)); + }); + }) } } diff --git a/pumpkin/src/client/mod.rs b/pumpkin/src/client/mod.rs index 4294bd7ef..af0ebe82a 100644 --- a/pumpkin/src/client/mod.rs +++ b/pumpkin/src/client/mod.rs @@ -175,7 +175,7 @@ impl Client { /// Send a Clientbound Packet to the Client pub async fn send_packet(&self, packet: &P) { - log::debug!("Sending packet with id {} to {}", P::PACKET_ID, self.id); + //log::debug!("Sending packet with id {} to {}", P::PACKET_ID, self.id); // assert!(!self.closed); let mut enc = self.enc.lock().await; if let Err(error) = enc.append_packet(packet) { @@ -201,11 +201,13 @@ impl Client { pub async fn try_send_packet(&self, packet: &P) -> Result<(), PacketError> { // assert!(!self.closed); + /* log::debug!( "Trying to send packet with id {} to {}", P::PACKET_ID, self.id ); + */ let mut enc = self.enc.lock().await; enc.append_packet(packet)?; @@ -392,7 +394,7 @@ impl Client { self.add_packet(packet).await; return true; } - Ok(None) => log::debug!("Waiting for more data to complete packet..."), + Ok(None) => (), //log::debug!("Waiting for more data to complete packet..."), Err(err) => log::warn!( "Failed to decode packet for id {}: {}", self.id, @@ -406,7 +408,7 @@ impl Client { let bytes_read = self.connection_reader.lock().await.read_buf(&mut buf).await; match bytes_read { Ok(cnt) => { - log::debug!("Read {} bytes", cnt); + //log::debug!("Read {} bytes", cnt); if cnt == 0 { self.close(); return false; @@ -427,7 +429,7 @@ impl Client { /// Kicks the Client with a reason depending on the connection state pub async fn kick(&self, reason: &str) { - log::info!("Kicking for id {} for {}", self.id, reason); + log::info!("Kicking id {} for {}", self.id, reason); match self.connection_state.load() { ConnectionState::Login => { self.try_send_packet(&CLoginDisconnect::new( diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index ab388f3cd..26b239b73 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -39,7 +39,7 @@ use super::Entity; use crate::{ client::{authentication::GameProfile, Client, PlayerConfig}, server::Server, - world::{player_chunker::chunk_section_from_pos, World}, + world::World, }; use crate::{error::PumpkinError, world::player_chunker::get_view_distance}; @@ -151,14 +151,27 @@ impl Player { pub async fn remove(&self) { self.living_entity.entity.world.remove_player(self).await; - let watched = chunk_section_from_pos(&self.living_entity.entity.block_pos.load()); + let watched = self.watched_section.load(); let view_distance = i32::from(get_view_distance(self).await); let cylindrical = Cylindrical::new(Vector2::new(watched.x, watched.z), view_distance); + let all_chunks = cylindrical.all_chunks_within(); + + log::debug!( + "Removing player id {}, unwatching {} chunks", + self.client.id, + all_chunks.len() + ); self.living_entity .entity .world - .mark_chunks_as_not_watched(&cylindrical.all_chunks_within()) + .mark_chunks_as_not_watched(&all_chunks) .await; + + log::debug!( + "Removed player id {} ({} chunks remain cached)", + self.client.id, + self.living_entity.entity.world.get_cached_chunk_len().await + ); } pub async fn tick(&self) { diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index f1d0e0023..be75dc7b7 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -24,8 +24,8 @@ use pumpkin_world::chunk::ChunkData; use pumpkin_world::coordinates::ChunkRelativeBlockCoordinates; use pumpkin_world::level::Level; use scoreboard::Scoreboard; -use tokio::sync::Mutex; use tokio::sync::{mpsc, RwLock}; +use tokio::sync::{mpsc::Receiver, Mutex}; pub mod scoreboard; @@ -283,10 +283,15 @@ impl World { pub async fn mark_chunks_as_watched(&self, chunks: &[Vector2]) { let level = self.level.lock().await; - level.mark_chunk_as_newly_watched(chunks).await; + level.mark_chunks_as_newly_watched(chunks); } - async fn spawn_world_chunks(&self, client: Arc, chunks: Vec>) { + pub async fn get_cached_chunk_len(&self) -> usize { + let level = self.level.lock().await; + level.loaded_chunk_count() + } + + fn spawn_world_chunks(&self, client: Arc, chunks: Vec>) { if client.closed.load(std::sync::atomic::Ordering::Relaxed) { log::info!( "The connection with {} has closed before world chunks were spawned", @@ -295,10 +300,10 @@ impl World { return; } let inst = std::time::Instant::now(); - let chunks = self.get_chunks(chunks).await; + let mut receiver = self.receive_chunks(chunks); tokio::spawn(async move { - for chunk_data in chunks { + while let Some(chunk_data) = receiver.recv().await { let chunk_data = chunk_data.read().await; let packet = CChunkData(&chunk_data); #[cfg(debug_assertions)] @@ -376,7 +381,7 @@ impl World { // Since we divide by 16 remnant can never exceed u8 let relative = ChunkRelativeBlockCoordinates::from(relative_coordinates); - let chunk = self.get_chunks(vec![chunk_coordinate]).await[0].clone(); + let chunk = self.receive_chunk(chunk_coordinate).await; chunk.write().await.blocks.set_block(relative, block_id); self.broadcast_packet_all(&CBlockUpdate::new( @@ -386,22 +391,25 @@ impl World { .await; } - pub async fn get_chunks(&self, chunks: Vec>) -> Vec>> { - let (sender, mut receive) = mpsc::channel(chunks.len()); + // Stream the chunks (don't collect them and then do stuff with them) + pub fn receive_chunks(&self, chunks: Vec>) -> Receiver>> { + let (sender, receive) = mpsc::channel(chunks.len()); { let level = self.level.clone(); - tokio::spawn(async move { level.lock().await.fetch_chunks(&chunks, sender) }); + tokio::spawn(async move { + let level = level.lock().await; + level.fetch_chunks(&chunks, sender).await; + }); } - tokio::spawn(async move { - let mut received = vec![]; + receive + } - while let Some(chunk) = receive.recv().await { - received.push(chunk); - } - received - }) - .await - .unwrap() + pub async fn receive_chunk(&self, chunk: Vector2) -> Arc> { + let mut receiver = self.receive_chunks(vec![chunk]); + receiver + .recv() + .await + .expect("Channel closed for unknown reason") } pub async fn break_block(&self, position: WorldPosition) { @@ -414,11 +422,8 @@ impl World { pub async fn get_block(&self, position: WorldPosition) -> BlockId { let (chunk, relative) = position.chunk_and_chunk_relative_position(); let relative = ChunkRelativeBlockCoordinates::from(relative); - self.get_chunks(vec![chunk]).await[0] - .clone() - .read() - .await - .blocks - .get_block(relative) + let chunk = self.receive_chunk(chunk).await; + let chunk = chunk.read().await; + chunk.blocks.get_block(relative) } } diff --git a/pumpkin/src/world/player_chunker.rs b/pumpkin/src/world/player_chunker.rs index 7842245f9..e42f520a5 100644 --- a/pumpkin/src/world/player_chunker.rs +++ b/pumpkin/src/world/player_chunker.rs @@ -23,9 +23,11 @@ pub async fn get_view_distance(player: &Player) -> i8 { pub async fn player_join(world: &World, player: Arc) { let new_watched = chunk_section_from_pos(&player.living_entity.entity.block_pos.load()); player.watched_section.store(new_watched); - let watched_section = new_watched; let chunk_pos = player.living_entity.entity.chunk_pos.load(); + assert_eq!(new_watched.x, chunk_pos.x); + assert_eq!(new_watched.z, chunk_pos.z); + log::debug!("Sending center chunk to {}", player.client.id); player .client @@ -42,66 +44,25 @@ pub async fn player_join(world: &World, player: Arc) { view_distance ); - let old_cylindrical = Cylindrical::new( - Vector2::new(watched_section.x, watched_section.z), - view_distance, - ); let new_cylindrical = Cylindrical::new(Vector2::new(chunk_pos.x, chunk_pos.z), view_distance); - let mut loading_chunks = Vec::new(); - let mut unloading_chunks = Vec::new(); - Cylindrical::for_each_changed_chunk( - old_cylindrical, - new_cylindrical, - |chunk_pos| { - loading_chunks.push(chunk_pos); - }, - |chunk_pos| { - unloading_chunks.push(chunk_pos); - // player - // .client - // .send_packet(&CUnloadChunk::new(chunk_pos.x, chunk_pos.z)); - }, - true, - ); - - log::debug!( - "{} added {} remove ({}) for {}", - loading_chunks.len(), - unloading_chunks.len(), - view_distance, - player.client.id - ); - - if !loading_chunks.is_empty() { - world.mark_chunks_as_watched(&loading_chunks).await; - world - .spawn_world_chunks(player.client.clone(), loading_chunks) - .await; - } + let loading_chunks = new_cylindrical.all_chunks_within(); - if !unloading_chunks.is_empty() { - world.mark_chunks_as_not_watched(&unloading_chunks).await; - for chunk in unloading_chunks { - if !player - .client - .closed - .load(std::sync::atomic::Ordering::Relaxed) - { - player - .client - .send_packet(&CUnloadChunk::new(chunk.x, chunk.z)) - .await; - } - } - } + world.mark_chunks_as_watched(&loading_chunks).await; + world.spawn_world_chunks(player.client.clone(), loading_chunks); } pub async fn update_position(player: &Player) { let entity = &player.living_entity.entity; let current_watched = player.watched_section.load(); let new_watched = chunk_section_from_pos(&entity.block_pos.load()); + player.watched_section.store(new_watched); + if current_watched != new_watched { + //log::debug!("changing chunks"); let chunk_pos = entity.chunk_pos.load(); + assert_eq!(new_watched.x, chunk_pos.x); + assert_eq!(new_watched.z, chunk_pos.z); + player .client .send_packet(&CCenterChunk { @@ -117,8 +78,6 @@ pub async fn update_position(player: &Player) { ); let new_cylindrical = Cylindrical::new(chunk_pos, view_distance); - player.watched_section.store(new_watched); - let mut loading_chunks = Vec::new(); let mut unloading_chunks = Vec::new(); Cylindrical::for_each_changed_chunk( @@ -130,14 +89,12 @@ pub async fn update_position(player: &Player) { |chunk_pos| { unloading_chunks.push(chunk_pos); }, - false, ); if !loading_chunks.is_empty() { entity.world.mark_chunks_as_watched(&loading_chunks).await; entity .world - .spawn_world_chunks(player.client.clone(), loading_chunks) - .await; + .spawn_world_chunks(player.client.clone(), loading_chunks); } if !unloading_chunks.is_empty() {