fix: reset player velocity state on client tick end

This commit is contained in:
Alexander Medvedev 2026-09-12 12:11:22 +02:00
parent 5bbbf4da84
commit 26baa1effd
24 changed files with 10120 additions and 399 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,92 +2,12 @@ use super::{ChunkLevel, ChunkPos, HashMapType, LevelChannel};
use crate::chunk_system::chunk_state::StagedChunkEnum; // Fixed path use crate::chunk_system::chunk_state::StagedChunkEnum; // Fixed path
use itertools::Itertools; use itertools::Itertools;
use std::cmp::min; use std::cmp::min;
use std::collections::VecDeque;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::fmt::Write; use std::fmt::Write;
use std::mem::swap; use std::mem::swap;
use std::sync::Arc; use std::sync::Arc;
use tracing::debug; use tracing::debug;
struct LevelCache(i32, i32, usize, [(i8, i8); 256 * 256]);
#[expect(clippy::large_stack_arrays)]
impl LevelCache {
const fn new() -> Self {
Self(0, 0, 0, [(0, 0); 256 * 256])
}
}
impl LevelCache {
fn clean(&mut self, pos: ChunkPos, level: i8) {
let dst = ChunkLoading::MAX_LEVEL - level + 1;
self.0 = pos.x - dst as i32;
self.1 = pos.y - dst as i32;
self.2 = (dst as usize) << 1 | 1;
self.3[..self.2 * self.2].fill((-127, -127));
}
fn get(&mut self, map: &ChunkLevel, pos: ChunkPos) -> i8 {
let dx = (pos.x - self.0) as usize;
let dy = (pos.y - self.1) as usize;
debug_assert!(pos.x >= self.0 && pos.y >= self.1);
let value = &mut self.3[dx * self.2 + dy];
if value.0 == -127 {
value.0 = *map.get(&pos).unwrap_or(&ChunkLoading::MAX_LEVEL);
value.1 = value.0;
}
value.1
}
fn set(&mut self, map: &ChunkLevel, pos: ChunkPos, level: i8) {
let dx = (pos.x - self.0) as usize;
let dy = (pos.y - self.1) as usize;
debug_assert!(pos.x >= self.0 && pos.y >= self.1);
let value = &mut self.3[dx * self.2 + dy];
if value.0 == -127 {
value.0 = *map.get(&pos).unwrap_or(&ChunkLoading::MAX_LEVEL);
}
value.1 = level;
}
fn write(
&self,
map: &mut ChunkLevel,
change: &mut HashMapType<ChunkPos, (StagedChunkEnum, StagedChunkEnum)>,
) {
for i in 0..self.2 {
for j in 0..self.2 {
let value = self.3[i * self.2 + j];
if value.0 != value.1 {
let pos = ChunkPos::new(i as i32 + self.0, j as i32 + self.1);
if value.1 == ChunkLoading::MAX_LEVEL {
map.remove(&pos);
} else {
map.insert(pos, value.1);
}
let value = (
StagedChunkEnum::level_to_stage(value.0),
StagedChunkEnum::level_to_stage(value.1),
);
if value.0 == value.1 {
continue;
}
match change.entry(pos) {
Entry::Occupied(mut entry) => {
let i = entry.get_mut();
debug_assert_eq!(i.1, value.0);
if i.0 == value.1 {
entry.remove();
} else {
i.1 = value.1;
}
}
Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
}
}
}
}
pub struct ChunkLoading { pub struct ChunkLoading {
pub is_priority_dirty: bool, pub is_priority_dirty: bool,
pub pos_level: ChunkLevel, pub pos_level: ChunkLevel,
@ -95,9 +15,6 @@ pub struct ChunkLoading {
pub ticket: HashMapType<ChunkPos, Vec<i8>>, // TODO lifetime & id pub ticket: HashMapType<ChunkPos, Vec<i8>>, // TODO lifetime & id
pub high_priority: Vec<ChunkPos>, pub high_priority: Vec<ChunkPos>,
pub sender: Arc<LevelChannel>, pub sender: Arc<LevelChannel>,
pub increase_update: VecDeque<(ChunkPos, i8)>,
pub decrease_update: VecDeque<(ChunkPos, i8)>,
cache: LevelCache,
} }
impl ChunkLoading { impl ChunkLoading {
@ -110,13 +27,13 @@ impl ChunkLoading {
let Some(&level) = levels.iter().min() else { let Some(&level) = levels.iter().min() else {
continue; continue;
}; };
let range = Self::MAX_LEVEL - level - 1; let range = (Self::MAX_LEVEL - level - 1).max(0) as u8;
for dx in -range..=range { for r in 0..=range {
for dy in -range..=range { let ring_level = level + r as i8;
for &(dx, dy) in pumpkin_data::chunk_view_lut::get_chebyshev_ring(r) {
let new_pos = ticket_pos.add_raw(dx as i32, dy as i32); let new_pos = ticket_pos.add_raw(dx as i32, dy as i32);
let level_from_source = level + dx.abs().max(dy.abs());
let i = temp.entry(new_pos).or_insert(Self::MAX_LEVEL); let i = temp.entry(new_pos).or_insert(Self::MAX_LEVEL);
*i = min(*i, level_from_source); *i = min(*i, ring_level);
} }
} }
} }
@ -205,9 +122,36 @@ impl ChunkLoading {
ticket: HashMapType::default(), ticket: HashMapType::default(),
high_priority: Vec::new(), high_priority: Vec::new(),
sender, sender,
increase_update: VecDeque::default(), }
decrease_update: VecDeque::default(), }
cache: LevelCache::new(),
fn record_change(
change: &mut HashMapType<ChunkPos, (StagedChunkEnum, StagedChunkEnum)>,
pos: ChunkPos,
old_level: i8,
new_level: i8,
) {
if old_level == new_level {
return;
}
let old_stage = StagedChunkEnum::level_to_stage(old_level);
let new_stage = StagedChunkEnum::level_to_stage(new_level);
if old_stage == new_stage {
return;
}
match change.entry(pos) {
Entry::Occupied(mut entry) => {
let i = entry.get_mut();
debug_assert_eq!(i.1, old_stage);
if i.0 == new_stage {
entry.remove();
} else {
i.1 = new_stage;
}
}
Entry::Vacant(entry) => {
entry.insert((old_stage, new_stage));
}
} }
} }
@ -230,88 +174,13 @@ impl ChunkLoading {
} }
} }
fn run_increase_update(&mut self) {
while let Some((pos, level)) = self.increase_update.pop_front() {
debug_assert!(level < Self::MAX_LEVEL);
if level > self.cache.get(&self.pos_level, pos) {
continue;
}
debug_assert_eq!(level, self.cache.get(&self.pos_level, pos));
let spread_level = level + 1;
if spread_level >= Self::MAX_LEVEL {
continue;
}
for dx in -1..2 {
for dy in -1..2 {
let new_pos = pos.add_raw(dx, dy);
if new_pos != pos {
self.check_then_push(new_pos, spread_level);
}
}
}
}
}
fn check_then_push(&mut self, pos: ChunkPos, level: i8) {
debug_assert!(level < Self::MAX_LEVEL);
let old = self.cache.get(&self.pos_level, pos);
if old <= level {
return;
}
self.cache.set(&self.pos_level, pos, level);
self.increase_update.push_back((pos, level));
}
fn run_decrease_update(&mut self, pos: ChunkPos, range: i32) {
while let Some((pos, level)) = self.decrease_update.pop_front() {
debug_assert!(level < Self::MAX_LEVEL);
let spread_level = level + 1;
for dx in -1..2 {
for dy in -1..2 {
let new_pos = pos.add_raw(dx, dy);
if new_pos == pos {
continue;
}
let new_pos_level = self.cache.get(&self.pos_level, new_pos);
if new_pos_level == Self::MAX_LEVEL {
continue;
}
debug_assert!(new_pos_level <= spread_level);
if new_pos_level == spread_level {
self.cache.set(&self.pos_level, new_pos, Self::MAX_LEVEL);
if spread_level < Self::MAX_LEVEL {
self.decrease_update.push_back((new_pos, spread_level));
}
} else {
self.increase_update.push_back((new_pos, new_pos_level));
}
}
}
}
for (ticket_pos, levels) in &self.ticket {
if (ticket_pos.x - pos.x).abs() <= range && (ticket_pos.y - pos.y).abs() <= range {
let Some(&level) = levels.iter().min() else {
continue;
};
debug_assert!(level < Self::MAX_LEVEL);
let old = self.cache.get(&self.pos_level, *ticket_pos);
if old <= level {
continue;
}
self.cache.set(&self.pos_level, *ticket_pos, level);
self.increase_update.push_back((*ticket_pos, level));
}
}
self.run_increase_update();
}
pub fn add_force_ticket(&mut self, pos: ChunkPos) { pub fn add_force_ticket(&mut self, pos: ChunkPos) {
// debug!("add force ticket at {pos:?}"); // debug!("add force ticket at {pos:?}");
self.high_priority.push(pos); self.high_priority.push(pos);
self.is_priority_dirty = true; self.is_priority_dirty = true;
self.add_ticket(pos, Self::FULL_CHUNK_LEVEL); self.add_ticket(pos, Self::FULL_CHUNK_LEVEL);
} }
pub fn remove_force_ticket(&mut self, pos: ChunkPos) { pub fn remove_force_ticket(&mut self, pos: ChunkPos) {
// debug!("remove force ticket at {pos:?}"); // debug!("remove force ticket at {pos:?}");
if let Some((index, _)) = self.high_priority.iter().find_position(|x| **x == pos) { if let Some((index, _)) = self.high_priority.iter().find_position(|x| **x == pos) {
@ -320,6 +189,7 @@ impl ChunkLoading {
self.is_priority_dirty = true; self.is_priority_dirty = true;
self.remove_ticket(pos, Self::FULL_CHUNK_LEVEL); self.remove_ticket(pos, Self::FULL_CHUNK_LEVEL);
} }
pub fn add_ticket(&mut self, pos: ChunkPos, level: i8) { pub fn add_ticket(&mut self, pos: ChunkPos, level: i8) {
// debug!("add ticket at {pos:?} level {level}"); // debug!("add ticket at {pos:?} level {level}");
debug_assert!(level < Self::MAX_LEVEL); debug_assert!(level < Self::MAX_LEVEL);
@ -336,15 +206,30 @@ impl ChunkLoading {
if old <= level { if old <= level {
return; return;
} }
self.cache.clean(pos, level);
self.cache.set(&self.pos_level, pos, level);
debug_assert!(self.increase_update.is_empty()); let max_range = (Self::MAX_LEVEL - level - 1).max(0) as u8;
self.increase_update.push_back((pos, level)); for r in 0..=max_range {
self.run_increase_update(); let ring_level = level + r as i8;
self.cache.write(&mut self.pos_level, &mut self.change); for &(dx, dy) in pumpkin_data::chunk_view_lut::get_chebyshev_ring(r) {
let p = pos.add_raw(dx as i32, dy as i32);
match self.pos_level.entry(p) {
Entry::Occupied(mut entry) => {
let cur = *entry.get();
if cur > ring_level {
*entry.get_mut() = ring_level;
Self::record_change(&mut self.change, p, cur, ring_level);
}
}
Entry::Vacant(entry) => {
entry.insert(ring_level);
Self::record_change(&mut self.change, p, Self::MAX_LEVEL, ring_level);
}
}
}
}
debug_assert!(self.debug_check_error()); debug_assert!(self.debug_check_error());
} }
pub fn remove_ticket(&mut self, pos: ChunkPos, level: i8) { pub fn remove_ticket(&mut self, pos: ChunkPos, level: i8) {
// debug!("remove ticket at {pos:?} level {level}"); // debug!("remove ticket at {pos:?} level {level}");
debug_assert!(level < Self::MAX_LEVEL); debug_assert!(level < Self::MAX_LEVEL);
@ -357,23 +242,55 @@ impl ChunkLoading {
return; return;
}; };
vec.remove(index); vec.remove(index);
match self.pos_level.entry(pos) { let source = *vec.iter().min().unwrap_or(&Self::MAX_LEVEL);
Entry::Occupied(entry) => { if vec.is_empty() {
let old_level = *entry.get(); self.ticket.remove(&pos);
let source = *vec.iter().min().unwrap_or(&Self::MAX_LEVEL); }
if vec.is_empty() {
self.ticket.remove(&pos); let old_level = *self.pos_level.get(&pos).unwrap_or(&Self::MAX_LEVEL);
} if level != old_level || source == level {
if level == old_level && source != level { debug_assert!(self.debug_check_error());
self.cache.clean(pos, old_level); return;
self.cache.set(&self.pos_level, pos, Self::MAX_LEVEL); }
debug_assert!(self.decrease_update.is_empty());
self.decrease_update.push_back((pos, level)); let range = (Self::MAX_LEVEL - old_level - 1).max(0) as u8;
self.run_decrease_update(pos, (Self::MAX_LEVEL - level - 1) as i32);
self.cache.write(&mut self.pos_level, &mut self.change); let nearby_tickets: Vec<(ChunkPos, i8)> = self
.ticket
.iter()
.filter_map(|(&t_pos, levels)| {
let &min_lvl = levels.iter().min()?;
let t_range = (Self::MAX_LEVEL - min_lvl - 1).max(0) as i32;
((t_pos.x - pos.x).abs() <= range as i32 + t_range
&& (t_pos.y - pos.y).abs() <= range as i32 + t_range)
.then_some((t_pos, min_lvl))
})
.collect();
for r in 0..=range {
let removed_contrib = old_level + r as i8;
for &(dx, dy) in pumpkin_data::chunk_view_lut::get_chebyshev_ring(r) {
let p = pos.add_raw(dx as i32, dy as i32);
let cur = *self.pos_level.get(&p).unwrap_or(&Self::MAX_LEVEL);
if cur == removed_contrib {
let mut new_level = Self::MAX_LEVEL;
for &(t_pos, t_level) in &nearby_tickets {
let dist = (p.x - t_pos.x).abs().max((p.y - t_pos.y).abs()) as i8;
let lvl = t_level + dist;
if lvl < new_level {
new_level = lvl;
}
}
if new_level != cur {
if new_level == Self::MAX_LEVEL {
self.pos_level.remove(&p);
} else {
self.pos_level.insert(p, new_level);
}
Self::record_change(&mut self.change, p, cur, new_level);
}
} }
} }
Entry::Vacant(_) => panic!(),
} }
debug_assert!(self.debug_check_error()); debug_assert!(self.debug_check_error());
} }

View File

@ -618,48 +618,41 @@ impl GenerationSchedule {
let stage = StagedChunkEnum::from(i); let stage = StagedChunkEnum::from(i);
let dependency = stage.get_direct_dependencies(); let dependency = stage.get_direct_dependencies();
let radius = stage.get_direct_radius(); let radius = stage.get_direct_radius();
for dx in -radius..=radius { self.unload_chunks.remove(&pos);
for dz in -radius..=radius { let req_stage = dependency[0];
let new_pos = pos.add_raw(dx, dz); let newly_created = Self::ensure_dependency_chain(
let req_stage = dependency[dx.abs().max(dz.abs()) as usize]; &mut self.graph,
self.unload_chunks.remove(&new_pos); &mut self.queue,
if new_pos == pos { &self.last_level,
let newly_created = Self::ensure_dependency_chain( &self.last_high_priority,
&mut self.graph, task,
&mut self.queue, pos,
&self.last_level, &mut holder,
&self.last_high_priority, req_stage,
task, );
new_pos, for (stage_i, &created) in newly_created.iter().enumerate() {
&mut holder, if created && stage_i > 1 {
req_stage, let stage = StagedChunkEnum::from(stage_i as u8);
); let dependency = stage.get_direct_dependencies();
for (stage_i, &created) in newly_created.iter().enumerate() { let radius = stage.get_direct_radius();
if created && stage_i > 1 { let cur_task = holder.tasks[stage_i];
let stage = StagedChunkEnum::from(stage_i as u8); for r in 1..=(radius as u8) {
let dependency = stage.get_direct_dependencies(); let neighbor_req = dependency[r as usize];
let radius = stage.get_direct_radius(); for &(ndx, ndz) in
let cur_task = holder.tasks[stage_i]; pumpkin_data::chunk_view_lut::get_chebyshev_ring(r)
for ndx in -radius..=radius { {
for ndz in -radius..=radius { let neighbor_pos = pos.add_raw(ndx as i32, ndz as i32);
if ndx == 0 && ndz == 0 { worklist.push_back((neighbor_pos, neighbor_req, cur_task));
continue;
}
let neighbor_pos = new_pos.add_raw(ndx, ndz);
let neighbor_req = dependency
[ndx.abs().max(ndz.abs()) as usize];
worklist.push_back((
neighbor_pos,
neighbor_req,
cur_task,
));
}
}
}
} }
continue;
} }
}
}
for r in 1..=(radius as u8) {
let req_stage = dependency[r as usize];
for &(dx, dz) in pumpkin_data::chunk_view_lut::get_chebyshev_ring(r) {
let new_pos = pos.add_raw(dx as i32, dz as i32);
self.unload_chunks.remove(&new_pos);
worklist.push_back((new_pos, req_stage, task)); worklist.push_back((new_pos, req_stage, task));
} }
} }
@ -693,13 +686,10 @@ impl GenerationSchedule {
let dependency = stage.get_direct_dependencies(); let dependency = stage.get_direct_dependencies();
let radius = stage.get_direct_radius(); let radius = stage.get_direct_radius();
let cur_task = holder.tasks[stage_i]; let cur_task = holder.tasks[stage_i];
for dx in -radius..=radius { for r in 1..=(radius as u8) {
for dz in -radius..=radius { let neighbor_req = dependency[r as usize];
if dx == 0 && dz == 0 { for &(dx, dz) in pumpkin_data::chunk_view_lut::get_chebyshev_ring(r) {
continue; let neighbor_pos = pos.add_raw(dx as i32, dz as i32);
}
let neighbor_pos = pos.add_raw(dx, dz);
let neighbor_req = dependency[dx.abs().max(dz.abs()) as usize];
worklist.push_back((neighbor_pos, neighbor_req, cur_task)); worklist.push_back((neighbor_pos, neighbor_req, cur_task));
} }
} }
@ -738,13 +728,11 @@ impl GenerationSchedule {
continue; continue;
} }
let dependencies = stage.get_direct_dependencies(); let dependencies = stage.get_direct_dependencies();
for dx in -radius..=radius { for r in 1..=(radius as u8) {
for dz in -radius..=radius { let req = dependencies[r as usize];
if dx == 0 && dz == 0 { for &(dx, dz) in pumpkin_data::chunk_view_lut::get_chebyshev_ring(r) {
continue; let neighbor = pos.add_raw(dx as i32, dz as i32);
} worklist.push((neighbor, req));
let neighbor = pos.add_raw(dx, dz);
worklist.push((neighbor, dependencies[dx.abs().max(dz.abs()) as usize]));
} }
} }
} }

View File

@ -36,30 +36,6 @@ impl Cylindrical {
(loading, unloading) (loading, unloading)
} }
#[must_use]
#[inline]
pub const fn left(&self) -> i32 {
self.center.x - self.view_distance.get() as i32 - 1
}
#[must_use]
#[inline]
pub const fn bottom(&self) -> i32 {
self.center.y - self.view_distance.get() as i32 - 1
}
#[must_use]
#[inline]
pub const fn right(&self) -> i32 {
self.center.x + self.view_distance.get() as i32 + 1
}
#[must_use]
#[inline]
pub const fn top(&self) -> i32 {
self.center.y + self.view_distance.get() as i32 + 1
}
#[must_use] #[must_use]
#[inline] #[inline]
pub const fn is_within_distance(&self, x: i32, z: i32) -> bool { pub const fn is_within_distance(&self, x: i32, z: i32) -> bool {
@ -111,17 +87,18 @@ mod test {
for view_distance in 1..=32 { for view_distance in 1..=32 {
cylinder.view_distance = NonZero::new(view_distance).unwrap(); cylinder.view_distance = NonZero::new(view_distance).unwrap();
let bound = view_distance as i32 + 1;
for chunk in cylinder.all_chunks_within() { for chunk in cylinder.all_chunks_within() {
assert!(chunk.x >= cylinder.left() && chunk.x <= cylinder.right()); assert!(chunk.x >= -bound && chunk.x <= bound);
assert!(chunk.y >= cylinder.bottom() && chunk.y <= cylinder.top()); assert!(chunk.y >= -bound && chunk.y <= bound);
} }
for x in (cylinder.left() - 2)..=(cylinder.right() + 2) { for x in (-bound - 2)..=(bound + 2) {
for z in (cylinder.bottom() - 2)..=(cylinder.top() + 2) { for z in (-bound - 2)..=(bound + 2) {
if cylinder.is_within_distance(x, z) { if cylinder.is_within_distance(x, z) {
assert!(x >= cylinder.left() && x <= cylinder.right()); assert!(x >= -bound && x <= bound);
assert!(z >= cylinder.bottom() && z <= cylinder.top()); assert!(z >= -bound && z <= bound);
} }
} }
} }

View File

@ -419,7 +419,7 @@ fn move_piston(
world.set_block_state( world.set_block_state(
&target_pos, &target_pos,
state, state,
BlockFlags::NOTIFY_ALL | BlockFlags::MOVED, BlockFlags::NOTIFY_LISTENERS | BlockFlags::MOVED,
); );
if let Some(moved_state) = moved_block_states.get(moved_blocks.len() - 1 - index) { if let Some(moved_state) = moved_block_states.get(moved_blocks.len() - 1 - index) {
@ -449,7 +449,7 @@ fn move_piston(
world.set_block_state( world.set_block_state(
&extended_pos, &extended_pos,
props.to_state_id(&Block::MOVING_PISTON), props.to_state_id(&Block::MOVING_PISTON),
BlockFlags::NOTIFY_ALL | BlockFlags::MOVED, BlockFlags::NOTIFY_LISTENERS | BlockFlags::MOVED,
); );
let mut props = PistonHeadLikeProperties::default(&Block::PISTON_HEAD); let mut props = PistonHeadLikeProperties::default(&Block::PISTON_HEAD);
props.facing = dir.to_facing(); props.facing = dir.to_facing();
@ -482,7 +482,9 @@ fn move_piston(
state.id, state.id,
BlockFlags::NOTIFY_LISTENERS, BlockFlags::NOTIFY_LISTENERS,
); );
world.update_neighbors(pos, None); world
.block_registry
.update_neighbors(world, pos, BlockFlags::NOTIFY_LISTENERS);
world.block_registry.prepare( world.block_registry.prepare(
world, world,
pos, pos,
@ -508,15 +510,22 @@ fn move_piston(
block_state.id, block_state.id,
BlockFlags::NOTIFY_LISTENERS, BlockFlags::NOTIFY_LISTENERS,
); );
world.update_neighbors(&broken_block_pos, None); world.update_neighbors_at(
&broken_block_pos,
Block::from_state_id(block_state.id),
None,
);
} }
} }
for &moved_block_pos in moved_blocks.iter().rev() { for (i, &moved_block_pos) in moved_blocks.iter().rev().enumerate() {
world.update_neighbors(&moved_block_pos, None); if let Some(old_state) = moved_block_states.get(moved_blocks.len() - 1 - i) {
let old_block = Block::from_state_id(old_state.id);
world.update_neighbors_at(&moved_block_pos, old_block, None);
}
} }
if extend { if extend {
world.update_neighbors(&extended_pos, None); world.update_neighbors_at(&extended_pos, &Block::PISTON_HEAD, None);
} }
true true

View File

@ -241,13 +241,35 @@ pub fn get_power_on_side(
) -> u8 { ) -> u8 {
let side_pos = pos.offset(side.to_block_direction().to_offset()); let side_pos = pos.offset(side.to_block_direction().to_offset());
let (side_block, side_state) = world.get_block_and_state(&side_pos); let (side_block, side_state) = world.get_block_and_state(&side_pos);
if !only_gate || is_diode(side_block) { let side_direction = side.to_block_direction();
world.block_registry.get_weak_redstone_power(
if only_gate {
if is_diode(side_block) {
world.block_registry.get_strong_redstone_power(
side_block,
world,
&side_pos,
side_state,
side_direction,
)
} else {
0
}
} else if side_block == &Block::REDSTONE_BLOCK {
15
} else if side_block == &Block::REDSTONE_WIRE {
let props = RedstoneWireLikeProperties::from_state_id(side_state.id);
props.power
} else if world
.block_registry
.emits_redstone_power(side_block, side_state, side_direction)
{
world.block_registry.get_strong_redstone_power(
side_block, side_block,
world, world,
&side_pos, &side_pos,
side_state, side_state,
side.to_block_direction(), side_direction,
) )
} else { } else {
0 0

View File

@ -1,23 +1,106 @@
use pumpkin_data::BlockStateId; use std::sync::Arc;
use pumpkin_data::block_properties::RailShapeStraight;
use pumpkin_data::entity::EntityType;
use pumpkin_data::{Block, BlockDirection, BlockStateId};
use pumpkin_macros::pumpkin_block; use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::tick::TickPriority;
use pumpkin_world::world::BlockFlags; use pumpkin_world::world::BlockFlags;
use crate::block::BlockBehaviour; use crate::block::BlockBehaviour;
use crate::block::CanPlaceAtArgs; use crate::block::CanPlaceAtArgs;
use crate::block::EmitsRedstonePowerArgs;
use crate::block::GetComparatorOutputArgs;
use crate::block::GetRedstonePowerArgs;
use crate::block::OnEntityCollisionArgs;
use crate::block::OnNeighborUpdateArgs; use crate::block::OnNeighborUpdateArgs;
use crate::block::OnPlaceArgs; use crate::block::OnPlaceArgs;
use crate::block::OnScheduledTickArgs;
use crate::block::OnStateReplacedArgs;
use crate::block::PlacedArgs; use crate::block::PlacedArgs;
use crate::entity::EntityBase; use crate::entity::EntityBase;
use crate::world::World;
use super::RailProperties; use super::RailProperties;
use super::common::{ use super::common::{
can_place_rail_at, compute_placed_rail_shape, rail_placement_is_valid, can_place_rail_at, compute_placed_rail_shape, rail_placement_is_valid,
update_flanking_rails_shape, update_flanking_rails_shape,
}; };
use pumpkin_data::block_properties::DetectorRailProperties;
#[pumpkin_block("minecraft:detector_rail")] #[pumpkin_block("minecraft:detector_rail")]
pub struct DetectorRailBlock; pub struct DetectorRailBlock;
fn is_minecart(entity_type: &EntityType) -> bool {
entity_type == &EntityType::MINECART
|| entity_type == &EntityType::CHEST_MINECART
|| entity_type == &EntityType::COMMAND_BLOCK_MINECART
|| entity_type == &EntityType::FURNACE_MINECART
|| entity_type == &EntityType::HOPPER_MINECART
|| entity_type == &EntityType::SPAWNER_MINECART
|| entity_type == &EntityType::TNT_MINECART
}
fn update_power_to_connected(world: &Arc<World>, pos: &BlockPos, shape: RailShapeStraight) {
let (c1, c2) = match shape {
RailShapeStraight::NorthSouth => (pos.north(), pos.south()),
RailShapeStraight::EastWest => (pos.east(), pos.west()),
RailShapeStraight::AscendingEast => (pos.west(), pos.east().up()),
RailShapeStraight::AscendingWest => (pos.east(), pos.west().up()),
RailShapeStraight::AscendingNorth => (pos.south(), pos.north().up()),
RailShapeStraight::AscendingSouth => (pos.north(), pos.south().up()),
};
world.update_neighbors(&c1, None);
world.update_neighbors(&c2, None);
}
impl DetectorRailBlock {
pub fn check_pressed(
&self,
world: &Arc<World>,
pos: &BlockPos,
block: &Block,
state_id: BlockStateId,
) {
if !can_place_rail_at(world.as_ref(), pos) {
return;
}
// Search bounding box matching vanilla AABB (0.2 to 0.8 on X and Z, 0.0 to 0.8 on Y)
let search_box = BoundingBox::new_array([0.2, 0.0, 0.2], [0.8, 0.8, 0.8]).at_pos(*pos);
let entities = world.get_entities_at_box(&search_box);
let has_minecart = entities
.iter()
.any(|e| is_minecart(e.get_entity().entity_type));
let mut props = DetectorRailProperties::from_state_id(state_id);
let was_pressed = props.powered;
if has_minecart && !was_pressed {
props.powered = true;
let new_state_id = props.to_state_id(block);
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_ALL);
update_power_to_connected(world, pos, props.shape);
world.update_neighbors(pos, None);
world.update_neighbors(&pos.down(), None);
} else if !has_minecart && was_pressed {
props.powered = false;
let new_state_id = props.to_state_id(block);
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_ALL);
update_power_to_connected(world, pos, props.shape);
world.update_neighbors(pos, None);
world.update_neighbors(&pos.down(), None);
}
if has_minecart {
world.schedule_block_tick(block, *pos, 20, TickPriority::Normal);
}
}
}
impl BlockBehaviour for DetectorRailBlock { impl BlockBehaviour for DetectorRailBlock {
fn on_place(&self, args: OnPlaceArgs<'_>) -> BlockStateId { fn on_place(&self, args: OnPlaceArgs<'_>) -> BlockStateId {
let mut rail_props = RailProperties::default(args.block); let mut rail_props = RailProperties::default(args.block);
@ -35,6 +118,7 @@ impl BlockBehaviour for DetectorRailBlock {
fn placed(&self, args: PlacedArgs<'_>) { fn placed(&self, args: PlacedArgs<'_>) {
update_flanking_rails_shape(args.world, args.block, args.state_id, args.position); update_flanking_rails_shape(args.world, args.block, args.state_id, args.position);
self.check_pressed(args.world, args.position, args.block, args.state_id);
} }
fn on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>) { fn on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>) {
@ -44,7 +128,55 @@ impl BlockBehaviour for DetectorRailBlock {
} }
} }
fn on_entity_collision(&self, args: OnEntityCollisionArgs<'_>) {
let props = DetectorRailProperties::from_state_id(args.state.id);
if !props.powered {
self.check_pressed(args.world, args.position, args.block, args.state.id);
}
}
fn on_scheduled_tick(&self, args: OnScheduledTickArgs<'_>) {
let state_id = args.world.get_block_state_id(args.position);
let props = DetectorRailProperties::from_state_id(state_id);
if props.powered {
self.check_pressed(args.world, args.position, args.block, state_id);
}
}
fn on_state_replaced(&self, args: OnStateReplacedArgs<'_>) {
if !args.moved {
let props = DetectorRailProperties::from_state_id(args.old_state_id);
if props.powered {
args.world.update_neighbors(args.position, None);
args.world.update_neighbors(&args.position.down(), None);
}
}
}
fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool {
can_place_rail_at(args.block_accessor, args.position) can_place_rail_at(args.block_accessor, args.position)
} }
fn emits_redstone_power(&self, _args: EmitsRedstonePowerArgs<'_>) -> bool {
true
}
fn get_weak_redstone_power(&self, args: GetRedstonePowerArgs<'_>) -> u8 {
let props = DetectorRailProperties::from_state_id(args.state.id);
if props.powered { 15 } else { 0 }
}
fn get_strong_redstone_power(&self, args: GetRedstonePowerArgs<'_>) -> u8 {
let props = DetectorRailProperties::from_state_id(args.state.id);
if props.powered && args.direction == BlockDirection::Up {
15
} else {
0
}
}
fn get_comparator_output(&self, args: GetComparatorOutputArgs<'_>) -> Option<u8> {
let props = DetectorRailProperties::from_state_id(args.state.id);
props.powered.then_some(0)
}
} }

View File

@ -1748,6 +1748,20 @@ impl LivingEntity {
return; return;
} }
// Vanilla parity: the fall_damage gamerule only affects players.
if caller.get_player().is_some()
&& !self
.entity
.world
.load()
.level_info
.load()
.game_rules
.fall_damage
{
return;
}
if fall_distance >= 2.0 if fall_distance >= 2.0
&& let Some(player) = caller.get_player() && let Some(player) = caller.get_player()
{ {

View File

@ -4,7 +4,6 @@ use crate::{
server::Server, server::Server,
world::{ world::{
World, World,
chunker::is_within_view_distance,
portal::{NetherPortal, PortalProcessor, PortalType, SourcePortalInfo}, portal::{NetherPortal, PortalProcessor, PortalType, SourcePortalInfo},
}, },
}; };
@ -2625,7 +2624,9 @@ impl Entity {
&& self.age.load(Ordering::Relaxed) % Self::FREEZE_DAMAGE_INTERVAL == 0 && self.age.load(Ordering::Relaxed) % Self::FREEZE_DAMAGE_INTERVAL == 0
{ {
let world = self.world.load_full(); let world = self.world.load_full();
if let Some(entity) = world.get_entity_by_id(self.entity_id) { if world.level_info.load().game_rules.freeze_damage
&& let Some(entity) = world.get_entity_by_id(self.entity_id)
{
entity.damage(entity.as_ref(), 1.0, DamageType::FREEZE); entity.damage(entity.as_ref(), 1.0, DamageType::FREEZE);
} }
} }
@ -3048,10 +3049,10 @@ impl Entity {
} else { } else {
let chunk_pos = self.chunk_pos.load(); let chunk_pos = self.chunk_pos.load();
for player in players.iter() { for player in players.iter() {
let center = player.get_entity().chunk_pos.load(); if player
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32; .watched_section
.load()
if is_within_view_distance(chunk_pos, center, view_distance) .is_within_distance(chunk_pos.x, chunk_pos.y)
&& let ClientPlatform::Bedrock(client) = player.client.as_ref() && let ClientPlatform::Bedrock(client) = player.client.as_ref()
{ {
bedrock_recipients.push(client); bedrock_recipients.push(client);
@ -3097,10 +3098,10 @@ impl Entity {
} else { } else {
let chunk_pos = self.chunk_pos.load(); let chunk_pos = self.chunk_pos.load();
for player in players.iter() { for player in players.iter() {
let center = player.get_entity().chunk_pos.load(); if player
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32; .watched_section
.load()
if is_within_view_distance(chunk_pos, center, view_distance) .is_within_distance(chunk_pos.x, chunk_pos.y)
&& let ClientPlatform::Java(_) = player.client.as_ref() && let ClientPlatform::Java(_) = player.client.as_ref()
{ {
java_recipients.push(player); java_recipients.push(player);

View File

@ -2567,11 +2567,9 @@ impl Player {
if let Ok(listener) = self.chunk_listener.try_lock() if let Ok(listener) = self.chunk_listener.try_lock()
&& let Ok(mut sender) = self.chunk_sender.try_lock() && let Ok(mut sender) = self.chunk_sender.try_lock()
{ {
let center = self.get_entity().chunk_pos.load(); let watched = self.watched_section.load();
let view_dist =
std::num::NonZeroI32::from(self.watched_section.load().view_distance).get();
while let Ok((pos, _)) = listener.try_recv() { while let Ok((pos, _)) = listener.try_recv() {
if (pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_dist { if watched.is_within_distance(pos.x, pos.y) {
sender.enqueue_chunk(pos); sender.enqueue_chunk(pos);
} }
} }
@ -2585,8 +2583,9 @@ impl Player {
ClientPlatform::Bedrock(_) => JavaMinecraftVersion::V_1_20_2, ClientPlatform::Bedrock(_) => JavaMinecraftVersion::V_1_20_2,
}; };
let view_distance = self.watched_section.load().view_distance;
let prepared_batch = self.chunk_sender.try_lock().ok().and_then(|mut sender| { let prepared_batch = self.chunk_sender.try_lock().ok().and_then(|mut sender| {
sender.prepare_batch(&world.level, player_chunk, epoch, version) sender.prepare_batch(&world.level, player_chunk, view_distance, epoch, version)
}); });
let total_sent_chunks = prepared_batch.map_or_else( let total_sent_chunks = prepared_batch.map_or_else(

View File

@ -184,6 +184,22 @@ impl EntityBase for MinecartEntity {
if is_activator_rail && let MinecartKind::Hopper(minecart) = &self.kind { if is_activator_rail && let MinecartKind::Hopper(minecart) = &self.kind {
minecart.set_enabled(!powered); minecart.set_enabled(!powered);
} }
} else if block.id == Block::DETECTOR_RAIL.id
&& let Some(server) = world.server.upgrade()
{
world.block_registry.on_entity_collision(
block,
&world,
self,
&block_pos,
world.get_block_state(&block_pos),
&server,
);
}
if is_powered_rail || is_activator_rail {
let props = PoweredRailLikeProperties::from_state_id(state_id);
let powered = props.powered;
if powered { if powered {
if is_powered_rail { if is_powered_rail {

View File

@ -1,6 +1,7 @@
use bytes::Bytes; use bytes::Bytes;
use rayon::prelude::*; use rayon::prelude::*;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use std::num::NonZero;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use crate::net::java::chunk_data::{CChunkData, ChunkLightExt}; use crate::net::java::chunk_data::{CChunkData, ChunkLightExt};
@ -13,6 +14,7 @@ use pumpkin_protocol::{ClientPacket, MultiVersionJavaPacket};
use pumpkin_util::math::vector2::Vector2; use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::version::JavaMinecraftVersion; use pumpkin_util::version::JavaMinecraftVersion;
use pumpkin_world::chunk::ChunkData; use pumpkin_world::chunk::ChunkData;
use pumpkin_world::cylindrical_chunk_iterator::Cylindrical;
use pumpkin_world::level::{Level, SyncChunk}; use pumpkin_world::level::{Level, SyncChunk};
use crate::net::ClientPlatform; use crate::net::ClientPlatform;
@ -127,27 +129,68 @@ impl ChunkSender {
} }
} }
fn collect_sorted_candidates(&self, level: &Level, center: Vector2<i32>) -> Vec<PreparedChunk> { fn collect_sorted_candidates(
&self,
level: &Level,
center: Vector2<i32>,
view_distance: NonZero<u8>,
) -> Vec<PreparedChunk> {
let quota_limit = self.send_quota.floor() as usize; let quota_limit = self.send_quota.floor() as usize;
let mut sorted: Vec<Vector2<i32>> = self.pending_chunks.iter().copied().collect();
sorted.sort_by_key(|pos| {
let dx = (pos.x - center.x).unsigned_abs() as u64;
let dz = (pos.y - center.y).unsigned_abs() as u64;
dx * dx + dz * dz
});
let mut ready = Vec::with_capacity(quota_limit); let mut ready = Vec::with_capacity(quota_limit);
for pos in sorted {
if ready.len() >= quota_limit { // If pending_chunks is small, sorting it directly avoids scanning offsets.
break; if self.pending_chunks.len() <= 16 {
let mut sorted: Vec<Vector2<i32>> = self.pending_chunks.iter().copied().collect();
sorted.sort_unstable_by_key(|pos| {
let dx = (pos.x - center.x).unsigned_abs() as u64;
let dz = (pos.y - center.y).unsigned_abs() as u64;
dx * dx + dz * dz
});
for pos in sorted {
if ready.len() >= quota_limit {
break;
}
if let Some(chunk) = level.loaded_chunks.get(&pos) {
ready.push(PreparedChunk {
position: pos,
chunk: chunk.value().clone(),
});
}
}
} else {
// Re-use precompiled cylindrical chunk view LUT which is already sorted center-outward.
let offsets = Cylindrical::get_offsets(view_distance.get());
for &(dx, dy) in offsets {
if ready.len() >= quota_limit {
break;
}
let pos = Vector2::new(center.x + i32::from(dx), center.y + i32::from(dy));
if self.pending_chunks.contains(&pos)
&& let Some(chunk) = level.loaded_chunks.get(&pos)
{
ready.push(PreparedChunk {
position: pos,
chunk: chunk.value().clone(),
});
}
} }
if let Some(chunk) = level.loaded_chunks.get(&pos) { // Fallback for any pending chunks outside the precomputed table
ready.push(PreparedChunk { if ready.is_empty() {
position: pos, for &pos in &self.pending_chunks {
chunk: chunk.value().clone(), if ready.len() >= quota_limit {
}); break;
}
if let Some(chunk) = level.loaded_chunks.get(&pos) {
ready.push(PreparedChunk {
position: pos,
chunk: chunk.value().clone(),
});
}
}
} }
} }
@ -158,6 +201,7 @@ impl ChunkSender {
&mut self, &mut self,
level: &Level, level: &Level,
player_chunk: Vector2<i32>, player_chunk: Vector2<i32>,
view_distance: NonZero<u8>,
epoch: u32, epoch: u32,
version: JavaMinecraftVersion, version: JavaMinecraftVersion,
) -> Option<PreparedBatch> { ) -> Option<PreparedBatch> {
@ -173,7 +217,7 @@ impl ChunkSender {
return None; return None;
} }
let candidates = self.collect_sorted_candidates(level, player_chunk); let candidates = self.collect_sorted_candidates(level, player_chunk, view_distance);
if candidates.is_empty() { if candidates.is_empty() {
return None; return None;
} }

View File

@ -116,6 +116,10 @@ pub struct JavaClient {
/// ///
/// Whether we are waiting for a response after sending a keep alive packet. /// Whether we are waiting for a response after sending a keep alive packet.
pub wait_for_keep_alive: AtomicBool, pub wait_for_keep_alive: AtomicBool,
/// Set to `true` when any movement packet is received this tick.
/// On `SClientTickEnd` (≥1.21.4), if still `false`, the player's known
/// movement is zeroed (they stood still). Matches vanilla's `receivedMovementThisTick`.
pub received_movement_this_tick: AtomicBool,
/// The keep alive packet payload we send. The client should respond with the same id. /// The keep alive packet payload we send. The client should respond with the same id.
pub keep_alive_id: AtomicCell<i64>, pub keep_alive_id: AtomicCell<i64>,
/// The last time we sent a keep alive packet. /// The last time we sent a keep alive packet.
@ -254,6 +258,7 @@ impl JavaClient {
brand: ArcSwap::from_pointee(pending.brand), brand: ArcSwap::from_pointee(pending.brand),
player: ArcSwap::from_pointee(None), player: ArcSwap::from_pointee(None),
wait_for_keep_alive: AtomicBool::new(false), wait_for_keep_alive: AtomicBool::new(false),
received_movement_this_tick: AtomicBool::new(false),
keep_alive_id: AtomicCell::new(0), keep_alive_id: AtomicCell::new(0),
last_keep_alive_time: AtomicCell::new(Instant::now()), last_keep_alive_time: AtomicCell::new(Instant::now()),
last_packet_time: AtomicCell::new(Instant::now()), last_packet_time: AtomicCell::new(Instant::now()),
@ -1018,7 +1023,7 @@ impl JavaClient {
); );
} }
id if id == SClientTickEnd::to_id(version) => { id if id == SClientTickEnd::to_id(version) => {
// TODO self.handle_client_tick_end(player);
} }
id if id == STestInstanceBlockAction::to_id(version) => { id if id == STestInstanceBlockAction::to_id(version) => {
self.handle_test_instance_block_action( self.handle_test_instance_block_action(

View File

@ -0,0 +1,19 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_client_tick_end(&self, player: &Player) {
// If no movement packet arrived this tick the player is standing still —
// zero out the known movement so consumers (e.g. riptide speed) see 0.
// Matches vanilla ServerGamePacketListenerImpl#handleClientTickEnd.
if !self
.received_movement_this_tick
.swap(false, Ordering::Relaxed)
{
player
.get_entity()
.movement
.store(Vector3::new(0.0, 0.0, 0.0));
}
}
}

View File

@ -223,6 +223,7 @@ pub mod chat_message;
pub mod chunk_batch; pub mod chunk_batch;
pub mod client_command; pub mod client_command;
pub mod client_information; pub mod client_information;
pub mod client_tick_end;
pub mod close_container; pub mod close_container;
pub mod command_suggestion; pub mod command_suggestion;
pub mod configuration_acknowledged; pub mod configuration_acknowledged;

View File

@ -3,6 +3,9 @@ use super::*;
impl JavaClient { impl JavaClient {
pub fn handle_move_vehicle(&self, player: &Arc<Player>, packet: &SMoveVehicle) { pub fn handle_move_vehicle(&self, player: &Arc<Player>, packet: &SMoveVehicle) {
// A movement packet was received this tick — tracked for SClientTickEnd zeroing.
self.received_movement_this_tick
.store(true, Ordering::Relaxed);
let entity = player.get_entity(); let entity = player.get_entity();
let last_pos = entity.pos.load(); let last_pos = entity.pos.load();
let pos = Vector3::new(packet.x, packet.y, packet.z); let pos = Vector3::new(packet.x, packet.y, packet.z);

View File

@ -3,6 +3,9 @@ use super::*;
impl JavaClient { impl JavaClient {
pub fn handle_player_ground(&self, player: &Player, ground: &SSetPlayerGround) { pub fn handle_player_ground(&self, player: &Player, ground: &SSetPlayerGround) {
// A movement packet was received this tick — tracked for SClientTickEnd zeroing.
self.received_movement_this_tick
.store(true, Ordering::Relaxed);
player player
.living_entity .living_entity
.entity .entity

View File

@ -52,6 +52,9 @@ impl JavaClient {
if !player.has_client_loaded() { if !player.has_client_loaded() {
return; return;
} }
// A movement packet was received this tick — tracked for SClientTickEnd zeroing.
self.received_movement_this_tick
.store(true, Ordering::Relaxed);
if player.get_entity().has_vehicle() { if player.get_entity().has_vehicle() {
return; return;
} }
@ -190,6 +193,9 @@ impl JavaClient {
if !player.has_client_loaded() { if !player.has_client_loaded() {
return; return;
} }
// A movement packet was received this tick — tracked for SClientTickEnd zeroing.
self.received_movement_this_tick
.store(true, Ordering::Relaxed);
if player.get_entity().has_vehicle() { if player.get_entity().has_vehicle() {
return; return;
} }

View File

@ -6,6 +6,9 @@ impl JavaClient {
if !player.has_client_loaded() { if !player.has_client_loaded() {
return; return;
} }
// A movement packet was received this tick — tracked for SClientTickEnd zeroing.
self.received_movement_this_tick
.store(true, Ordering::Relaxed);
if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() { if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() {
self.try_kick(&TextComponent::translate_cross( self.try_kick(&TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT, translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT,

View File

@ -49,18 +49,24 @@ impl ActiveChunkTracker {
active_chunks: &mut FxHashSet<Vector2<i32>>, active_chunks: &mut FxHashSet<Vector2<i32>>,
newly_active: &mut Vec<Vector2<i32>>, newly_active: &mut Vec<Vector2<i32>>,
) { ) {
for dx in -area.simulation_distance..=area.simulation_distance { let offsets = pumpkin_data::chunk_view_lut::get_chebyshev_square(
for dz in -area.simulation_distance..=area.simulation_distance { area.simulation_distance.max(0) as u8,
self.add_chunk(area.center.add_raw(dx, dz), active_chunks, newly_active); );
} for &(dx, dz) in offsets {
self.add_chunk(
area.center.add_raw(dx as i32, dz as i32),
active_chunks,
newly_active,
);
} }
} }
fn remove_area(&mut self, area: ActivePlayerArea, active_chunks: &mut FxHashSet<Vector2<i32>>) { fn remove_area(&mut self, area: ActivePlayerArea, active_chunks: &mut FxHashSet<Vector2<i32>>) {
for dx in -area.simulation_distance..=area.simulation_distance { let offsets = pumpkin_data::chunk_view_lut::get_chebyshev_square(
for dz in -area.simulation_distance..=area.simulation_distance { area.simulation_distance.max(0) as u8,
self.remove_chunk(area.center.add_raw(dx, dz), active_chunks); );
} for &(dx, dz) in offsets {
self.remove_chunk(area.center.add_raw(dx as i32, dz as i32), active_chunks);
} }
} }
@ -75,24 +81,26 @@ impl ActiveChunkTracker {
match previous { match previous {
None => self.add_area(area, active_chunks, newly_active), None => self.add_area(area, active_chunks, newly_active),
Some(previous) if previous != area => { Some(previous) if previous != area => {
for dx in -previous.simulation_distance..=previous.simulation_distance { let prev_offsets = pumpkin_data::chunk_view_lut::get_chebyshev_square(
for dz in -previous.simulation_distance..=previous.simulation_distance { previous.simulation_distance.max(0) as u8,
let pos = previous.center.add_raw(dx, dz); );
if (pos.x - area.center.x).abs() > area.simulation_distance for &(dx, dz) in prev_offsets {
|| (pos.y - area.center.y).abs() > area.simulation_distance let pos = previous.center.add_raw(dx as i32, dz as i32);
{ if (pos.x - area.center.x).abs() > area.simulation_distance
self.remove_chunk(pos, active_chunks); || (pos.y - area.center.y).abs() > area.simulation_distance
} {
self.remove_chunk(pos, active_chunks);
} }
} }
for dx in -area.simulation_distance..=area.simulation_distance { let curr_offsets = pumpkin_data::chunk_view_lut::get_chebyshev_square(
for dz in -area.simulation_distance..=area.simulation_distance { area.simulation_distance.max(0) as u8,
let pos = area.center.add_raw(dx, dz); );
if (pos.x - previous.center.x).abs() > previous.simulation_distance for &(dx, dz) in curr_offsets {
|| (pos.y - previous.center.y).abs() > previous.simulation_distance let pos = area.center.add_raw(dx as i32, dz as i32);
{ if (pos.x - previous.center.x).abs() > previous.simulation_distance
self.add_chunk(pos, active_chunks, newly_active); || (pos.y - previous.center.y).abs() > previous.simulation_distance
} {
self.add_chunk(pos, active_chunks, newly_active);
} }
} }
} }

View File

@ -28,16 +28,15 @@ pub fn get_view_distance(player: &Player) -> NonZero<u8> {
.clamp(fallback, max_view_distance) .clamp(fallback, max_view_distance)
} }
// Checks if the target chunk is within the view distance // Checks if the target chunk is within Chebyshev distance (L_infinity) of the center chunk.
// of the center chunk. Uses Chebyshev distance.
#[must_use] #[must_use]
#[inline] #[inline]
pub fn is_within_view_distance( pub fn is_within_chebyshev_distance(
center: Vector2<i32>, center: Vector2<i32>,
target: Vector2<i32>, target: Vector2<i32>,
view_distance: i32, distance: i32,
) -> bool { ) -> bool {
(target.x - center.x).abs().max((target.y - center.y).abs()) <= view_distance (target.x - center.x).abs().max((target.y - center.y).abs()) <= distance
} }
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
@ -99,7 +98,7 @@ pub fn update_position(player: &Arc<Player>) {
) )
}); });
let new_sim_level = (!is_spectator).then(|| { let new_sim_level = (!is_spectator || spectators_generate_chunks).then(|| {
let sim_dist = world.server.upgrade().map_or(10, |s| { let sim_dist = world.server.upgrade().map_or(10, |s| {
s.advanced_config.networking.java.simulation_distance.get() s.advanced_config.networking.java.simulation_distance.get()
}); });
@ -173,7 +172,7 @@ pub fn update_position(player: &Arc<Player>) {
} }
if !loading_chunks.is_empty() { if !loading_chunks.is_empty() {
world.spawn_world_entity_chunks(player.clone(), loading_chunks, new_chunk_center); world.spawn_world_entity_chunks(player.clone(), loading_chunks);
} }
world.entity_tracker.update_player_position(player, &world); world.entity_tracker.update_player_position(player, &world);
} }

View File

@ -24,7 +24,7 @@ use crate::entity::player::Player;
use crate::net::ClientPlatform; use crate::net::ClientPlatform;
use crate::net::java::JavaClient; use crate::net::java::JavaClient;
use crate::world::World; use crate::world::World;
use crate::world::chunker::{get_view_distance, is_within_view_distance}; use crate::world::chunker::get_view_distance;
pub struct TrackedEntity { pub struct TrackedEntity {
pub entity: Arc<dyn EntityBase>, pub entity: Arc<dyn EntityBase>,
@ -113,8 +113,10 @@ impl TrackedEntity {
let range_sq = visible_range_blocks * visible_range_blocks; let range_sq = visible_range_blocks * visible_range_blocks;
let entity_chunk = self.entity.get_entity().chunk_pos.load(); let entity_chunk = self.entity.get_entity().chunk_pos.load();
let player_chunk = player_entity.chunk_pos.load(); let in_view = player
let in_view = is_within_view_distance(entity_chunk, player_chunk, player_vd); .watched_section
.load()
.is_within_distance(entity_chunk.x, entity_chunk.y);
let is_visible = dist_sq <= range_sq && self.broadcast_to_player(player) && in_view; let is_visible = dist_sq <= range_sq && self.broadcast_to_player(player) && in_view;

View File

@ -31,8 +31,7 @@ pub mod time;
pub mod villager_poi; pub mod villager_poi;
use crate::block::RandomTickArgs; use crate::block::RandomTickArgs;
use crate::world::chunker::get_view_distance; use crate::world::chunker::is_within_chebyshev_distance;
use crate::world::chunker::is_within_view_distance;
use crate::{block::BlockEvent, entity::item::ItemEntity}; use crate::{block::BlockEvent, entity::item::ItemEntity};
use crate::{ use crate::{
block::{ block::{
@ -447,8 +446,11 @@ impl World {
let mut newly_active = Vec::new(); let mut newly_active = Vec::new();
let mut current_players = FxHashSet::default(); let mut current_players = FxHashSet::default();
let spectators_generate_chunks =
self.level_info.load().game_rules.spectators_generate_chunks;
for player in players.iter() { for player in players.iter() {
if player.is_spectator() { if player.is_spectator() && !spectators_generate_chunks {
continue; continue;
} }
let id = player.gameprofile.id; let id = player.gameprofile.id;
@ -1351,7 +1353,7 @@ impl World {
let chunk_pos = BlockPos::floored_v(*position).chunk_position(); let chunk_pos = BlockPos::floored_v(*position).chunk_position();
for player in self.players.load().iter() { for player in self.players.load().iter() {
if is_within_view_distance(chunk_pos, player.get_entity().chunk_pos.load(), 1) if is_within_chebyshev_distance(chunk_pos, player.get_entity().chunk_pos.load(), 1)
&& let ClientPlatform::Bedrock(client) = player.client.as_ref() && let ClientPlatform::Bedrock(client) = player.client.as_ref()
&& let Ok(data) = client.serialize_packet(&packet) && let Ok(data) = client.serialize_packet(&packet)
{ {
@ -1389,7 +1391,7 @@ impl World {
let recipients = players.iter().filter(|p| { let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load(); let center = p.get_entity().chunk_pos.load();
// If the sound reaches their chunk, send it! // If the sound reaches their chunk, send it!
is_within_view_distance(chunk_pos, center, audible_chunks) is_within_chebyshev_distance(chunk_pos, center, audible_chunks)
}); });
let recipients_by_version = Self::collect_java_recipients_by_version(recipients); let recipients_by_version = Self::collect_java_recipients_by_version(recipients);
@ -1419,7 +1421,7 @@ impl World {
} }
let center = p.get_entity().chunk_pos.load(); let center = p.get_entity().chunk_pos.load();
is_within_view_distance(chunk_pos, center, audible_chunks) is_within_chebyshev_distance(chunk_pos, center, audible_chunks)
}); });
let recipients_by_version = Self::collect_java_recipients_by_version(recipients); let recipients_by_version = Self::collect_java_recipients_by_version(recipients);
@ -1705,9 +1707,9 @@ impl World {
let mut java_recipients = Vec::new(); let mut java_recipients = Vec::new();
let recipients = players.iter().filter(|p| { let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load(); p.watched_section
let view_distance = get_view_distance(p).get() as i32; .load()
is_within_view_distance(chunk_pos, center, view_distance) .is_within_distance(chunk_pos.x, chunk_pos.y)
}); });
let mut bedrock_packets = Vec::new(); let mut bedrock_packets = Vec::new();
@ -1784,9 +1786,10 @@ impl World {
if !bedrock_water_packets.is_empty() { if !bedrock_water_packets.is_empty() {
let players = self.players.load(); let players = self.players.load();
let recipients = players.iter().filter(|player| { let recipients = players.iter().filter(|player| {
let center = player.get_entity().chunk_pos.load(); player
let view_distance = get_view_distance(player).get() as i32; .watched_section
is_within_view_distance(chunk_pos, center, view_distance) .load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
}); });
for player in recipients { for player in recipients {
if let ClientPlatform::Bedrock(client) = player.client.as_ref() { if let ClientPlatform::Bedrock(client) = player.client.as_ref() {
@ -4625,22 +4628,13 @@ impl World {
// NOTE: This function doesn't actually await on anything, it just spawns two tokio tasks // NOTE: This function doesn't actually await on anything, it just spawns two tokio tasks
/// IMPORTANT: Chunks have to be non-empty /// IMPORTANT: Chunks have to be non-empty
fn spawn_world_entity_chunks( fn spawn_world_entity_chunks(self: &Arc<Self>, player: Arc<Player>, chunks: Vec<Vector2<i32>>) {
self: &Arc<Self>,
player: Arc<Player>,
chunks: Vec<Vector2<i32>>,
center_chunk: Vector2<i32>,
) {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let inst = std::time::Instant::now(); let inst = std::time::Instant::now();
// Sort such that the first chunks are closest to the center. // Note: `chunks` originates from `Cylindrical::changed_chunks`, which is
let mut chunks = chunks; // already ordered from closest to farthest from center by the precompiled
chunks.sort_unstable_by_key(|pos| { // cylindrical chunk view LUT. No re-sorting needed.
let rel_x = pos.x - center_chunk.x;
let rel_z = pos.y - center_chunk.y;
rel_x * rel_x + rel_z * rel_z
});
let mut entity_receiver = self.level.receive_entity_chunks(chunks); let mut entity_receiver = self.level.receive_entity_chunks(chunks);
let level = self.level.clone(); let level = self.level.clone();
@ -5204,10 +5198,11 @@ impl World {
let players = self.players.load(); let players = self.players.load();
for player in players.iter() { for player in players.iter() {
let center = player.get_entity().chunk_pos.load(); if player
let view_distance = get_view_distance(player).get() as i32; .watched_section
.load()
if is_within_view_distance(chunk_pos, center, view_distance) { .is_within_distance(chunk_pos.x, chunk_pos.y)
{
player.client.try_enqueue_spawn_packet(entity); player.client.try_enqueue_spawn_packet(entity);
} }
} }
@ -7018,16 +7013,13 @@ impl World {
} }
/// Broadcasts a packet to all players who currently have the target chunk loaded. /// Broadcasts a packet to all players who currently have the target chunk loaded.
/// This uses highly optimized Chebyshev distance math (Chunk Grid) instead of floating point distance checks.
pub fn broadcast_to_chunk<P: ClientPacket>(&self, chunk_pos: Vector2<i32>, packet: &P) { pub fn broadcast_to_chunk<P: ClientPacket>(&self, chunk_pos: Vector2<i32>, packet: &P) {
let players = self.players.load(); let players = self.players.load();
let recipients = players.iter().filter(|p| { let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load(); p.watched_section
let view_distance = get_view_distance(p).get() as i32; .load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
// Chebyshev distance (Minecraft's chunk loading shape)
is_within_view_distance(chunk_pos, center, view_distance)
}); });
let recipients_by_version = Self::collect_java_recipients_by_version(recipients); let recipients_by_version = Self::collect_java_recipients_by_version(recipients);
@ -7041,9 +7033,10 @@ impl World {
) { ) {
let players = self.players.load(); let players = self.players.load();
let recipients = players.iter().filter_map(|player| { let recipients = players.iter().filter_map(|player| {
let center = player.get_entity().chunk_pos.load(); if player
let view_distance = get_view_distance(player).get() as i32; .watched_section
if is_within_view_distance(chunk_pos, center, view_distance) .load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
&& let ClientPlatform::Bedrock(client) = player.client.as_ref() && let ClientPlatform::Bedrock(client) = player.client.as_ref()
{ {
return Some(client); return Some(client);
@ -7064,9 +7057,9 @@ impl World {
let mut bedrock_recipients = Vec::new(); let mut bedrock_recipients = Vec::new();
let recipients = players.iter().filter(|p| { let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load(); p.watched_section
let view_distance = get_view_distance(p).get() as i32; .load()
is_within_view_distance(chunk_pos, center, view_distance) .is_within_distance(chunk_pos.x, chunk_pos.y)
}); });
for p in recipients { for p in recipients {
@ -7095,10 +7088,9 @@ impl World {
if except.contains(&p.get_entity().entity_uuid) { if except.contains(&p.get_entity().entity_uuid) {
return false; return false;
} }
let center = p.get_entity().chunk_pos.load(); p.watched_section
let view_distance = get_view_distance(p).get() as i32; .load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
is_within_view_distance(chunk_pos, center, view_distance)
}); });
let recipients_by_version = Self::collect_java_recipients_by_version(recipients); let recipients_by_version = Self::collect_java_recipients_by_version(recipients);
@ -7117,10 +7109,9 @@ impl World {
if except.contains(&p.get_entity().entity_uuid) { if except.contains(&p.get_entity().entity_uuid) {
return false; return false;
} }
let center = p.get_entity().chunk_pos.load(); p.watched_section
let view_distance = get_view_distance(p).get() as i32; .load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
is_within_view_distance(chunk_pos, center, view_distance)
}); });
let mut java_recipients = Vec::new(); let mut java_recipients = Vec::new();

View File

@ -2,6 +2,7 @@ use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
const MAX_VIEW_DISTANCE: u8 = 32; const MAX_VIEW_DISTANCE: u8 = 32;
const MAX_CHEBYSHEV_RADIUS: u8 = 48;
pub fn build() -> TokenStream { pub fn build() -> TokenStream {
let entries = (0..=MAX_VIEW_DISTANCE).map(|dist| { let entries = (0..=MAX_VIEW_DISTANCE).map(|dist| {
@ -32,11 +33,81 @@ pub fn build() -> TokenStream {
let array_len = MAX_VIEW_DISTANCE as usize + 1; let array_len = MAX_VIEW_DISTANCE as usize + 1;
// Generate Chebyshev / square concentric ring offsets
let mut all_chebyshev_offsets = vec![];
let mut ring_ranges = vec![];
let mut square_ranges = vec![];
let mut current_idx = 0usize;
for r in 0..=MAX_CHEBYSHEV_RADIUS {
let r_i = r as i8;
let start = current_idx;
if r == 0 {
all_chebyshev_offsets.push((0i8, 0i8));
current_idx += 1;
} else {
// Top and bottom rows
for x in -r_i..=r_i {
all_chebyshev_offsets.push((x, -r_i));
all_chebyshev_offsets.push((x, r_i));
current_idx += 2;
}
// Left and right columns (excluding corners already added above)
for z in (-r_i + 1)..=(r_i - 1) {
all_chebyshev_offsets.push((-r_i, z));
all_chebyshev_offsets.push((r_i, z));
current_idx += 2;
}
}
let end = current_idx;
ring_ranges.push((start, end));
square_ranges.push((0usize, end));
}
let chebyshev_total = all_chebyshev_offsets.len();
let chebyshev_elems = all_chebyshev_offsets
.into_iter()
.map(|(x, z)| quote!((#x, #z)));
let ring_bounds = ring_ranges.iter().map(|(s, e)| quote!((#s, #e)));
let square_bounds = square_ranges.iter().map(|(_, e)| quote!(#e));
let chebyshev_len = MAX_CHEBYSHEV_RADIUS as usize + 1;
quote! { quote! {
/// The maximum supported view distance /// The maximum supported view distance
pub const MAX_VIEW_DISTANCE: u8 = #MAX_VIEW_DISTANCE; pub const MAX_VIEW_DISTANCE: u8 = #MAX_VIEW_DISTANCE;
/// Static precomputed lookup table for relative chunk offsets by view distance (0..=32). /// Static precomputed lookup table for relative chunk offsets by view distance (0..=32).
pub static CHUNK_VIEW_LUT: [&[(i8, i8)]; #array_len] = [ #(#entries),* ]; pub static CHUNK_VIEW_LUT: [&[(i8, i8)]; #array_len] = [ #(#entries),* ];
/// The maximum supported Chebyshev / square radius (used for ticket levels and simulation distance)
pub const MAX_CHEBYSHEV_RADIUS: u8 = #MAX_CHEBYSHEV_RADIUS;
/// Flat backing storage for all concentric Chebyshev offsets up to `MAX_CHEBYSHEV_RADIUS`.
pub static CHEBYSHEV_OFFSETS: [(i8, i8); #chebyshev_total] = [ #(#chebyshev_elems),* ];
/// (start, end) index bounds in `CHEBYSHEV_OFFSETS` for each Chebyshev radius (0..=48).
pub static CHEBYSHEV_RING_BOUNDS: [(usize, usize); #chebyshev_len] = [ #(#ring_bounds),* ];
/// End index in `CHEBYSHEV_OFFSETS` for all chunks within Chebyshev radius (0..=48).
pub static CHEBYSHEV_SQUARE_BOUNDS: [usize; #chebyshev_len] = [ #(#square_bounds),* ];
/// Returns a precomputed slice of chunk offsets at exact Chebyshev radius `radius` (0..=48).
#[inline]
#[must_use]
pub fn get_chebyshev_ring(radius: u8) -> &'static [(i8, i8)] {
let r = (radius as usize).min(MAX_CHEBYSHEV_RADIUS as usize);
let (start, end) = CHEBYSHEV_RING_BOUNDS[r];
&CHEBYSHEV_OFFSETS[start..end]
}
/// Returns a precomputed slice of all chunk offsets within Chebyshev radius `radius` (0..=48), concentric-sorted.
#[inline]
#[must_use]
pub fn get_chebyshev_square(radius: u8) -> &'static [(i8, i8)] {
let r = (radius as usize).min(MAX_CHEBYSHEV_RADIUS as usize);
let end = CHEBYSHEV_SQUARE_BOUNDS[r];
&CHEBYSHEV_OFFSETS[..end]
}
} }
} }