mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
test: the scene motion corpus resolves the player against entity boxes
`MotionWorld.entities` was already real — `EntityCollisions` inflates the entity query by 1e-7 and turns each box into a collision shape — but nothing ever reached it: `VanillaSceneMotionOracle.Level.getEntityCollisions` returned `List.of()` and the probe passed `entities: &[]`, so no fixture resolved the player against another body's box. `entity_box_corpus` adds 29 fixtures of boxes: a wall whose near face sits on the player's leading face plus or minus the amounts the 1e-7 inflation decides between (0, 5e-8, 1e-7, 2e-7, 1e-4, signed zero, half a block), a box low enough to step onto and ones too tall, a box the player starts inside, a gap narrower than the player, diagonal and sprinting approaches, and boxes met while jumping and overhead. The oracle answers `getEntityCollisions` from them the way `ColliderGetter` does: the `getSize() < 1e-7` shortcut, `bounds.inflate(1e-7)`, then one `Shapes.create(box)` per intersecting body. Dropping the slice again fails at output line 4263: vanilla stops at the wall with `horizontalCollision` true and z held at 4039800000000000, the probe walks through with z 40398504816ec226. With the slice the suite matches at 2,853,483 states per side. The corpus claims collisions only. `LivingEntity.pushEntities` gives the bodies a velocity change and can cram the player, and the snapshot carries player state alone, so `getPushableEntities` stays empty and nothing here claims the pushing half.
This commit is contained in:
1 parent
47c09e613f
commit
bd143d0b43
3 files changed
+79
-2
No files matched your search
@@ -7,6 +7,7 @@ use horizon_client::{
|
||||
motion::{LAVA, MotionEvent, MotionResources, MotionWorld, PlayerMotion, WATER},
|
||||
world::World,
|
||||
};
|
||||
use horizon_core::geometry::Aabb;
|
||||
use horizon_core::geometry::Vec3;
|
||||
use horizon_core::{position::BlockPos, random::RandomSource};
|
||||
use horizon_world::attributes::{Modifier, Operation};
|
||||
@@ -299,9 +300,25 @@ pub fn main() -> Result<()> {
|
||||
// portal_tick that went through.
|
||||
let mut portal = horizon_client::nether_portal::PortalState::default();
|
||||
let mut travel: Option<horizon_core::position::BlockPos> = None;
|
||||
// The fixture's other entities, as the boxes `Level.getEntityCollisions`
|
||||
// selects from: the resident scene holds only the player.
|
||||
let entities: Vec<Aabb> = f["entities"]
|
||||
.as_array()
|
||||
.map(|boxes| {
|
||||
boxes
|
||||
.iter()
|
||||
.map(|b| {
|
||||
Aabb::new(
|
||||
Vec3::new(number(&b[0]), number(&b[1]), number(&b[2])),
|
||||
Vec3::new(number(&b[3]), number(&b[4]), number(&b[5])),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for op in f["ops"].as_array().unwrap() {
|
||||
let mut world = MotionWorld {
|
||||
entities: &[],
|
||||
entities: &entities,
|
||||
player: &mut player,
|
||||
scene: &mut scene,
|
||||
assets: &assets,
|
||||
|
||||
@@ -65,6 +65,7 @@ public final class VanillaSceneMotionOracle {
|
||||
@Override public void recordDamage(DamageSource source,float amount){actor.events.add(String.format("record-damage %08x",Float.floatToIntBits(amount)));}
|
||||
}
|
||||
static float single(JsonElement v){return Float.intBitsToFloat(Integer.parseUnsignedInt(v.getAsString(),16));}
|
||||
static double dbl(JsonElement v){return Double.longBitsToDouble(Long.parseUnsignedLong(v.getAsString(),16));}
|
||||
static void write(Field f,Object o,Object value){try{f.set(o,value);}catch(Exception e){throw new AssertionError(e);}}
|
||||
static Field field(Class<?> c,String n)throws Exception{var f=c.getDeclaredField(n);f.setAccessible(true);return f;}
|
||||
static String blockPos(BlockPos p){return p.getX()+","+p.getY()+","+p.getZ();}
|
||||
@@ -125,7 +126,20 @@ public final class VanillaSceneMotionOracle {
|
||||
@Override public void addParticle(net.minecraft.core.particles.ParticleOptions particle,double x,double y,double z,double xd,double yd,double zd){
|
||||
actor.events.add("splash-particle "+(particle==net.minecraft.core.particles.ParticleTypes.BUBBLE?"bubble":particle==net.minecraft.core.particles.ParticleTypes.SPLASH?"splash":"unknown")+" "+vectorBits(new Vec3(x,y,z))+" "+vectorBits(new Vec3(xd,yd,zd)));
|
||||
}
|
||||
@Override public List<VoxelShape> getEntityCollisions(Entity e,AABB b){return List.of();}
|
||||
// CollisionGetter.getEntityCollisions over the fixture's other entities,
|
||||
// which the scene holds only as boxes: the 1e-7 query inflation, the
|
||||
// degenerate-bounds shortcut and the per-entity shape, without needing
|
||||
// Entity objects for bodies whose velocity the scene never compares.
|
||||
// getPushableEntities stays empty: LivingEntity.pushEntities would push
|
||||
// those bodies (and can cram the player), and the probe carries player
|
||||
// state only, so the corpus does not claim the pushing half.
|
||||
List<AABB> boxes=new ArrayList<>();
|
||||
@Override public List<VoxelShape> getEntityCollisions(Entity e,AABB b){
|
||||
if(b.getSize()<1e-7)return List.of();
|
||||
var shapes=new ArrayList<VoxelShape>();
|
||||
for(var box:boxes)if(box.intersects(b.inflate(1e-7)))shapes.add(Shapes.create(box));
|
||||
return shapes;
|
||||
}
|
||||
@Override public List<Entity> getPushableEntities(Entity e,AABB b){return List.of();}
|
||||
@Override public List<Entity> getEntities(Entity e,AABB b){return e!=actor&&b.intersects(actor.getBoundingBox())?List.of(actor):List.of();}
|
||||
@Override public WorldBorder getWorldBorder(){return border;}
|
||||
@@ -391,6 +405,9 @@ public final class VanillaSceneMotionOracle {
|
||||
items[healthKey.id()]=p.healthItem=new SynchedEntityData.DataItem<Float>(healthKey,1f);items[absorptionKey.id()]=p.absorptionItem=new SynchedEntityData.DataItem<Float>(absorptionKey,0f);
|
||||
write(field(Entity.class,"entityData"),p,dataCtor.newInstance(p,items));p.setHealth(p.getMaxHealth());p.events.clear();level.time=0;level.rules.regenerate=true;level.difficulty=net.minecraft.world.Difficulty.NORMAL;
|
||||
p.setDeltaMovement(Vec3.ZERO);p.setPos(VanillaSceneCollisionOracle.vector(row.get("position")));
|
||||
// The fixture's other entities as boxes: min xyz then max xyz, exact bits.
|
||||
level.boxes=new ArrayList<>();
|
||||
if(row.has("entities"))for(var box:row.getAsJsonArray("entities")){var c=box.getAsJsonArray();level.boxes.add(new AABB(dbl(c.get(0)),dbl(c.get(1)),dbl(c.get(2)),dbl(c.get(3)),dbl(c.get(4)),dbl(c.get(5))));}
|
||||
for(var value:row.getAsJsonArray("ops")){
|
||||
var op=value.getAsJsonObject();switch(op.get("mode").getAsString()){
|
||||
case "shift"->p.setShiftKeyDown(op.get("value").getAsBoolean());
|
||||
|
||||
@@ -480,8 +480,51 @@ def swim_corpus():
|
||||
ops+=[op('air_tick') for _ in range(40)]+[tick() for _ in range(40)]+clear((21,4,21),(29,7,29))+fill('stone',(25,3,25),(25,3,25))
|
||||
yield dict(position=vec((25.5,5,25.5)),ops=ops)
|
||||
|
||||
def entity_box_corpus():
|
||||
"""Other entities as boxes the player walks into. `Level.getEntityCollisions`
|
||||
queries with the box inflated by 1e-7, so the corpus puts shapes exactly on
|
||||
that boundary; `Entity.move` then resolves against them like block shapes.
|
||||
The bodies themselves are not simulated: the snapshot carries player state,
|
||||
and LivingEntity.pushEntities (their velocity, cramming) is out of scope."""
|
||||
def tick(direction=(0,0,.98),jumping=False):
|
||||
return op('simulation_tick',vec(direction),jumping=jumping)
|
||||
def box(minimum,maximum):
|
||||
return dict(entities=[vec(minimum)+vec(maximum)])
|
||||
# A one-block wall in front of a player walking along +z, its near face put
|
||||
# at the player's leading face (z 25.8) plus or minus the amounts the 1e-7
|
||||
# query inflation decides between: strict AABB intersection keeps the wall
|
||||
# out at 0 and 5e-8, and only the inflation admits it there.
|
||||
for delta in [-1e-4,-2e-7,-1e-7,-5e-8,-0.,0.,5e-8,1e-7,2e-7,1e-4,1.]:
|
||||
z=25.8+delta
|
||||
yield dict(position=vec((25.5,4,25.5)),**box((25,4,z),(26,6,z+1)),
|
||||
ops=[op('speed',single(.2))]+[tick() for _ in range(30)])
|
||||
# A low box the player steps onto, and the same box too tall to step.
|
||||
for height in [.5,.6,1.]:
|
||||
yield dict(position=vec((25.5,4,25.5)),**box((24,4,26),(27,4+height,27)),
|
||||
ops=[op('speed',single(.2))]+[tick() for _ in range(30)])
|
||||
# A box the player starts inside: Entity.move's collect-collisions path with
|
||||
# a shape that already overlaps the moving bounds.
|
||||
for inset in [0.,.25,.5,1.]:
|
||||
yield dict(position=vec((25.5,4,25.5)),**box((25.5-inset,4,25.5-inset),(25.5+inset,6,25.5+inset)),
|
||||
ops=[op('speed',single(.2))]+[tick() for _ in range(20)]+[tick((0,0,-.98)) for _ in range(20)])
|
||||
# Two boxes leaving a gap narrower than the player, and a diagonal approach
|
||||
# into their corner (one axis blocked, the other sliding).
|
||||
for width in [0.,.2,.5]:
|
||||
yield dict(position=vec((25.5,4,20.5)),
|
||||
entities=[vec((20,4,25)) + vec((26,6,26)), vec((20,4,26+width)) + vec((26,6,27+width))],
|
||||
ops=[op('speed',single(.2)),op('yaw',single(0))]+[tick() for _ in range(60)])
|
||||
for yaw in [0,45,90,225]:
|
||||
yield dict(position=vec((25.5,4,24.5)),**box((24,4,26),(27,6,27)),
|
||||
ops=[op('speed',single(.2)),op('yaw',single(yaw)),op('sprint',True)]+[tick((0,0,.98)) for _ in range(40)])
|
||||
# A box that only meets the player while jumping over it, and one above the
|
||||
# head so the jump is cut short.
|
||||
for height in [.5,1.5,2.5,3.5]:
|
||||
yield dict(position=vec((25.5,4,25.5)),entities=[vec((25,4,26))+vec((26,4+height,27)),vec((25,4+height+1.8,26))+vec((26,4+height+2.8,27))],
|
||||
ops=[op('speed',single(.2))]+[tick(jumping=i%20==0) for i in range(60)])
|
||||
|
||||
def corpus():
|
||||
yield from flight_corpus()
|
||||
yield from entity_box_corpus()
|
||||
yield from attribute_corpus(False)
|
||||
yield from portal_corpus()
|
||||
yield from pose_corpus(False)
|
||||
|
||||
Reference in new issue
Block a user