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. ///