fix: reset player velocity state on client tick end
This commit is contained in:
parent
5bbbf4da84
commit
26baa1effd
File diff suppressed because it is too large
Load Diff
@ -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());
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
19
crates/pumpkin/src/net/java/play/client_tick_end.rs
Normal file
19
crates/pumpkin/src/net/java/play/client_tick_end.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user