Merge pull request #836 from LookImEcho-Rick/codex/falling-block-collision-parity
🤖 Agent: Merged. Reviewed against vanilla: landing comes from Entity.move's vertical collision, the landing position is blockPosition(), velocity is zeroed per axis, and placement goes through Level.isUnobstructed with the collision shape. Locally on this head, fmt and clippy are clean, and contextual_collision, level_place, placement, powder_snow_collision and world_movement all PASS. Thanks LookImEcho-Rick!
This commit is contained in:
@@ -131,12 +131,25 @@ impl SceneCollisions<'_, '_> {
|
||||
movement: Vec3,
|
||||
max_step: f32,
|
||||
on_ground: bool,
|
||||
) -> Result<Vec3> {
|
||||
self.resolve_with_context(bounds, movement, max_step, on_ground, self.context(bounds))
|
||||
}
|
||||
/// Resolve movement with the entity's captured collision context. Most
|
||||
/// callers only need the position context; entities such as falling blocks
|
||||
/// also affect contextual block shapes (for example powder snow).
|
||||
pub fn resolve_with_context(
|
||||
&mut self,
|
||||
bounds: Aabb,
|
||||
movement: Vec3,
|
||||
max_step: f32,
|
||||
on_ground: bool,
|
||||
context: CollisionContext<'_>,
|
||||
) -> Result<Vec3> {
|
||||
Ok(resolve_world_movement(
|
||||
self,
|
||||
MovementRequest {
|
||||
entity: EntityIdentity(0),
|
||||
context: self.context(bounds),
|
||||
context,
|
||||
bounds,
|
||||
movement,
|
||||
max_step,
|
||||
|
||||
@@ -193,6 +193,8 @@ use horizon_core::{
|
||||
use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
use horizon_world::{
|
||||
block_collision::BlockCollisionRegistry,
|
||||
collision_context::{CollisionContext, CollisionEntity, CollisionItem},
|
||||
entity_obstruction::{EntityIdentity, EntityObstructionWorld, ObstructionEntity},
|
||||
level::{
|
||||
Behaviours, Dimension as LevelDimension, EntityQuery, Landing, LevelAccess, LevelEvent,
|
||||
PlaceContext, PlaceOutcome, PlayerContext, PlayerEffect, Ticks, flags as level_flags,
|
||||
@@ -451,12 +453,14 @@ struct Hit {
|
||||
struct GameLevel<'w> {
|
||||
world: &'w mut World,
|
||||
facts: &'w BlockFacts,
|
||||
collision: &'w BlockCollisionRegistry<'w>,
|
||||
events: &'w mut Vec<LevelEvent>,
|
||||
dimension: LevelDimension,
|
||||
darken: i32,
|
||||
raining: bool,
|
||||
thundering: bool,
|
||||
player_box: Option<([f64; 3], [f64; 3])>,
|
||||
player_collision: Option<PlayerCollision>,
|
||||
/// Minecart boxes (detector rails).
|
||||
cart_boxes: Vec<([f64; 3], [f64; 3])>,
|
||||
biomes: &'w Biomes,
|
||||
@@ -472,6 +476,32 @@ struct GameLevel<'w> {
|
||||
pending: &'w mut spill::Pending,
|
||||
spill: &'w mut spill::Spill,
|
||||
}
|
||||
struct PlayerCollision {
|
||||
y: f64,
|
||||
descending: bool,
|
||||
fall_distance: f64,
|
||||
main_hand_item: Arc<str>,
|
||||
main_hand_count: i32,
|
||||
leather_boots: bool,
|
||||
}
|
||||
struct PlayerObstruction(Aabb);
|
||||
impl EntityObstructionWorld for PlayerObstruction {
|
||||
type Error = std::convert::Infallible;
|
||||
fn obstruction_candidates(
|
||||
&mut self,
|
||||
_excluded: Option<EntityIdentity>,
|
||||
_bounds: Aabb,
|
||||
) -> std::result::Result<Vec<ObstructionEntity>, Self::Error> {
|
||||
Ok(vec![ObstructionEntity {
|
||||
identity: EntityIdentity(0),
|
||||
root_vehicle: EntityIdentity(0),
|
||||
bounds: self.0,
|
||||
removed: false,
|
||||
spectator: false,
|
||||
blocks_building: true,
|
||||
}])
|
||||
}
|
||||
}
|
||||
impl LevelAccess for GameLevel<'_> {
|
||||
fn precipitation(&self, pos: BlockPos) -> Option<horizon_world::level::Precipitation> {
|
||||
self.behaviours.precipitation_info(self, pos)
|
||||
@@ -584,21 +614,63 @@ impl LevelAccess for GameLevel<'_> {
|
||||
fn dimension(&self) -> LevelDimension {
|
||||
self.dimension
|
||||
}
|
||||
/// Level.isUnobstructed against the player for full-collision blocks.
|
||||
/// `Level.isUnobstructed`: the placed state's collision shape against the player.
|
||||
fn is_unobstructed(&self, pos: BlockPos, state: BlockStateId) -> bool {
|
||||
let Some((lo, hi)) = self.player_box else {
|
||||
return true;
|
||||
};
|
||||
if !self.facts.get(state).full_collision {
|
||||
let player = self.player_collision.as_ref();
|
||||
let context = player.map_or_else(
|
||||
|| CollisionContext::position(lo[1]),
|
||||
|player| {
|
||||
CollisionContext::for_entity(
|
||||
CollisionEntity {
|
||||
identity: EntityIdentity(0),
|
||||
y: player.y,
|
||||
descending: player.descending,
|
||||
main_hand: Some(CollisionItem {
|
||||
item: &player.main_hand_item,
|
||||
count: player.main_hand_count,
|
||||
}),
|
||||
},
|
||||
false,
|
||||
)
|
||||
},
|
||||
);
|
||||
let mut scene = collisions::SceneCollisions {
|
||||
world: &*self.world,
|
||||
binding: self.collision,
|
||||
subject: player.map(|player| horizon_client::collisions::PowderSnowSubject {
|
||||
fall_distance: player.fall_distance,
|
||||
falling_block: false,
|
||||
leather_boots: player.leather_boots,
|
||||
walkable_type: false,
|
||||
lava_walker: false,
|
||||
}),
|
||||
};
|
||||
let Ok(shape) = self
|
||||
.collision
|
||||
.collision_shape_with_context(&mut scene, state, pos, &context)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if shape.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let (x, y, z) = (f64::from(pos.x), f64::from(pos.y), f64::from(pos.z));
|
||||
!(x < hi[0] - 1e-7
|
||||
&& x + 1.0 > lo[0] + 1e-7
|
||||
&& y < hi[1] - 1e-7
|
||||
&& y + 1.0 > lo[1] + 1e-7
|
||||
&& z < hi[2] - 1e-7
|
||||
&& z + 1.0 > lo[2] + 1e-7)
|
||||
let shape = shape.moved(horizon_core::geometry::Vec3::new(
|
||||
f64::from(pos.x),
|
||||
f64::from(pos.y),
|
||||
f64::from(pos.z),
|
||||
));
|
||||
horizon_world::entity_obstruction::is_unobstructed(
|
||||
&mut PlayerObstruction(Aabb::new(
|
||||
horizon_core::geometry::Vec3::new(lo[0], lo[1], lo[2]),
|
||||
horizon_core::geometry::Vec3::new(hi[0], hi[1], hi[2]),
|
||||
)),
|
||||
None,
|
||||
&shape,
|
||||
)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
/// Entities that trigger pressure plates, tripwires and detector rails:
|
||||
/// the player (a mob) and the minecarts.
|
||||
@@ -1769,6 +1841,22 @@ impl Game<'_> {
|
||||
],
|
||||
)
|
||||
});
|
||||
let player_collision = self.player.as_ref().filter(|_| !spectator).map(|p| {
|
||||
let main_hand = self.inventory.get(self.selected).and_then(Option::as_ref);
|
||||
PlayerCollision {
|
||||
y: p.position.y,
|
||||
descending: p.velocity.y < 0.0,
|
||||
fall_distance: p.fall_distance,
|
||||
main_hand_item: main_hand.map_or_else(
|
||||
|| Arc::from("minecraft:air"),
|
||||
|stack| Arc::clone(&stack.item),
|
||||
),
|
||||
main_hand_count: main_hand.map_or(0, |stack| stack.count as i32),
|
||||
leather_boots: self.armor[3]
|
||||
.as_ref()
|
||||
.is_some_and(|stack| stack.item.as_ref() == "minecraft:leather_boots"),
|
||||
}
|
||||
});
|
||||
let cart_boxes = self
|
||||
.carts
|
||||
.iter()
|
||||
@@ -1790,12 +1878,14 @@ impl Game<'_> {
|
||||
cart_boxes,
|
||||
world: &mut self.world,
|
||||
facts: &self.res.facts,
|
||||
collision: self.collision,
|
||||
events: &mut self.level_events,
|
||||
dimension,
|
||||
darken,
|
||||
raining: rain > 0.2,
|
||||
thundering: thunder > 0.9,
|
||||
player_box,
|
||||
player_collision,
|
||||
biomes: &self.res.biomes,
|
||||
seed: self.level.seed,
|
||||
behaviours: &self.res.behaviours,
|
||||
@@ -2298,65 +2388,87 @@ impl Game<'_> {
|
||||
let mut falling = std::mem::take(&mut self.falling);
|
||||
let mut landed = Vec::new();
|
||||
let mut dropped = Vec::new();
|
||||
let mut collision_error = None;
|
||||
let entity_drops = self.ui.rules.bool("entity_drops");
|
||||
falling.retain_mut(|f| {
|
||||
let original = f.clone();
|
||||
f.previous = f.position;
|
||||
f.age = f.age.wrapping_add(1);
|
||||
if !f.no_gravity {
|
||||
f.velocity.y -= 0.04;
|
||||
}
|
||||
let wanted = f.velocity;
|
||||
let mut scene = collisions::SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: Some(collisions::PowderSnowSubject {
|
||||
fall_distance: f.fall_distance,
|
||||
falling_block: true,
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
let context = CollisionContext::for_entity(
|
||||
CollisionEntity {
|
||||
identity: EntityIdentity(0),
|
||||
y: f.position.y,
|
||||
descending: wanted.y < 0.0,
|
||||
main_hand: None,
|
||||
},
|
||||
false,
|
||||
);
|
||||
let movement = match scene.resolve_with_context(
|
||||
horizon_client::falling_block::bounds(f.position),
|
||||
wanted,
|
||||
0.0,
|
||||
false,
|
||||
context,
|
||||
) {
|
||||
Ok(movement) => movement,
|
||||
Err(error) => {
|
||||
*f = original;
|
||||
collision_error.get_or_insert(error);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
let next = Vec3::new(
|
||||
f.position.x + f.velocity.x,
|
||||
f.position.y + f.velocity.y,
|
||||
f.position.z + f.velocity.z,
|
||||
f.position.x + movement.x,
|
||||
f.position.y + movement.y,
|
||||
f.position.z + movement.z,
|
||||
);
|
||||
let below = self.world.block(
|
||||
next.x.floor() as i32,
|
||||
next.y.floor() as i32,
|
||||
next.z.floor() as i32,
|
||||
);
|
||||
let below_facts = self.res.facts.get(below);
|
||||
// PowderSnowBlock.getCollisionShape for a FallingBlockEntity: the
|
||||
// whole block, or 0.9 high after a fall of more than 2.5 blocks,
|
||||
// where the block it rests in is the snow itself.
|
||||
let powder_snow = &*below_facts.name == "minecraft:powder_snow";
|
||||
let sunk = powder_snow
|
||||
&& f.fall_distance > 2.5
|
||||
&& next.y - next.y.floor() < f64::from(0.9_f32);
|
||||
let solid =
|
||||
(below_facts.full_collision || powder_snow && f.fall_distance <= 2.5 || sunk)
|
||||
&& f.velocity.y < 0.0;
|
||||
let on_ground = wanted.y < 0.0 && movement.y != wanted.y;
|
||||
// FallingBlockEntity.tick: off the ground, it gives up after 600
|
||||
// ticks (100 outside the world) and drops itself as an item.
|
||||
let y = next.y.floor() as i32;
|
||||
let outside =
|
||||
y <= horizon_client::chunk::MIN_Y || y >= horizon_client::chunk::MIN_Y + 384;
|
||||
if !solid && (f.age > 100 && outside || f.age > 600) {
|
||||
if !on_ground && (f.age > 100 && outside || f.age > 600) {
|
||||
if f.drop_item && entity_drops {
|
||||
dropped.push((f.state, next));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if solid {
|
||||
let pos = [
|
||||
next.x.floor() as i32,
|
||||
next.y.floor() as i32 + i32::from(!sunk),
|
||||
next.z.floor() as i32,
|
||||
];
|
||||
let landing_y = if sunk {
|
||||
next.y.floor() + f64::from(0.9_f32)
|
||||
} else {
|
||||
f64::from(pos[1])
|
||||
};
|
||||
f.fall_distance += (f.position.y - landing_y).max(0.0);
|
||||
f.position = Vec3::new(next.x, landing_y, next.z);
|
||||
landed.push((f.clone(), pos));
|
||||
return false;
|
||||
}
|
||||
if next.y < f.position.y {
|
||||
f.fall_distance += f.position.y - next.y;
|
||||
}
|
||||
f.position = next;
|
||||
if on_ground {
|
||||
let pos = [
|
||||
f.position.x.floor() as i32,
|
||||
f.position.y.floor() as i32,
|
||||
f.position.z.floor() as i32,
|
||||
];
|
||||
landed.push((f.clone(), pos));
|
||||
return false;
|
||||
}
|
||||
if movement.x != wanted.x {
|
||||
f.velocity.x = 0.0;
|
||||
}
|
||||
if movement.y != wanted.y {
|
||||
f.velocity.y = 0.0;
|
||||
}
|
||||
if movement.z != wanted.z {
|
||||
f.velocity.z = 0.0;
|
||||
}
|
||||
f.velocity = Vec3::new(
|
||||
f.velocity.x * 0.98,
|
||||
f.velocity.y * 0.98,
|
||||
@@ -2387,7 +2499,11 @@ impl Game<'_> {
|
||||
for (block, pos) in landed {
|
||||
self.finish_falling_block(block, pos);
|
||||
}
|
||||
self.process_level_events()
|
||||
self.process_level_events()?;
|
||||
if let Some(error) = collision_error {
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn break_block(&mut self, pos: [i32; 3]) -> Result<()> {
|
||||
self.with_player_actor(|game| game.break_block_as_player(pos))
|
||||
|
||||
@@ -18,18 +18,28 @@ except ImportError:
|
||||
|
||||
def corpus(facts):
|
||||
def bits(v):return struct.pack('>d',float(v)).hex()
|
||||
def row(state,delta=(1,-0.08,0),step=0.6,grounded=True,context=2,offset=0,fail=-1,extra=(),entities=(),border=None):
|
||||
def row(state,delta=(1,-0.08,0),step=0.6,grounded=True,context=2,offset=0,fail=-1,extra=(),entities=(),border=None,box=None):
|
||||
blocks=[[x+offset,-1,z,facts['defaults']['minecraft:stone']] for x in range(-2,4) for z in range(-2,3)]
|
||||
blocks.append([offset,0,0,state]);blocks.extend(extra)
|
||||
return dict(chunks=[[x,z] for x in range((offset>>4)-1,(offset>>4)+2) for z in range(-1,2)],
|
||||
blocks=blocks,context=context,y=bits(0),entities=[0,0],fail=fail,limit=100000,suffocating=False,
|
||||
box=list(map(bits,[-0.6+offset,0,0.2,-0.2+offset,1.8,0.8])),
|
||||
box=list(map(bits,box if box is not None else [-0.6+offset,0,0.2,-0.2+offset,1.8,0.8])),
|
||||
movement=dict(delta=list(map(bits,delta)),step=bits(step),grounded=grounded,
|
||||
entities=[list(map(bits,b)) for b in entities],border=None if border is None else list(map(bits,border))))
|
||||
states=facts['states']
|
||||
for state in range(len(states)):
|
||||
yield row(state,context=state%4)
|
||||
defaults=facts['defaults']
|
||||
falling_surfaces=[('minecraft:stone_slab',0.5),('minecraft:oak_stairs',1.0),('minecraft:snow',0.125),
|
||||
('minecraft:oak_fence',1.5),('minecraft:iron_bars',1.0),('minecraft:soul_sand',0.875),
|
||||
('minecraft:farmland',0.9375)]
|
||||
for block_id,top in falling_surfaces:
|
||||
state=defaults[block_id]
|
||||
size=struct.unpack('>f',struct.pack('>f',0.98))[0]
|
||||
half=size/2
|
||||
bottom=top+0.1
|
||||
box=[0.5-half,bottom,0.5-half,0.5+half,bottom+size,0.5+half]
|
||||
yield row(state,delta=(0,-0.25,0),step=0,grounded=False,context=2,box=box)
|
||||
obstacles=[i for i,s in enumerate(states) if s['id'] in ['minecraft:oak_stairs','minecraft:stone_slab',
|
||||
'minecraft:snow','minecraft:scaffolding','minecraft:powder_snow','minecraft:water',
|
||||
'minecraft:shulker_box','minecraft:moving_piston','minecraft:piston','minecraft:soul_sand',
|
||||
@@ -114,7 +124,7 @@ def main():
|
||||
'definition_counts':dict(collections.Counter(definitions.values())),
|
||||
'native_chunk_storage':'ProtoChunk', 'rust_chunk_storage':'TerrainChunk', 'fixtures':count, 'sources_sha256':tested, 'fixtures_sha256':fixtures,
|
||||
'rust_output_sha256':lab.digest(build/'rust.txt', 'sha256'), 'references':references,
|
||||
'scope':'Actual native Entity.collide executes initial collision and step-up decisions over BlockCollisions, registered block shapes and resident ProtoChunk sections. Rust combines owned/fallible step resolution with registry-bound TerrainChunk scanning. All registered states are used as obstacles, plus step heights, falling/grounded motion, zero and underflow motion, partial blocks, fluids, dynamic blocks, ceilings, chunk boundaries and query failures. Entity collider lists and world-border proximity/shape are controlled inputs; query order and reuse, shape collection and resulting displacement are compared. Contexts are deliberately supplied empty/position/placement snapshots in both worlds. This does not establish entity broad-phase selection, actual world-border proximity, travel/gravity/velocity or grounded side effects, experimental minecarts or a playable game.'}
|
||||
'scope':'Actual native Entity.collide executes initial collision and step-up decisions over BlockCollisions, registered block shapes and resident ProtoChunk sections. Rust combines owned/fallible step resolution with registry-bound TerrainChunk scanning. All registered states are used as obstacles, plus step heights, falling/grounded motion, zero and underflow motion, partial blocks including exact falling-block-sized boxes over slabs, stairs, snow, fences, iron bars, soul sand and farmland, fluids, dynamic blocks, ceilings, chunk boundaries and query failures. Entity collider lists and world-border proximity/shape are controlled inputs; query order and reuse, shape collection and resulting displacement are compared. Contexts are deliberately supplied empty/position/placement snapshots in both worlds. This does not establish entity broad-phase selection, actual world-border proximity, travel/gravity/velocity or grounded side effects, experimental minecarts or a playable game.'}
|
||||
lab.write_json(report_path, report);print(f'Scoped world movement parity passed: {report_path}', flush=True)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user