diff --git a/crates/horizon-client/examples/probe/scene_motion_probe.rs b/crates/horizon-client/examples/probe/scene_motion_probe.rs index 43564d3bcf..c1e65e45db 100644 --- a/crates/horizon-client/examples/probe/scene_motion_probe.rs +++ b/crates/horizon-client/examples/probe/scene_motion_probe.rs @@ -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 = None; + // The fixture's other entities, as the boxes `Level.getEntityCollisions` + // selects from: the resident scene holds only the player. + let entities: Vec = 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, diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSceneMotionOracle.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSceneMotionOracle.java index 51026150a6..f65f980c84 100644 --- a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSceneMotionOracle.java +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSceneMotionOracle.java @@ -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 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 boxes=new ArrayList<>(); + @Override public List getEntityCollisions(Entity e,AABB b){ + if(b.getSize()<1e-7)return List.of(); + var shapes=new ArrayList(); + for(var box:boxes)if(box.intersects(b.inflate(1e-7)))shapes.add(Shapes.create(box)); + return shapes; + } @Override public List getPushableEntities(Entity e,AABB b){return List.of();} @Override public List 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(healthKey,1f);items[absorptionKey.id()]=p.absorptionItem=new SynchedEntityData.DataItem(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()); diff --git a/tools/test_native_scene_motion.py b/tools/test_native_scene_motion.py index adef16e1d2..ac56e9c600 100644 --- a/tools/test_native_scene_motion.py +++ b/tools/test_native_scene_motion.py @@ -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)