diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs
index dc76441b..115c7a63 100644
--- a/MinecraftClient/McClient.cs
+++ b/MinecraftClient/McClient.cs
@@ -685,6 +685,7 @@ namespace MinecraftClient
playerPhysics.SetPosition(location.X, location.Y, location.Z);
playerPhysics.Yaw = playerYaw;
playerPhysics.Pitch = playerPitch;
+ playerPhysics.DebugLog = msg => Log.Debug(msg);
physicsInitialized = true;
}
diff --git a/MinecraftClient/Physics/PhysicsConsts.cs b/MinecraftClient/Physics/PhysicsConsts.cs
index 95cb02c1..c2b32b13 100644
--- a/MinecraftClient/Physics/PhysicsConsts.cs
+++ b/MinecraftClient/Physics/PhysicsConsts.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace MinecraftClient.Physics
{
///
@@ -6,12 +8,19 @@ namespace MinecraftClient.Physics
///
public static class PhysicsConsts
{
- // --- Player dimensions ---
+ // --- Player dimensions per pose (vanilla Avatar.POSES, 26.1) ---
public const double PlayerWidth = 0.6;
- public const double PlayerHeight = 1.8;
- public const double PlayerSneakHeight = 1.5;
- public const double PlayerSwimHeight = 0.6;
- public const double PlayerEyeHeight = 1.62;
+ public const double PlayerStandingHeight = 1.8;
+ public const double PlayerStandingEyeHeight = 1.62;
+ public const double PlayerCrouchingHeight = 1.5;
+ public const double PlayerCrouchingEyeHeight = 1.27;
+ public const double PlayerSwimmingHeight = 0.6;
+ public const double PlayerSwimmingEyeHeight = 0.4;
+
+ [Obsolete("Use PlayerStandingHeight instead")]
+ public const double PlayerHeight = PlayerStandingHeight;
+ [Obsolete("Use PlayerStandingEyeHeight instead")]
+ public const double PlayerEyeHeight = PlayerStandingEyeHeight;
// --- Gravity ---
public const double DefaultGravity = 0.08;
diff --git a/MinecraftClient/Physics/PlayerPhysics.cs b/MinecraftClient/Physics/PlayerPhysics.cs
index bf18429f..2081bb7b 100644
--- a/MinecraftClient/Physics/PlayerPhysics.cs
+++ b/MinecraftClient/Physics/PlayerPhysics.cs
@@ -4,9 +4,9 @@ using MinecraftClient.Mapping;
namespace MinecraftClient.Physics
{
///
- /// Core physics tick engine for the player, faithfully replicating vanilla 1.21.11 physics.
+ /// Core physics tick engine for the player, faithfully replicating vanilla 1.21.11+ physics.
/// Mirrors the combined logic of Entity.move(), LivingEntity.aiStep()/travel()/travelInAir(),
- /// Player.travel(), and LocalPlayer.aiStep().
+ /// Player.travel(), Player.updatePlayerPose(), and LocalPlayer.aiStep().
///
public class PlayerPhysics
{
@@ -33,15 +33,34 @@ namespace MinecraftClient.Physics
public bool Sneaking;
public bool CreativeFlying;
public bool InWater;
+ public bool IsUnderWater;
public bool InLava;
public bool OnClimbable;
public bool HasSlowFalling;
public bool HasLevitation;
public int LevitationAmplifier;
- // Player dimensions
- public double PlayerWidth = PhysicsConsts.PlayerWidth;
- public double PlayerHeight = PhysicsConsts.PlayerHeight;
+ // --- Pose system (vanilla Player.updatePlayerPose / Avatar.POSES) ---
+ public EntityPose CurrentPose { get; private set; } = EntityPose.Standing;
+ private EntityPose previousPose = EntityPose.Standing;
+
+ public double PlayerWidth => PhysicsConsts.PlayerWidth;
+
+ public double PlayerHeight => CurrentPose switch
+ {
+ EntityPose.Sneaking => PhysicsConsts.PlayerCrouchingHeight,
+ EntityPose.Swimming or EntityPose.FallFlying or EntityPose.SpinAttack
+ => PhysicsConsts.PlayerSwimmingHeight,
+ _ => PhysicsConsts.PlayerStandingHeight
+ };
+
+ public double EyeHeight => CurrentPose switch
+ {
+ EntityPose.Sneaking => PhysicsConsts.PlayerCrouchingEyeHeight,
+ EntityPose.Swimming or EntityPose.FallFlying or EntityPose.SpinAttack
+ => PhysicsConsts.PlayerSwimmingEyeHeight,
+ _ => PhysicsConsts.PlayerStandingEyeHeight
+ };
// Anti-jump-spam
private int noJumpDelay;
@@ -52,6 +71,11 @@ namespace MinecraftClient.Physics
// Movement speed attribute (base = 0.1 for players)
public float MovementSpeed = 0.1f;
+ ///
+ /// Debug log callback. Set from McClient to route messages through MCC's logger.
+ ///
+ public Action? DebugLog;
+
///
/// Get the player's bounding box at current position
///
@@ -67,6 +91,9 @@ namespace MinecraftClient.Physics
{
TickCount++;
+ // Update pose (vanilla Player.updatePlayerPose)
+ UpdatePlayerPose(world);
+
// Velocity threshold zeroing (LivingEntity.aiStep)
ZeroTinyVelocity();
@@ -81,6 +108,14 @@ namespace MinecraftClient.Physics
if (noJumpDelay > 0)
noJumpDelay--;
+
+ // Periodic state dump every 5 seconds (100 ticks)
+ if (DebugLog is not null && TickCount % 100 == 0)
+ {
+ DebugLog($"[Physics] tick={TickCount} pos={Position} vel={DeltaMovement} " +
+ $"ground={OnGround} pose={CurrentPose} fall={FallDistance:F2} " +
+ $"water={InWater} underwater={IsUnderWater} swim={IsSwimming()} sneak={Sneaking}");
+ }
}
///
@@ -240,6 +275,9 @@ namespace MinecraftClient.Physics
// Block speed factor (soul sand, honey, etc.)
ApplyBlockSpeedFactor(world);
+
+ // SlimeBlock.stepOn: slow horizontal movement when walking on slime
+ ApplySlimeStepOn(world);
}
///
@@ -353,12 +391,6 @@ namespace MinecraftClient.Physics
double resolvedLenSqr = resolved.LengthSqr();
if (resolvedLenSqr > 1.0E-7 || movement.LengthSqr() - resolvedLenSqr < 1.0E-7)
{
- // Fall distance reset via trace (simplified: reset on hitting ground)
- if (FallDistance != 0.0 && resolvedLenSqr >= 1.0)
- {
- // Simplified: just check vertical collision
- }
-
Position = Position.Add(resolved);
}
@@ -385,13 +417,46 @@ namespace MinecraftClient.Physics
blockedZ ? 0 : DeltaMovement.Z);
}
+ // Vanilla: Block.updateEntityMovementAfterFallOn -> SlimeBlock.bounceUp
if (VerticalCollision)
+ UpdateMovementAfterFallOn(world);
+ }
+
+ ///
+ /// Vanilla Block.updateEntityMovementAfterFallOn / SlimeBlock.bounceUp.
+ /// Called when vertical collision is detected. Handles slime block bounce.
+ ///
+ private void UpdateMovementAfterFallOn(World world)
+ {
+ Location belowFeet = new(Position.X, Position.Y - 0.2, Position.Z);
+ Material landedOn = world.GetBlock(belowFeet).Type;
+
+ if (landedOn == Material.SlimeBlock && !IsSuppressingBounce())
{
- // Slime block bounce would go here; for now just zero Y
+ double vy = DeltaMovement.Y;
+ if (vy < 0.0)
+ {
+ // LivingEntity bounce factor = 1.0
+ DeltaMovement = new Vec3d(DeltaMovement.X, -vy, DeltaMovement.Z);
+ DebugLog?.Invoke($"[Physics] Slime bounce! vy={vy:F4} -> {-vy:F4} at {Position}");
+ }
+ else
+ {
+ DeltaMovement = new Vec3d(DeltaMovement.X, 0, DeltaMovement.Z);
+ }
+ }
+ else
+ {
+ // Default: zero vertical velocity
DeltaMovement = new Vec3d(DeltaMovement.X, 0, DeltaMovement.Z);
}
}
+ ///
+ /// Vanilla Entity.isSuppressingBounce() - sneaking suppresses slime bounce.
+ ///
+ private bool IsSuppressingBounce() => Sneaking;
+
///
/// Sneak edge detection: prevent walking off edges while sneaking.
/// Equivalent to Player.maybeBackOffFromEdge(Vec3, MoverType).
@@ -507,6 +572,26 @@ namespace MinecraftClient.Physics
}
}
+ ///
+ /// Vanilla SlimeBlock.stepOn: reduces horizontal speed when walking on slime blocks.
+ /// Triggered when vertical velocity is small and player is not sneaking.
+ ///
+ private void ApplySlimeStepOn(World world)
+ {
+ if (!OnGround) return;
+
+ Location belowFeet = new(Position.X, Position.Y - 0.5000010, Position.Z);
+ if (world.GetBlock(belowFeet).Type != Material.SlimeBlock) return;
+
+ double absDeltaY = Math.Abs(DeltaMovement.Y);
+ if (absDeltaY >= 0.1 || Sneaking) return;
+
+ double scale = 0.4 + absDeltaY * 0.2;
+ DeltaMovement = DeltaMovement.Multiply(scale, 1.0, scale);
+
+ DebugLog?.Invoke($"[Physics] Slime stepOn slowdown: scale={scale:F3}, vel={DeltaMovement}");
+ }
+
///
/// Get friction value for a material. Default 0.6, special blocks differ.
///
@@ -549,10 +634,84 @@ namespace MinecraftClient.Physics
InWater = feetBlock == Material.Water || headBlock == Material.Water
|| feetBlock == Material.BubbleColumn;
+ IsUnderWater = headBlock == Material.Water;
InLava = feetBlock == Material.Lava || headBlock == Material.Lava;
OnClimbable = feetBlock.CanBeClimbedOn();
}
+ // ==================== Pose System ====================
+
+ ///
+ /// Vanilla Player.updatePlayerPose().
+ /// Determines the correct pose based on player state and space constraints.
+ /// Forces crawling (Swimming pose on land) when standing/crouching does not fit.
+ ///
+ private void UpdatePlayerPose(World world)
+ {
+ EntityPose desired = GetDesiredPose();
+ EntityPose actual;
+
+ if (CanPlayerFitWithPose(world, EntityPose.Swimming))
+ {
+ if (CanPlayerFitWithPose(world, desired))
+ actual = desired;
+ else if (CanPlayerFitWithPose(world, EntityPose.Sneaking))
+ actual = EntityPose.Sneaking;
+ else
+ actual = EntityPose.Swimming;
+ }
+ else
+ {
+ actual = desired;
+ }
+
+ if (actual != previousPose)
+ {
+ DebugLog?.Invoke($"[Physics] Pose: {previousPose} -> {actual} (desired={desired}, " +
+ $"height={GetHeightForPose(actual):F1}, pos={Position})");
+ previousPose = actual;
+ }
+
+ CurrentPose = actual;
+ }
+
+ ///
+ /// Vanilla Player.getDesiredPose() -- determines what pose the player wants.
+ ///
+ private EntityPose GetDesiredPose()
+ {
+ if (IsSwimming())
+ return EntityPose.Swimming;
+ if (Sneaking && !CreativeFlying)
+ return EntityPose.Sneaking;
+ return EntityPose.Standing;
+ }
+
+ ///
+ /// Vanilla Entity.isSwimming() for players: sprinting underwater and not flying.
+ ///
+ private bool IsSwimming() => !CreativeFlying && Sprinting && IsUnderWater;
+
+ ///
+ /// Check if the player can fit at current position with the given pose's dimensions.
+ /// Vanilla Player.canPlayerFitWithinBlocksAndEntitiesWhen(Pose).
+ ///
+ private bool CanPlayerFitWithPose(World world, EntityPose pose)
+ {
+ double height = GetHeightForPose(pose);
+ Aabb box = Aabb.OfSize(Position.X, Position.Y, Position.Z, PlayerWidth, height);
+ Aabb deflated = box.Deflate(1.0E-7, 1.0E-7, 1.0E-7);
+ return CollisionDetector.NoCollision(world, deflated);
+ }
+
+ private static double GetHeightForPose(EntityPose pose) => pose switch
+ {
+ EntityPose.Sneaking => PhysicsConsts.PlayerCrouchingHeight,
+ EntityPose.Swimming or EntityPose.FallFlying or EntityPose.SpinAttack
+ => PhysicsConsts.PlayerSwimmingHeight,
+ _ => PhysicsConsts.PlayerStandingHeight
+ };
+
///
/// Set position from server teleport / initial spawn.
///