using System; using System.Collections.Generic; using MinecraftClient.Mapping; namespace MinecraftClient.Physics { /// /// Performs AABB collision detection against the block world. /// Mirrors Entity.collide(), collideBoundingBox(), collideWithShapes() from vanilla MC. /// public static class CollisionDetector { /// /// Resolve movement with full collision detection including step-up. /// This is the main entry point, equivalent to Entity.collide(Vec3). /// public static Vec3d Collide(World world, Aabb entityBox, Vec3d movement, bool onGround, float maxUpStep) { if (movement.LengthSqr() == 0.0) return movement; // Collect block collision shapes in the movement path var colliders = CollectBlockColliders(world, entityBox.ExpandTowards(movement)); Vec3d resolved = CollideWithShapes(movement, entityBox, colliders); bool blockedX = Math.Abs(movement.X - resolved.X) > 1.0E-5; bool blockedZ = Math.Abs(movement.Z - resolved.Z) > 1.0E-5; bool blockedY = movement.Y != resolved.Y; bool hitGroundDuringMove = blockedY && movement.Y < 0.0; // Step-up logic: if blocked horizontally and on ground or just landed if (maxUpStep > 0.0f && (hitGroundDuringMove || onGround) && (blockedX || blockedZ)) { // Try stepping up Aabb stepBase = hitGroundDuringMove ? entityBox.Move(0, resolved.Y, 0) : entityBox; Aabb expanded = stepBase.ExpandTowards(movement.X, maxUpStep, movement.Z) .ExpandTowards(0, hitGroundDuringMove ? 0 : -1.0E-5, 0); var stepColliders = CollectBlockColliders(world, expanded); // Try various step heights float[] candidateHeights = CollectCandidateStepHeights(stepBase, stepColliders, maxUpStep, (float)resolved.Y); foreach (float stepY in candidateHeights) { Vec3d stepMovement = new Vec3d(movement.X, stepY, movement.Z); Vec3d stepResolved = CollideWithShapes(stepMovement, stepBase, stepColliders); if (stepResolved.HorizontalDistanceSqr() > resolved.HorizontalDistanceSqr()) { double yOffset = entityBox.MinY - stepBase.MinY; return stepResolved.Subtract(0, yOffset, 0); } } } return resolved; } /// /// Collide movement against a list of shapes using axis-separated resolution. /// Matches Entity.collideWithShapes() with vanilla's axis ordering (Y first, then larger horizontal axis). /// private static Vec3d CollideWithShapes(Vec3d movement, Aabb entityBox, List colliders) { if (colliders.Count == 0) return movement; Vec3d accumulated = Vec3d.Zero; int[] axisOrder = GetAxisStepOrder(movement); foreach (int axis in axisOrder) { double dist = movement.Get(axis); if (dist == 0.0) continue; double resolved = CollideAxis(axis, entityBox.Move(accumulated), colliders, dist); accumulated = accumulated.With(axis, resolved); } return accumulated; } /// /// Get axis processing order matching vanilla Direction.Axis.axisStepOrder(Vec3): /// Y is always first, then the larger horizontal axis, then the smaller. /// private static int[] GetAxisStepOrder(Vec3d movement) { return Math.Abs(movement.X) < Math.Abs(movement.Z) ? [1, 2, 0] // Y Z X : [1, 0, 2]; // Y X Z } /// /// Collide along a single axis against all block shapes. /// Equivalent to Shapes.collide(axis, box, shapes, distance). /// private static double CollideAxis(int axis, Aabb entityBox, List colliders, double movement) { foreach (var collider in colliders) { if (Math.Abs(movement) < PhysicsConsts.CollisionEpsilon) return 0.0; movement = entityBox.Collide(axis, collider, movement); } return movement; } /// /// Collect all block collision AABBs that overlap the given search area. /// Equivalent to BlockCollisions iterator in vanilla. /// public static List CollectBlockColliders(World world, Aabb searchBox) { var result = new List(); int minBX = (int)Math.Floor(searchBox.MinX - PhysicsConsts.CollisionEpsilon) - 1; int maxBX = (int)Math.Floor(searchBox.MaxX + PhysicsConsts.CollisionEpsilon) + 1; int minBY = (int)Math.Floor(searchBox.MinY - PhysicsConsts.CollisionEpsilon) - 1; int maxBY = (int)Math.Floor(searchBox.MaxY + PhysicsConsts.CollisionEpsilon) + 1; int minBZ = (int)Math.Floor(searchBox.MinZ - PhysicsConsts.CollisionEpsilon) - 1; int maxBZ = (int)Math.Floor(searchBox.MaxZ + PhysicsConsts.CollisionEpsilon) + 1; for (int bx = minBX; bx <= maxBX; bx++) { for (int bz = minBZ; bz <= maxBZ; bz++) { for (int by = minBY; by <= maxBY; by++) { Block block = world.GetBlock(new Location(bx, by, bz)); Aabb[] shapes = BlockShapes.GetShapes(block); foreach (var shape in shapes) { Aabb worldShape = shape.Move(bx, by, bz); if (worldShape.Intersects(searchBox)) result.Add(worldShape); } } } } return result; } /// /// Collect candidate step-up heights, matching Entity.collectCandidateStepUpHeights(). /// Returns sorted distinct step heights between current resolved Y and maxUpStep. /// private static float[] CollectCandidateStepHeights(Aabb stepBase, List colliders, float maxUpStep, float currentY) { var heights = new SortedSet(); foreach (var collider in colliders) { float h = (float)(collider.MaxY - stepBase.MinY); if (h > currentY && h <= maxUpStep) heights.Add(h); } if (heights.Count == 0) return new[] { maxUpStep }; var result = new float[heights.Count]; heights.CopyTo(result); return result; } /// /// Check if a position is on ground by testing for vertical collision below. /// public static bool IsOnGround(World world, Aabb entityBox) { Aabb testBox = entityBox.ExpandTowards(0, -0.06, 0); return CollectBlockColliders(world, testBox).Count > 0; } /// /// Check if a given position has no collision (for checking if player fits somewhere). /// public static bool NoCollision(World world, Aabb entityBox) { return CollectBlockColliders(world, entityBox).Count == 0; } } }