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 itertools::Itertools;
use std::cmp::min;
use std::collections::VecDeque;
use std::collections::hash_map::Entry;
use std::fmt::Write;
use std::mem::swap;
use std::sync::Arc;
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 is_priority_dirty: bool,
pub pos_level: ChunkLevel,
@ -95,9 +15,6 @@ pub struct ChunkLoading {
pub ticket: HashMapType<ChunkPos, Vec<i8>>, // TODO lifetime & id
pub high_priority: Vec<ChunkPos>,
pub sender: Arc<LevelChannel>,
pub increase_update: VecDeque<(ChunkPos, i8)>,
pub decrease_update: VecDeque<(ChunkPos, i8)>,
cache: LevelCache,
}
impl ChunkLoading {
@ -110,13 +27,13 @@ impl ChunkLoading {
let Some(&level) = levels.iter().min() else {
continue;
};
let range = Self::MAX_LEVEL - level - 1;
for dx in -range..=range {
for dy in -range..=range {
let range = (Self::MAX_LEVEL - level - 1).max(0) as u8;
for r in 0..=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 level_from_source = level + dx.abs().max(dy.abs());
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(),
high_priority: Vec::new(),
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) {
// debug!("add force ticket at {pos:?}");
self.high_priority.push(pos);
self.is_priority_dirty = true;
self.add_ticket(pos, Self::FULL_CHUNK_LEVEL);
}
pub fn remove_force_ticket(&mut self, pos: ChunkPos) {
// debug!("remove force ticket at {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.remove_ticket(pos, Self::FULL_CHUNK_LEVEL);
}
pub fn add_ticket(&mut self, pos: ChunkPos, level: i8) {
// debug!("add ticket at {pos:?} level {level}");
debug_assert!(level < Self::MAX_LEVEL);
@ -336,15 +206,30 @@ impl ChunkLoading {
if old <= level {
return;
}
self.cache.clean(pos, level);
self.cache.set(&self.pos_level, pos, level);
debug_assert!(self.increase_update.is_empty());
self.increase_update.push_back((pos, level));
self.run_increase_update();
self.cache.write(&mut self.pos_level, &mut self.change);
let max_range = (Self::MAX_LEVEL - level - 1).max(0) as u8;
for r in 0..=max_range {
let ring_level = 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);
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());
}
pub fn remove_ticket(&mut self, pos: ChunkPos, level: i8) {
// debug!("remove ticket at {pos:?} level {level}");
debug_assert!(level < Self::MAX_LEVEL);
@ -357,23 +242,55 @@ impl ChunkLoading {
return;
};
vec.remove(index);
match self.pos_level.entry(pos) {
Entry::Occupied(entry) => {
let old_level = *entry.get();
let source = *vec.iter().min().unwrap_or(&Self::MAX_LEVEL);
if vec.is_empty() {
self.ticket.remove(&pos);
}
if level == old_level && source != level {
self.cache.clean(pos, old_level);
self.cache.set(&self.pos_level, pos, Self::MAX_LEVEL);
debug_assert!(self.decrease_update.is_empty());
self.decrease_update.push_back((pos, level));
self.run_decrease_update(pos, (Self::MAX_LEVEL - level - 1) as i32);
self.cache.write(&mut self.pos_level, &mut self.change);
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 {
debug_assert!(self.debug_check_error());
return;
}
let range = (Self::MAX_LEVEL - old_level - 1).max(0) as u8;
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());
}

View File

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

View File

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

View File

@ -419,7 +419,7 @@ fn move_piston(
world.set_block_state(
&target_pos,
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) {
@ -449,7 +449,7 @@ fn move_piston(
world.set_block_state(
&extended_pos,
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);
props.facing = dir.to_facing();
@ -482,7 +482,9 @@ fn move_piston(
state.id,
BlockFlags::NOTIFY_LISTENERS,
);
world.update_neighbors(pos, None);
world
.block_registry
.update_neighbors(world, pos, BlockFlags::NOTIFY_LISTENERS);
world.block_registry.prepare(
world,
pos,
@ -508,15 +510,22 @@ fn move_piston(
block_state.id,
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() {
world.update_neighbors(&moved_block_pos, None);
for (i, &moved_block_pos) in moved_blocks.iter().rev().enumerate() {
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 {
world.update_neighbors(&extended_pos, None);
world.update_neighbors_at(&extended_pos, &Block::PISTON_HEAD, None);
}
true

View File

@ -241,13 +241,35 @@ pub fn get_power_on_side(
) -> u8 {
let side_pos = pos.offset(side.to_block_direction().to_offset());
let (side_block, side_state) = world.get_block_and_state(&side_pos);
if !only_gate || is_diode(side_block) {
world.block_registry.get_weak_redstone_power(
let side_direction = side.to_block_direction();
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,
world,
&side_pos,
side_state,
side.to_block_direction(),
side_direction,
)
} else {
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_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::tick::TickPriority;
use pumpkin_world::world::BlockFlags;
use crate::block::BlockBehaviour;
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::OnPlaceArgs;
use crate::block::OnScheduledTickArgs;
use crate::block::OnStateReplacedArgs;
use crate::block::PlacedArgs;
use crate::entity::EntityBase;
use crate::world::World;
use super::RailProperties;
use super::common::{
can_place_rail_at, compute_placed_rail_shape, rail_placement_is_valid,
update_flanking_rails_shape,
};
use pumpkin_data::block_properties::DetectorRailProperties;
#[pumpkin_block("minecraft:detector_rail")]
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 {
fn on_place(&self, args: OnPlaceArgs<'_>) -> BlockStateId {
let mut rail_props = RailProperties::default(args.block);
@ -35,6 +118,7 @@ impl BlockBehaviour for DetectorRailBlock {
fn placed(&self, args: PlacedArgs<'_>) {
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<'_>) {
@ -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 {
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;
}
// 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
&& let Some(player) = caller.get_player()
{

View File

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

View File

@ -2567,11 +2567,9 @@ impl Player {
if let Ok(listener) = self.chunk_listener.try_lock()
&& let Ok(mut sender) = self.chunk_sender.try_lock()
{
let center = self.get_entity().chunk_pos.load();
let view_dist =
std::num::NonZeroI32::from(self.watched_section.load().view_distance).get();
let watched = self.watched_section.load();
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);
}
}
@ -2585,8 +2583,9 @@ impl Player {
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| {
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(

View File

@ -184,6 +184,22 @@ impl EntityBase for MinecartEntity {
if is_activator_rail && let MinecartKind::Hopper(minecart) = &self.kind {
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 is_powered_rail {

View File

@ -1,6 +1,7 @@
use bytes::Bytes;
use rayon::prelude::*;
use rustc_hash::{FxHashMap, FxHashSet};
use std::num::NonZero;
use std::sync::{Arc, Weak};
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::version::JavaMinecraftVersion;
use pumpkin_world::chunk::ChunkData;
use pumpkin_world::cylindrical_chunk_iterator::Cylindrical;
use pumpkin_world::level::{Level, SyncChunk};
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 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);
for pos in sorted {
if ready.len() >= quota_limit {
break;
// If pending_chunks is small, sorting it directly avoids scanning offsets.
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) {
ready.push(PreparedChunk {
position: pos,
chunk: chunk.value().clone(),
});
// Fallback for any pending chunks outside the precomputed table
if ready.is_empty() {
for &pos in &self.pending_chunks {
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,
level: &Level,
player_chunk: Vector2<i32>,
view_distance: NonZero<u8>,
epoch: u32,
version: JavaMinecraftVersion,
) -> Option<PreparedBatch> {
@ -173,7 +217,7 @@ impl ChunkSender {
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() {
return None;
}

View File

@ -116,6 +116,10 @@ pub struct JavaClient {
///
/// Whether we are waiting for a response after sending a keep alive packet.
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.
pub keep_alive_id: AtomicCell<i64>,
/// The last time we sent a keep alive packet.
@ -254,6 +258,7 @@ impl JavaClient {
brand: ArcSwap::from_pointee(pending.brand),
player: ArcSwap::from_pointee(None),
wait_for_keep_alive: AtomicBool::new(false),
received_movement_this_tick: AtomicBool::new(false),
keep_alive_id: AtomicCell::new(0),
last_keep_alive_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) => {
// TODO
self.handle_client_tick_end(player);
}
id if id == STestInstanceBlockAction::to_id(version) => {
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 client_command;
pub mod client_information;
pub mod client_tick_end;
pub mod close_container;
pub mod command_suggestion;
pub mod configuration_acknowledged;

View File

@ -3,6 +3,9 @@ use super::*;
impl JavaClient {
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 last_pos = entity.pos.load();
let pos = Vector3::new(packet.x, packet.y, packet.z);

View File

@ -3,6 +3,9 @@ use super::*;
impl JavaClient {
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
.living_entity
.entity

View File

@ -52,6 +52,9 @@ impl JavaClient {
if !player.has_client_loaded() {
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() {
return;
}
@ -190,6 +193,9 @@ impl JavaClient {
if !player.has_client_loaded() {
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() {
return;
}

View File

@ -6,6 +6,9 @@ impl JavaClient {
if !player.has_client_loaded() {
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() {
self.try_kick(&TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT,

View File

@ -49,18 +49,24 @@ impl ActiveChunkTracker {
active_chunks: &mut FxHashSet<Vector2<i32>>,
newly_active: &mut Vec<Vector2<i32>>,
) {
for dx in -area.simulation_distance..=area.simulation_distance {
for dz in -area.simulation_distance..=area.simulation_distance {
self.add_chunk(area.center.add_raw(dx, dz), active_chunks, newly_active);
}
let offsets = pumpkin_data::chunk_view_lut::get_chebyshev_square(
area.simulation_distance.max(0) as u8,
);
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>>) {
for dx in -area.simulation_distance..=area.simulation_distance {
for dz in -area.simulation_distance..=area.simulation_distance {
self.remove_chunk(area.center.add_raw(dx, dz), active_chunks);
}
let offsets = pumpkin_data::chunk_view_lut::get_chebyshev_square(
area.simulation_distance.max(0) as u8,
);
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 {
None => self.add_area(area, active_chunks, newly_active),
Some(previous) if previous != area => {
for dx in -previous.simulation_distance..=previous.simulation_distance {
for dz in -previous.simulation_distance..=previous.simulation_distance {
let pos = previous.center.add_raw(dx, dz);
if (pos.x - area.center.x).abs() > area.simulation_distance
|| (pos.y - area.center.y).abs() > area.simulation_distance
{
self.remove_chunk(pos, active_chunks);
}
let prev_offsets = pumpkin_data::chunk_view_lut::get_chebyshev_square(
previous.simulation_distance.max(0) as u8,
);
for &(dx, dz) in prev_offsets {
let pos = previous.center.add_raw(dx as i32, dz as i32);
if (pos.x - area.center.x).abs() > area.simulation_distance
|| (pos.y - area.center.y).abs() > area.simulation_distance
{
self.remove_chunk(pos, active_chunks);
}
}
for dx in -area.simulation_distance..=area.simulation_distance {
for dz in -area.simulation_distance..=area.simulation_distance {
let pos = area.center.add_raw(dx, dz);
if (pos.x - previous.center.x).abs() > previous.simulation_distance
|| (pos.y - previous.center.y).abs() > previous.simulation_distance
{
self.add_chunk(pos, active_chunks, newly_active);
}
let curr_offsets = pumpkin_data::chunk_view_lut::get_chebyshev_square(
area.simulation_distance.max(0) as u8,
);
for &(dx, dz) in curr_offsets {
let pos = area.center.add_raw(dx as i32, dz as i32);
if (pos.x - previous.center.x).abs() > previous.simulation_distance
|| (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)
}
// Checks if the target chunk is within the view distance
// of the center chunk. Uses Chebyshev distance.
// Checks if the target chunk is within Chebyshev distance (L_infinity) of the center chunk.
#[must_use]
#[inline]
pub fn is_within_view_distance(
pub fn is_within_chebyshev_distance(
center: Vector2<i32>,
target: Vector2<i32>,
view_distance: i32,
distance: i32,
) -> 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)]
@ -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| {
s.advanced_config.networking.java.simulation_distance.get()
});
@ -173,7 +172,7 @@ pub fn update_position(player: &Arc<Player>) {
}
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);
}

View File

@ -24,7 +24,7 @@ use crate::entity::player::Player;
use crate::net::ClientPlatform;
use crate::net::java::JavaClient;
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 entity: Arc<dyn EntityBase>,
@ -113,8 +113,10 @@ impl TrackedEntity {
let range_sq = visible_range_blocks * visible_range_blocks;
let entity_chunk = self.entity.get_entity().chunk_pos.load();
let player_chunk = player_entity.chunk_pos.load();
let in_view = is_within_view_distance(entity_chunk, player_chunk, player_vd);
let in_view = player
.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;

View File

@ -31,8 +31,7 @@ pub mod time;
pub mod villager_poi;
use crate::block::RandomTickArgs;
use crate::world::chunker::get_view_distance;
use crate::world::chunker::is_within_view_distance;
use crate::world::chunker::is_within_chebyshev_distance;
use crate::{block::BlockEvent, entity::item::ItemEntity};
use crate::{
block::{
@ -447,8 +446,11 @@ impl World {
let mut newly_active = Vec::new();
let mut current_players = FxHashSet::default();
let spectators_generate_chunks =
self.level_info.load().game_rules.spectators_generate_chunks;
for player in players.iter() {
if player.is_spectator() {
if player.is_spectator() && !spectators_generate_chunks {
continue;
}
let id = player.gameprofile.id;
@ -1351,7 +1353,7 @@ impl World {
let chunk_pos = BlockPos::floored_v(*position).chunk_position();
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 Ok(data) = client.serialize_packet(&packet)
{
@ -1389,7 +1391,7 @@ impl World {
let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load();
// 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);
@ -1419,7 +1421,7 @@ impl World {
}
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);
@ -1705,9 +1707,9 @@ impl World {
let mut java_recipients = Vec::new();
let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load();
let view_distance = get_view_distance(p).get() as i32;
is_within_view_distance(chunk_pos, center, view_distance)
p.watched_section
.load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
});
let mut bedrock_packets = Vec::new();
@ -1784,9 +1786,10 @@ impl World {
if !bedrock_water_packets.is_empty() {
let players = self.players.load();
let recipients = players.iter().filter(|player| {
let center = player.get_entity().chunk_pos.load();
let view_distance = get_view_distance(player).get() as i32;
is_within_view_distance(chunk_pos, center, view_distance)
player
.watched_section
.load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
});
for player in recipients {
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
/// IMPORTANT: Chunks have to be non-empty
fn spawn_world_entity_chunks(
self: &Arc<Self>,
player: Arc<Player>,
chunks: Vec<Vector2<i32>>,
center_chunk: Vector2<i32>,
) {
fn spawn_world_entity_chunks(self: &Arc<Self>, player: Arc<Player>, chunks: Vec<Vector2<i32>>) {
#[cfg(debug_assertions)]
let inst = std::time::Instant::now();
// Sort such that the first chunks are closest to the center.
let mut chunks = chunks;
chunks.sort_unstable_by_key(|pos| {
let rel_x = pos.x - center_chunk.x;
let rel_z = pos.y - center_chunk.y;
rel_x * rel_x + rel_z * rel_z
});
// Note: `chunks` originates from `Cylindrical::changed_chunks`, which is
// already ordered from closest to farthest from center by the precompiled
// cylindrical chunk view LUT. No re-sorting needed.
let mut entity_receiver = self.level.receive_entity_chunks(chunks);
let level = self.level.clone();
@ -5204,10 +5198,11 @@ impl World {
let players = self.players.load();
for player in players.iter() {
let center = player.get_entity().chunk_pos.load();
let view_distance = get_view_distance(player).get() as i32;
if is_within_view_distance(chunk_pos, center, view_distance) {
if player
.watched_section
.load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
{
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.
/// 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) {
let players = self.players.load();
let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load();
let view_distance = get_view_distance(p).get() as i32;
// Chebyshev distance (Minecraft's chunk loading shape)
is_within_view_distance(chunk_pos, center, view_distance)
p.watched_section
.load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
});
let recipients_by_version = Self::collect_java_recipients_by_version(recipients);
@ -7041,9 +7033,10 @@ impl World {
) {
let players = self.players.load();
let recipients = players.iter().filter_map(|player| {
let center = player.get_entity().chunk_pos.load();
let view_distance = get_view_distance(player).get() as i32;
if is_within_view_distance(chunk_pos, center, view_distance)
if player
.watched_section
.load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
&& let ClientPlatform::Bedrock(client) = player.client.as_ref()
{
return Some(client);
@ -7064,9 +7057,9 @@ impl World {
let mut bedrock_recipients = Vec::new();
let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load();
let view_distance = get_view_distance(p).get() as i32;
is_within_view_distance(chunk_pos, center, view_distance)
p.watched_section
.load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
});
for p in recipients {
@ -7095,10 +7088,9 @@ impl World {
if except.contains(&p.get_entity().entity_uuid) {
return false;
}
let center = p.get_entity().chunk_pos.load();
let view_distance = get_view_distance(p).get() as i32;
is_within_view_distance(chunk_pos, center, view_distance)
p.watched_section
.load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
});
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) {
return false;
}
let center = p.get_entity().chunk_pos.load();
let view_distance = get_view_distance(p).get() as i32;
is_within_view_distance(chunk_pos, center, view_distance)
p.watched_section
.load()
.is_within_distance(chunk_pos.x, chunk_pos.y)
});
let mut java_recipients = Vec::new();

View File

@ -2,6 +2,7 @@ use proc_macro2::TokenStream;
use quote::quote;
const MAX_VIEW_DISTANCE: u8 = 32;
const MAX_CHEBYSHEV_RADIUS: u8 = 48;
pub fn build() -> TokenStream {
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;
// 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! {
/// The maximum supported view distance
pub const MAX_VIEW_DISTANCE: u8 = #MAX_VIEW_DISTANCE;
/// Static precomputed lookup table for relative chunk offsets by view distance (0..=32).
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]
}
}
}