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;
}
}
}