mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-11 02:21:38 -10:00
Merge pull request #13953 from DUzzL/fix/13852-entity-ticking-neighbours
🤖 Agent: the 2-column gate matches ChunkMap.prepareEntityTickingChunk (getChunkRangeFuture radius 2), but it ran 25 hash probes per entity per call site; pushed a per-chunk memo inside the closure so a crowd in one chunk pays the neighbourhood check once. Forced chunks stream 3-4 rings, so a /forceload'd chunk still gets its ring.
This commit is contained in:
3 files changed
+72
-6
No files matched your search
@@ -1228,8 +1228,8 @@ impl Game<'_> {
|
||||
/// `DistanceManager.inEntityTickingRange` in an entity-ticking chunk
|
||||
/// (block coordinates), from every player's tickets (`update_chunk_tickets`).
|
||||
pub(super) fn entity_ticking_area(&self) -> impl Fn(i32, i32) -> bool + use<> {
|
||||
let levels = self.tickets.levels();
|
||||
move |x, z| levels.entity_ticking_at(x, z)
|
||||
self.tickets
|
||||
.entity_ticking_area(&self.world.chunks, self.remote.is_none())
|
||||
}
|
||||
/// The entity phase of `tick_creatures`: every mob's tick, deaths, and the
|
||||
/// projectiles and effects that follow from them.
|
||||
|
||||
@@ -1338,10 +1338,16 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
game.options.dedicated = Some(Vec::new());
|
||||
game.world.chunks.insert(
|
||||
(0, 0),
|
||||
std::sync::Arc::new(horizon_client::chunk::Chunk::empty(0, 0)),
|
||||
);
|
||||
// Entity ticking waits for every chunk within 2 columns
|
||||
// (`ChunkMap.prepareEntityTickingChunk`).
|
||||
for z in -2..=2 {
|
||||
for x in -2..=2 {
|
||||
game.world.chunks.insert(
|
||||
(x, z),
|
||||
std::sync::Arc::new(horizon_client::chunk::Chunk::empty(x, z)),
|
||||
);
|
||||
}
|
||||
}
|
||||
game.streaming.forced.insert((0, 0));
|
||||
Some(game)
|
||||
}
|
||||
@@ -1805,6 +1811,30 @@ mod tests {
|
||||
game.particles.requested.as_ref().unwrap().len()
|
||||
}
|
||||
|
||||
/// #13852: a chunk enters entity ticking only once every chunk within 2
|
||||
/// columns is loaded (`ChunkMap.prepareEntityTickingChunk`), so TNT beside
|
||||
/// a missing chunk keeps its fuse until the neighbourhood is whole.
|
||||
#[test]
|
||||
fn tnt_waits_while_a_chunk_two_columns_away_is_missing() {
|
||||
let Some(mut game) = server_game() else {
|
||||
return;
|
||||
};
|
||||
game.world.chunks.remove(&(2, 0));
|
||||
game.add_primed_tnt(BlockPos::new(8, 64, 8), 0.0, 80, None, None);
|
||||
game.tnts[0].no_gravity = true;
|
||||
game.tnts[0].velocity = Vec3::ZERO;
|
||||
for _ in 0..3 {
|
||||
game.tick_world(true).unwrap();
|
||||
}
|
||||
assert_eq!(game.tnts[0].fuse, 80, "no entity ticking yet");
|
||||
game.world.chunks.insert(
|
||||
(2, 0),
|
||||
std::sync::Arc::new(horizon_client::chunk::Chunk::empty(2, 0)),
|
||||
);
|
||||
game.tick_world(true).unwrap();
|
||||
assert!(game.tnts[0].fuse < 80, "ticks once the chunk is back");
|
||||
}
|
||||
|
||||
/// #10063: TNT lit between level ticks (flint and steel, a command) has its
|
||||
/// first tick in the next level tick, so its fuse reads 79 after it, and it
|
||||
/// explodes on the 80th (`PrimedTnt.tick`). Horizon held every TNT that
|
||||
|
||||
@@ -186,6 +186,10 @@ impl Levels {
|
||||
fn get(&self, x: i32, z: i32) -> Option<(u8, u8)> {
|
||||
Some(self.0.as_ref()?.get(x, z))
|
||||
}
|
||||
/// Whether any ticket exists; with none, everything ticks.
|
||||
pub fn has_tickets(&self) -> bool {
|
||||
self.0.is_some()
|
||||
}
|
||||
/// `DistanceManager.inEntityTickingRange`, in an entity-ticking chunk:
|
||||
/// where entities tick.
|
||||
pub fn entity_ticking(&self, x: i32, z: i32) -> bool {
|
||||
@@ -252,9 +256,41 @@ pub struct ChunkTickets {
|
||||
sources: Option<Sources>,
|
||||
levels: Levels,
|
||||
portal: Vec<PortalTicket>,
|
||||
/// The resident chunk keys at one membership revision.
|
||||
resident: std::sync::Mutex<(u64, Arc<crate::fast_hash::FxHashSet<(i32, i32)>>)>,
|
||||
}
|
||||
|
||||
impl ChunkTickets {
|
||||
/// `Levels::entity_ticking_at`, held back as `ChunkMap.prepareEntityTickingChunk`
|
||||
/// does: a chunk waits until every chunk within 2 columns is loaded. With no
|
||||
/// tickets everything ticks.
|
||||
pub fn entity_ticking_area(
|
||||
&self,
|
||||
chunks: &crate::chunk_map::ChunkMap,
|
||||
gate: bool,
|
||||
) -> impl Fn(i32, i32) -> bool + use<> {
|
||||
let levels = self.levels.clone();
|
||||
let resident = (gate && levels.0.is_some()).then(|| {
|
||||
let revision = chunks.membership_revision();
|
||||
let mut cache = self.resident.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if cache.0 != revision {
|
||||
*cache = (revision, Arc::new(chunks.keys().copied().collect()));
|
||||
}
|
||||
cache.1.clone()
|
||||
});
|
||||
// Callers ask once per entity: remember each chunk's answer, so an
|
||||
// entity costs one lookup instead of the 25 of its neighbourhood.
|
||||
let surrounded = std::cell::RefCell::new(crate::fast_hash::FxHashMap::default());
|
||||
move |x, z| {
|
||||
levels.entity_ticking_at(x, z)
|
||||
&& resident.as_ref().is_none_or(|keys| {
|
||||
let (cx, cz) = (x >> 4, z >> 4);
|
||||
*surrounded.borrow_mut().entry((cx, cz)).or_insert_with(|| {
|
||||
(cz - 2..=cz + 2).all(|z| (cx - 2..=cx + 2).all(|x| keys.contains(&(x, z))))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
/// `None`: tick everywhere.
|
||||
pub fn update(&mut self, mut sources: Option<Sources>) {
|
||||
// The game submits a fresh `Sources` value every tick even when no
|
||||
|
||||
Reference in new issue
Block a user