Merge pull request #9632 from DUzzL/fix/9607-scaffolding-sight

🤖 Agent: Line of sight now clips with the viewer's collision context, as LivingEntity.hasLineOfSight's ClipContext(COLLIDER, entity) does, so scaffolding is open from below and solid from above. I ran the new test against prepared resources too: it builds the world and passes, not vacuously.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-05 03:40:07 +01:00
commit d158e44736
2 files changed
+61 -6

No files matched your search

+5 -1
View File
@@ -647,7 +647,11 @@ impl Mob {
return false;
};
let eye = Vec3::new(self.position.x, self.eye_y(), self.position.z);
let v = w.line_of_sight(eye, Vec3::new(t.position.x, t.eye_y, t.position.z));
let v = w.line_of_sight_for(
Some(self.position.y),
eye,
Vec3::new(t.position.x, t.eye_y, t.position.z),
);
self.ai.seen_cache.push((who, v));
v
}
+56 -5
View File
@@ -116,6 +116,19 @@ impl MobWorld<'_> {
}
/// Level.clip with COLLIDER shapes (optionally stopping at any fluid): true on a miss.
pub fn clear_between(&self, from: Vec3, to: Vec3, fluids_block: bool) -> bool {
self.clear_between_for(None, from, to, fluids_block)
}
/// The same for an entity whose `getY()` is `feet_y`: `ClipContext` hands
/// its `CollisionContext.of(entity)` to every block, so scaffolding is
/// solid for a viewer above it and open for one below.
fn clear_between_for(
&self,
feet_y: Option<f64>,
from: Vec3,
to: Vec3,
fluids_block: bool,
) -> bool {
let context = feet_y.map(horizon_world::collision_context::CollisionContext::position);
let mut scene = SceneCollisions {
world: self.world,
binding: self.collision,
@@ -129,10 +142,15 @@ impl MobWorld<'_> {
if self.facts.is_air(state) {
return None;
}
let shape = self
.collision
.collision_shape(&mut scene, state, [p.x, p.y, p.z])
.ok()?;
let shape = match &context {
Some(context) => self
.collision
.collision_shape_with_context(&mut scene, state, p, context),
None => self
.collision
.collision_shape(&mut scene, state, [p.x, p.y, p.z]),
}
.ok()?;
shape.clip(from, to, p).map(|_| ())
});
matches!(result, Ok(None))
@@ -219,12 +237,16 @@ impl MobWorld<'_> {
}
/// LivingEntity.hasLineOfSight: eye to eye within 128 blocks.
pub fn line_of_sight(&self, from_eye: Vec3, to: Vec3) -> bool {
self.line_of_sight_for(None, from_eye, to)
}
/// The same for a viewer standing at `feet_y` (its collision context).
pub fn line_of_sight_for(&self, feet_y: Option<f64>, from_eye: Vec3, to: Vec3) -> bool {
let d =
(to.x - from_eye.x).powi(2) + (to.y - from_eye.y).powi(2) + (to.z - from_eye.z).powi(2);
if d > 128.0 * 128.0 {
return false;
}
self.clear_between(from_eye, to, false)
self.clear_between_for(feet_y, from_eye, to, false)
}
/// Level.canSeeSky: sky light 15 at the position (no roof above).
pub fn can_see_sky(&self, x: i32, y: i32, z: i32) -> bool {
@@ -542,3 +564,32 @@ pub(crate) fn test_player(position: Vec3) -> super::PlayerView {
vehicle: None,
}
}
#[cfg(test)]
mod scaffolding_sight_tests {
use super::*;
/// ScaffoldingBlock.getCollisionShape: a viewer below the block sees
/// through it, one above does not (issue 9607).
#[test]
fn scaffolding_blocks_sight_only_from_above() {
let ran = super::with_built_test_world(
&[],
|world, registry| {
world.chunks.insert(
(0, 0),
std::sync::Arc::new(crate::chunk::Chunk::empty(0, 0)),
);
let scaffolding = registry.default_state("minecraft:scaffolding").unwrap();
world.set(0, 65, 0, scaffolding).unwrap();
},
|w, _| {
let below = Vec3::new(0.5, 64.0 + 1.62, 0.5);
let above = Vec3::new(0.5, 66.0 + 1.62, 0.5);
assert!(w.line_of_sight_for(Some(64.0), below, above));
assert!(!w.line_of_sight_for(Some(66.0), above, below));
},
);
let _ = ran;
}
}