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 = movement.X != resolved.X;
bool blockedZ = movement.Z != resolved.Z;
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() — processes axes in order of smallest movement first.
///
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: Y first if moving down, otherwise smallest absolute movement first.
/// Vanilla uses Direction.axisStepOrder(Vec3) which returns axes sorted by absolute movement.
///
private static int[] GetAxisStepOrder(Vec3d movement)
{
double absX = Math.Abs(movement.X);
double absY = Math.Abs(movement.Y);
double absZ = Math.Abs(movement.Z);
if (absX > absZ)
{
if (absZ > absY)
return new[] { 1, 2, 0 }; // Y Z X
if (absX > absY)
return new[] { 1, 0, 2 }; // Y X Z
return new[] { 0, 1, 2 }; // X Y Z
}
else
{
if (absX > absY)
return new[] { 1, 0, 2 }; // Y X Z
if (absZ > absY)
return new[] { 1, 2, 0 }; // Y Z X
return new[] { 2, 1, 0 }; // Z Y X
}
}
///
/// 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;
}
}
}