Checkpoint resident crouching and pose integration in progress

Back up the current Rust pose, collision, ledge and native oracle work before further integration. This is a work-in-progress checkpoint: probe output, camera integration and differential validation are still pending; full vanilla parity is not claimed.
This commit is contained in:
Ludovic Marie committed 2026-10-07 18:08:04 +02:00
1 parent 24ce21abee
commit 20714b5d77
6 files changed
+144 -32

No files matched your search

+2 -2
View File
@@ -341,10 +341,10 @@ impl BlockLandingWorld for MotionWorld<'_, '_, '_> {
Ok(true)
}
fn entity_dimensions(&self) -> (f32, f32) {
(0.6, 1.8)
(self.player.dimensions.width, self.player.dimensions.height)
}
fn suppressing_bounce(&mut self) -> Result<bool> {
Ok(false)
Ok(self.player.shift_key_down)
}
fn damage_source(
&mut self,
+1
View File
@@ -9,6 +9,7 @@ pub mod input;
pub mod landing;
pub mod models;
pub mod motion;
pub mod pose;
pub mod sprint;
pub mod sprites;
pub mod world;
+40 -20
View File
@@ -14,6 +14,7 @@ use horizon_world::{
collision_context::{CollisionContext, CollisionEntity, CollisionItem},
entity_move::{self, CollisionFlags, EntityMoveWorld, MovementHistory, MoverType},
entity_obstruction::EntityIdentity,
edge_backoff::{self, EdgeBackoffInput, EdgeBackoffWorld},
ground_support::{GroundState, find_supporting_block},
living_air::{self, LivingAirWorld},
living_fall::ImpulseContext,
@@ -135,6 +136,10 @@ pub struct PlayerMotion {
pub jump_trigger_time: i32,
pub sprinting: bool,
pub sprint_input: crate::sprint::SprintState,
pub shift_key_down: bool,
pub local_crouching: bool,
pub pose: crate::pose::Pose,
pub dimensions: crate::pose::Dimensions,
pub speed: f32,
pub last_climbable: Option<BlockPos>,
pub needs_sync: bool,
@@ -186,6 +191,10 @@ impl PlayerMotion {
jump_trigger_time: 0,
sprinting: false,
sprint_input: crate::sprint::SprintState::default(),
shift_key_down: false,
local_crouching: false,
pose: crate::pose::Pose::Standing,
dimensions: crate::pose::Pose::Standing.dimensions(1.0),
speed: 0.0,
last_climbable: None,
needs_sync: false,
@@ -195,19 +204,7 @@ impl PlayerMotion {
})
}
pub fn bounds(&self) -> Aabb {
let half = f64::from(0.6_f32 / 2.0);
Aabb::new(
Vec3::new(
self.position.x - half,
self.position.y,
self.position.z - half,
),
Vec3::new(
self.position.x + half,
self.position.y + f64::from(1.8_f32),
self.position.z + half,
),
)
self.dimensions.bounds(self.position)
}
pub fn attribute(&mut self, name: &str) -> Result<f64> {
let key = self
@@ -256,6 +253,7 @@ impl PlayerMotion {
on_ground: self.ground.on_ground,
horizontal_collision: self.collision.horizontal,
minor_horizontal_collision: self.collision.minor,
moving_slowly: self.moving_slowly(),
..Default::default()
};
// The sprint setter edits attributes and flags, independent of local
@@ -304,7 +302,7 @@ impl MotionWorld<'_, '_, '_> {
CollisionEntity {
identity: EntityIdentity(0),
y: self.player.position.y,
descending: false,
descending: self.player.shift_key_down,
main_hand: Some(CollisionItem::EMPTY),
},
false,
@@ -316,6 +314,12 @@ impl MotionWorld<'_, '_, '_> {
binding: self.collision_binding,
}
}
pub(crate) fn no_player_collision(&mut self, bounds:Aabb)->Result<bool> {
let context=self.context();
Ok(world_movement::no_world_collision(&mut self.collisions(),world_movement::CollisionCheckRequest {
entity:EntityIdentity(0),context,bounds,candidate_budget:1_000_000,
})?)
}
fn block(&self, p: BlockPos) -> BlockStateId {
self.scene.block(p.x, p.y, p.z)
}
@@ -383,6 +387,7 @@ impl MotionWorld<'_, '_, '_> {
self.player.health.advance_clock();
let previous = self.player.position;
self.player_movement_tick(direction, jumping)?;
self.update_player_pose()?;
// The resident scene accepts the resolved local movement directly.
// Vanilla dispatches this separately from travel, on packet acceptance.
self.check_movement_statistics(previous.vector_to(self.player.position))?;
@@ -390,9 +395,12 @@ impl MotionWorld<'_, '_, '_> {
.food_tick(&self.resources.health, self.scene.health_rules)
}
pub fn controlled_simulation_tick(&mut self, keys: crate::sprint::Keys) -> Result<()> {
self.update_local_crouching()?;
self.player.sprint_input_tick(keys)?;
self.player.shift_key_down=keys.shift;
let sneak=if self.player.moving_slowly() {Some(self.player.attribute("minecraft:sneaking_speed")? as f32)}else{None};
self.simulation_tick(
crate::input::modify_input(keys.direction(), None, None),
crate::input::modify_input(keys.direction(), None, sneak),
keys.jump,
)
}
@@ -484,7 +492,7 @@ impl MovementStatisticsWorld for MotionWorld<'_, '_, '_> {
Ok(self.player.sprinting)
}
fn crouching(&mut self) -> Result<bool> {
Ok(false)
Ok(self.player.pose==crate::pose::Pose::Crouching)
}
fn fall_flying(&mut self) -> Result<bool> {
Ok(false)
@@ -509,6 +517,13 @@ impl MovementBlockProvider for MotionWorld<'_, '_, '_> {
Ok(self.block(p))
}
}
impl EdgeBackoffWorld for MotionWorld<'_, '_, '_> {
type Error=Box<dyn std::error::Error>;
fn max_up_step(&mut self)->Result<f32> {Ok(self.player.attribute("minecraft:step_height")? as f32)}
fn staying_on_ground_surface(&mut self)->Result<bool> {Ok(self.player.shift_key_down)}
fn on_ground(&mut self)->Result<bool> {Ok(self.player.ground.on_ground)}
fn no_collision(&mut self,bounds:Aabb)->Result<bool> {self.no_player_collision(bounds)}
}
impl EntityMoveWorld for MotionWorld<'_, '_, '_> {
type Error = Box<dyn std::error::Error>;
fn no_physics(&self) -> bool {
@@ -549,8 +564,13 @@ impl EntityMoveWorld for MotionWorld<'_, '_, '_> {
self.player.velocity = value;
Ok(())
}
fn back_off_edge(&mut self, movement: Vec3, _: MoverType) -> Result<Vec3> {
Ok(movement)
fn back_off_edge(&mut self, movement: Vec3, mover: MoverType) -> Result<Vec3> {
let input=EdgeBackoffInput {
movement,bounds:self.player.bounds(),flying:false,
self_or_player:matches!(mover,MoverType::SelfMovement|MoverType::Player),
fall_distance:self.player.fall_distance,query_budget:1_000_000,
};
edge_backoff::back_off_from_edge(self,input).map_err(|e|io::Error::other(e.to_string()).into())
}
fn collide(&mut self, movement: Vec3) -> Result<Vec3> {
let max_step = self.player.attribute("minecraft:step_height")? as f32;
@@ -698,7 +718,7 @@ impl EntityMoveWorld for MotionWorld<'_, '_, '_> {
impl RestitutionWorld for MotionWorld<'_, '_, '_> {
type Error = Box<dyn std::error::Error>;
fn suppressing_bounce(&mut self) -> Result<bool> {
Ok(false)
Ok(self.player.shift_key_down)
}
fn entity_bounciness(&mut self) -> Result<f64> {
self.player.attribute("minecraft:bounciness")
@@ -814,7 +834,7 @@ impl LivingAirWorld for MotionWorld<'_, '_, '_> {
== "minecraft:scaffolding")
}
fn suppressing_ladder_slide(&mut self) -> Result<bool> {
Ok(false)
Ok(self.player.shift_key_down)
}
fn is_player_class(&self) -> bool {
true
+77
View File
@@ -0,0 +1,77 @@
//! Resident player pose, native dimension arithmetic and dry pose-fit selection.
//! Full synchronized entity schemas, sleeping and fluid/flight lifecycles remain
//! separate from these stored movement fields.
use crate::{Result, motion::{MotionWorld, PlayerMotion}};
use horizon_core::geometry::{Aabb, Vec3};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Pose {
#[default]
Standing,
Crouching,
Swimming,
}
impl Pose {
pub fn name(self) -> &'static str {
match self { Self::Standing=>"STANDING",Self::Crouching=>"CROUCHING",Self::Swimming=>"SWIMMING" }
}
pub fn dimensions(self, scale: f32) -> Dimensions {
let (height,eye_height)=match self {Self::Standing=>(1.8,1.62),Self::Crouching=>(1.5,1.27),Self::Swimming=>(0.6,0.4)};
Dimensions { width:0.6*scale,height:height*scale,eye_height:eye_height*scale }
}
}
#[derive(Clone, Copy, Debug)]
pub struct Dimensions {
pub width:f32,
pub height:f32,
pub eye_height:f32,
}
impl Dimensions {
pub fn bounds(self, position:Vec3)->Aabb {
let half=f64::from(self.width/2.0);
Aabb::new(Vec3::new(position.x-half,position.y,position.z-half),
Vec3::new(position.x+half,position.y+f64::from(self.height),position.z+half))
}
}
impl PlayerMotion {
pub fn dimensions_for_pose(&mut self, pose:Pose)->Result<Dimensions> {
Ok(pose.dimensions(self.attribute("minecraft:scale")? as f32))
}
pub fn refresh_dimensions(&mut self)->Result<()> {
self.dimensions=self.dimensions_for_pose(self.pose)?;
Ok(())
}
pub fn set_pose(&mut self, pose:Pose)->Result<()> {
if self.pose!=pose {
self.pose=pose;
self.refresh_dimensions()?;
}
Ok(())
}
pub fn moving_slowly(&self)->bool {
self.local_crouching || self.pose==Pose::Swimming // dry scene: not in water
}
}
impl MotionWorld<'_, '_, '_> {
pub fn can_fit_pose(&mut self, pose:Pose)->Result<bool> {
let bounds=self.player.dimensions_for_pose(pose)?.bounds(self.player.position);
self.no_player_collision(bounds.deflate(Vec3::new(1.0e-7,1.0e-7,1.0e-7)))
}
/// LocalPlayer.aiStep computes crouching BEFORE KeyboardInput.tick, using
/// the old shift input. This is distinct from Entity's current pose.
pub fn update_local_crouching(&mut self)->Result<()> {
self.player.local_crouching=self.can_fit_pose(Pose::Crouching)?
&& (self.player.shift_key_down || !self.can_fit_pose(Pose::Standing)?);
Ok(())
}
/// Dry Player.updatePlayerPose after movement: crawl when neither standing
/// nor crouching fits; leave the current pose if even crawling cannot fit.
pub fn update_player_pose(&mut self)->Result<()> {
if !self.can_fit_pose(Pose::Swimming)? {return Ok(());}
let desired=if self.player.shift_key_down {Pose::Crouching}else{Pose::Standing};
let pose=if self.can_fit_pose(desired)? {desired}
else if self.can_fit_pose(Pose::Crouching)? {Pose::Crouching}
else {Pose::Swimming};
self.player.set_pose(pose)
}
}
@@ -112,6 +112,11 @@ public final class VanillaSceneMotionOracle {
// method; regular resident movement keeps its actual standing/dry state.
Integer statisticsFlags;
Object sprintController;
Pose scenePose=Pose.STANDING;
@Override public Pose getPose(){return scenePose;}
@Override public void setPose(Pose pose){if(scenePose!=pose){scenePose=pose;super.refreshDimensions();}}
public boolean fitPose(Pose pose){return super.canPlayerFitWithinBlocksAndEntitiesWhen(pose);}
public void poseTick(){updatePlayerPose();}
boolean statFlag(int bit){return (statisticsFlags&(1<<bit))!=0;}
SynchedEntityData.DataItem<Float> healthItem,absorptionItem;Level sceneLevel;Tracker tracker;net.minecraft.server.PlayerAdvancements advancements;
Actor(){super(null,null,null,null);}
@@ -140,7 +145,7 @@ public final class VanillaSceneMotionOracle {
@Override public AttributeMap getAttributes(){return attrs;}
@Override protected boolean getSharedFlag(int bit){return (flags&(1<<bit))!=0;}
@Override protected void setSharedFlag(int bit,boolean v){flags=(byte)(v?flags|(1<<bit):flags&~(1<<bit));}
@Override public void setPos(double x,double y,double z){write(position,this,new Vec3(x,y,z));setBoundingBox(EntityDimensions.scalable(.6f,1.8f).makeBoundingBox(position()));}
@Override public void setPos(double x,double y,double z){write(position,this,new Vec3(x,y,z));setBoundingBox(EntityDimensions.scalable(getBbWidth(),getBbHeight()).makeBoundingBox(position()));}
@Override public BlockPos blockPosition(){return BlockPos.containing(position());}
@Override public BlockState getInBlockState(){return level().getBlockState(blockPosition());}
@Override public ItemStack getMainHandItem(){return ItemStack.EMPTY;}
@@ -156,10 +161,6 @@ public final class VanillaSceneMotionOracle {
@Override public boolean isInLiquid(){return false;}
@Override public boolean isFallFlying(){return statisticsFlags!=null&&statFlag(8);}
@Override public boolean isSpectator(){return false;}
@Override public Pose getPose(){return Pose.STANDING;}
@Override public boolean isDescending(){return false;}
@Override public boolean isSuppressingBounce(){return false;}
@Override public boolean isSuppressingSlidingDownLadder(){return false;}
@Override public boolean isNoGravity(){return false;}
@Override public boolean hasEffect(Holder<MobEffect> effect){return false;}
@Override public MobEffectInstance getEffect(Holder<MobEffect> effect){return null;}
@@ -226,6 +227,7 @@ public final class VanillaSceneMotionOracle {
p.getEntityData().isDirty(),p.healthItem.isDirty(),p.absorptionItem.isDirty(),p.sceneLevel.time);
var food=p.getFoodData();out.printf("food %d %08x %d %d %d %s%n",food.getFoodLevel(),Float.floatToIntBits(food.getSaturationLevel()),foodTimer.getInt(food),p.tickCount,p.sceneLevel.difficulty.ordinal(),p.sceneLevel.rules.regenerate);
out.println("input "+p.isSprinting()+" "+(p.sprintController==null?"0 false false":p.sprintController.getClass().getMethod("state").invoke(p.sprintController)));
out.printf("pose %s %s %s %08x %08x %08x%n",p.getPose(),p.isShiftKeyDown(),p.sprintController!=null&&((Boolean)p.sprintController.getClass().getMethod("crouching").invoke(p.sprintController)),Float.floatToIntBits(p.getBbWidth()),Float.floatToIntBits(p.getBbHeight()),Float.floatToIntBits(p.getEyeHeight()));
}
@SuppressWarnings("unchecked") public static void main(String[] args)throws Exception{
net.minecraft.SharedConstants.tryDetectVersion();net.minecraft.server.Bootstrap.bootStrap();Path dir=Path.of(args[1]);
@@ -255,7 +257,8 @@ public final class VanillaSceneMotionOracle {
for(var line:(Iterable<String>)lines::iterator){
var row=JsonParser.parseString(line).getAsJsonObject();var p=(Actor)allocate.invoke(unsafe,Actor.class);p.configure(level);p.events=new ArrayList<>();level.actor=p;
write(field(Entity.class,"type"),p,EntityTypes.PLAYER);
write(field(Entity.class,"passengers"),p,com.google.common.collect.ImmutableList.of());write(field(Entity.class,"dimensions"),p,EntityDimensions.scalable(.6f,1.8f));
p.scenePose=Pose.STANDING;
write(field(Entity.class,"passengers"),p,com.google.common.collect.ImmutableList.of());write(field(Entity.class,"dimensions"),p,p.getDefaultDimensions(Pose.STANDING));write(field(Entity.class,"eyeHeight"),p,1.62f);
write(field(Entity.class,"interpolationHandler"),p,InterpolationHandler.NO_OP);
var equipment=new EntityEquipment();write(field(LivingEntity.class,"equipment"),p,equipment);
write(field(Player.class,"inventory"),p,new Inventory(p,equipment));
@@ -274,10 +277,14 @@ public final class VanillaSceneMotionOracle {
p.setDeltaMovement(Vec3.ZERO);p.setPos(VanillaSceneCollisionOracle.vector(row.get("position")));
for(var value:row.getAsJsonArray("ops")){
var op=value.getAsJsonObject();switch(op.get("mode").getAsString()){
case "shift"->p.setShiftKeyDown(op.get("value").getAsBoolean());
case "pose_tick"->p.poseTick();
case "refresh_dimensions"->p.refreshDimensions();
case "pose"->p.setPose(Pose.valueOf(op.get("value").getAsString()));
case "controlled_tick"->{
if(p.sprintController==null)p.sprintController=Class.forName("dev.horizonlab.parity.VanillaSprintInputOracle$Controller").getConstructor(ServerPlayer.class).newInstance(p);
int mask=op.get("value").getAsInt();var direction=(net.minecraft.world.phys.Vec2)p.sprintController.getClass().getMethod("step",int.class).invoke(p.sprintController,mask);
p.clock();var before=p.position();p.playerMovementTick(new Vec3(direction.x,0,direction.y),(mask&16)!=0);p.checkMovementStatistics(p.getX()-before.x,p.getY()-before.y,p.getZ()-before.z);p.getFoodData().tick(p);
p.clock();var before=p.position();p.playerMovementTick(new Vec3(direction.x,0,direction.y),(mask&16)!=0);p.poseTick();p.checkMovementStatistics(p.getX()-before.x,p.getY()-before.y,p.getZ()-before.z);p.getFoodData().tick(p);
}
case "health"->p.setHealth(single(op.get("value")));
case "absorption"->p.setAbsorptionAmount(single(op.get("value")));
@@ -290,7 +297,7 @@ public final class VanillaSceneMotionOracle {
case "eat"->p.getFoodData().eat(op.get("nutrition").getAsInt(),single(op.get("value")));
case "food_tick"->p.getFoodData().tick(p);
case "peaceful"->p.peaceful();
case "simulation_tick"->{p.clock();var before=p.position();p.playerMovementTick(VanillaSceneCollisionOracle.vector(op.get("value")),op.get("jumping").getAsBoolean());p.checkMovementStatistics(p.getX()-before.x,p.getY()-before.y,p.getZ()-before.z);p.getFoodData().tick(p);}
case "simulation_tick"->{p.clock();var before=p.position();p.playerMovementTick(VanillaSceneCollisionOracle.vector(op.get("value")),op.get("jumping").getAsBoolean());p.poseTick();p.checkMovementStatistics(p.getX()-before.x,p.getY()-before.y,p.getZ()-before.z);p.getFoodData().tick(p);}
case "movement_statistics"->{var delta=VanillaSceneCollisionOracle.vector(op.get("value"));p.checkMovementStatistics(delta.x,delta.y,delta.z);}
case "statistics_context"->{
p.statisticsFlags=op.get("flags").getAsInt();p.getAbilities().invulnerable=op.get("invulnerable").getAsBoolean();
@@ -51,12 +51,17 @@ public final class VanillaSprintInputOracle {
@Override public boolean isUsingItem(){return flag(7);}
@Override public boolean isFallFlying(){return flag(9);}
@Override public boolean isUnderWater(){return flag(10);}
@Override public boolean isMovingSlowly(){return flag(11);}
@Override public boolean isMovingSlowly(){return controlled==null?flag(11):super.isMovingSlowly();}
@Override public Pose getPose(){return controlled==null?Pose.STANDING:controlled.getPose();}
@Override public double getAttributeValue(Holder<net.minecraft.world.entity.ai.attributes.Attribute> attribute){return controlled==null?super.getAttributeValue(attribute):controlled.getAttributeValue(attribute);}
@Override public boolean isSwimming(){return flag(12);}
@Override public boolean isInWater(){return flag(13);}
@Override public boolean onGround(){return flag(14);}
@Override public boolean isSleeping(){return false;}
@Override public boolean canPlayerFitWithinBlocksAndEntitiesWhen(Pose pose){return true;}
@Override public boolean canPlayerFitWithinBlocksAndEntitiesWhen(Pose pose){
if(controlled==null)return true;
try{var method=Player.class.getDeclaredMethod("canPlayerFitWithinBlocksAndEntitiesWhen",Pose.class);method.setAccessible(true);return (Boolean)method.invoke(controlled,pose);}catch(ReflectiveOperationException e){throw new AssertionError(e);}
}
@Override public float getEffectBlendFactor(Holder<MobEffect> effect,float partial){return 0;}
@Override public boolean isEyeInFluid(TagKey<Fluid> tag){return false;}
@Override public boolean onClimbable(){return false;}
@@ -94,9 +99,11 @@ public final class VanillaSprintInputOracle {
p.horizontalCollision=p.controlled.horizontalCollision;p.minorHorizontalCollision=p.controlled.minorHorizontalCollision;
p.calls.clear();for(int i=0;i<keys.length;i++)down.setBoolean(keys[i],(mask&(1<<i))!=0);
try{p.aiStep();throw new AssertionError("missing sprint boundary");}catch(Boundary expected){}
p.controlled.setShiftKeyDown((mask&32)!=0);
return (net.minecraft.world.phys.Vec2)modify.invoke(p,p.input.getMoveVector());
}
public String state(){return p.trigger()+" "+p.input.hasForwardImpulse()+" "+p.input.keyPresses.shift();}
public boolean crouching(){return p.isCrouching();}
}
static void initialize()throws Exception{
if(unsafe!=null)return;