#include "collision.h"
#include <cmath>
#include "irr_aabb3d.h"
#include "mapblock.h"
#include "map.h"
#include "nodedef.h"
#include "gamedef.h"
#include "util/numeric.h"
#if CHECK_CLIENT_BUILD()
#include "client/clientenvironment.h"
#include "client/localplayer.h"
#endif
#include "serverenvironment.h"
#include "server/serveractiveobject.h"
#include "util/timetaker.h"
#include "profiler.h"
#include "object_properties.h"
#ifdef __FAST_MATH__
#warning "-ffast-math is known to cause bugs in collision code, do not use!"
#endif
bool g_collision_problems_encountered = false;
namespace {
struct NearbyCollisionInfo {
NearbyCollisionInfo(bool is_ul, int bouncy, v3s16 pos, const aabb3f &box) :
obj(nullptr),
box(box),
position(pos),
bouncy(bouncy),
is_unloaded(is_ul)
{}
NearbyCollisionInfo(ActiveObject *obj, int bouncy, const aabb3f &box) :
obj(obj),
box(box),
bouncy(bouncy)
{}
inline bool isObject() const { return obj != nullptr; }
ActiveObject *obj;
aabb3f box;
v3s16 position;
u8 bouncy;
bool is_unloaded = false,
is_step_up = false;
};
inline f32 truncate(const f32 val, const f32 factor)
{
return truncf(val * factor) / factor;
}
inline v3f truncate(const v3f vec, const f32 factor)
{
return v3f(
truncate(vec.X, factor),
truncate(vec.Y, factor),
truncate(vec.Z, factor)
);
}
inline v3f rangelimv(const v3f vec, const f32 low, const f32 high)
{
return v3f(
rangelim(vec.X, low, high),
rangelim(vec.Y, low, high),
rangelim(vec.Z, low, high)
);
}
}
CollisionAxis axisAlignedCollision(
const aabb3f &staticbox, const aabb3f &movingbox,
const v3f speed, f32 *dtime)
{
aabb3f relbox(
(movingbox.MaxEdge.X - movingbox.MinEdge.X) + (staticbox.MaxEdge.X - staticbox.MinEdge.X),
(movingbox.MaxEdge.Y - movingbox.MinEdge.Y) + (staticbox.MaxEdge.Y - staticbox.MinEdge.Y),
(movingbox.MaxEdge.Z - movingbox.MinEdge.Z) + (staticbox.MaxEdge.Z - staticbox.MinEdge.Z),
std::max(movingbox.MaxEdge.X, staticbox.MaxEdge.X) - std::min(movingbox.MinEdge.X, staticbox.MinEdge.X),
std::max(movingbox.MaxEdge.Y, staticbox.MaxEdge.Y) - std::min(movingbox.MinEdge.Y, staticbox.MinEdge.Y),
std::max(movingbox.MaxEdge.Z, staticbox.MaxEdge.Z) - std::min(movingbox.MinEdge.Z, staticbox.MinEdge.Z)
);
const f32 dtime_max = *dtime;
f32 inner_margin;
f32 distance;
f32 time;
if (speed.Y) {
distance = relbox.MaxEdge.Y - relbox.MinEdge.Y;
*dtime = distance / std::abs(speed.Y);
time = std::max(*dtime, 0.0f);
if (*dtime <= dtime_max) {
inner_margin = std::max(-0.5f * (staticbox.MaxEdge.Y - staticbox.MinEdge.Y), -2.0f);
if ((speed.Y > 0 && staticbox.MinEdge.Y - movingbox.MaxEdge.Y > inner_margin) ||
(speed.Y < 0 && movingbox.MinEdge.Y - staticbox.MaxEdge.Y > inner_margin)) {
if (
(std::max(movingbox.MaxEdge.X + speed.X * time, staticbox.MaxEdge.X)
- std::min(movingbox.MinEdge.X + speed.X * time, staticbox.MinEdge.X)
- relbox.MinEdge.X < 0) &&
(std::max(movingbox.MaxEdge.Z + speed.Z * time, staticbox.MaxEdge.Z)
- std::min(movingbox.MinEdge.Z + speed.Z * time, staticbox.MinEdge.Z)
- relbox.MinEdge.Z < 0)
)
return COLLISION_AXIS_Y;
}
}
else {
return COLLISION_AXIS_NONE;
}
}
if (speed.X) {
distance = relbox.MaxEdge.X - relbox.MinEdge.X;
*dtime = distance / std::abs(speed.X);
time = std::max(*dtime, 0.0f);
if (*dtime <= dtime_max) {
inner_margin = std::max(-0.5f * (staticbox.MaxEdge.X - staticbox.MinEdge.X), -2.0f);
if ((speed.X > 0 && staticbox.MinEdge.X - movingbox.MaxEdge.X > inner_margin) ||
(speed.X < 0 && movingbox.MinEdge.X - staticbox.MaxEdge.X > inner_margin)) {
if (
(std::max(movingbox.MaxEdge.Y + speed.Y * time, staticbox.MaxEdge.Y)
- std::min(movingbox.MinEdge.Y + speed.Y * time, staticbox.MinEdge.Y)
- relbox.MinEdge.Y < 0) &&
(std::max(movingbox.MaxEdge.Z + speed.Z * time, staticbox.MaxEdge.Z)
- std::min(movingbox.MinEdge.Z + speed.Z * time, staticbox.MinEdge.Z)
- relbox.MinEdge.Z < 0)
)
return COLLISION_AXIS_X;
}
} else {
return COLLISION_AXIS_NONE;
}
}
if (speed.Z) {
distance = relbox.MaxEdge.Z - relbox.MinEdge.Z;
*dtime = distance / std::abs(speed.Z);
time = std::max(*dtime, 0.0f);
if (*dtime <= dtime_max) {
inner_margin = std::max(-0.5f * (staticbox.MaxEdge.Z - staticbox.MinEdge.Z), -2.0f);
if ((speed.Z > 0 && staticbox.MinEdge.Z - movingbox.MaxEdge.Z > inner_margin) ||
(speed.Z < 0 && movingbox.MinEdge.Z - staticbox.MaxEdge.Z > inner_margin)) {
if (
(std::max(movingbox.MaxEdge.X + speed.X * time, staticbox.MaxEdge.X)
- std::min(movingbox.MinEdge.X + speed.X * time, staticbox.MinEdge.X)
- relbox.MinEdge.X < 0) &&
(std::max(movingbox.MaxEdge.Y + speed.Y * time, staticbox.MaxEdge.Y)
- std::min(movingbox.MinEdge.Y + speed.Y * time, staticbox.MinEdge.Y)
- relbox.MinEdge.Y < 0)
)
return COLLISION_AXIS_Z;
}
}
}
return COLLISION_AXIS_NONE;
}
bool wouldCollideWithCeiling(
const std::vector<NearbyCollisionInfo> &cinfo,
const aabb3f &movingbox,
f32 y_increase, f32 d)
{
assert(y_increase >= 0);
for (const auto &it : cinfo) {
const aabb3f &staticbox = it.box;
if ((movingbox.MaxEdge.Y - d <= staticbox.MinEdge.Y) &&
(movingbox.MaxEdge.Y + y_increase > staticbox.MinEdge.Y) &&
(movingbox.MinEdge.X < staticbox.MaxEdge.X) &&
(movingbox.MaxEdge.X > staticbox.MinEdge.X) &&
(movingbox.MinEdge.Z < staticbox.MaxEdge.Z) &&
(movingbox.MaxEdge.Z > staticbox.MinEdge.Z))
return true;
}
return false;
}
static bool add_area_node_boxes(const v3s16 min, const v3s16 max, IGameDef *gamedef,
Environment *env, std::vector<NearbyCollisionInfo> &cinfo)
{
const auto *nodedef = gamedef->getNodeDefManager();
bool any_position_valid = false;
thread_local std::vector<aabb3f> nodeboxes;
Map *map = &env->getMap();
const bool air_walkable = nodedef->get(CONTENT_AIR).walkable;
v3s16 last_bp(S16_MAX);
MapBlock *last_block = nullptr;
v3s16 p;
for (p.Z = min.Z; p.Z <= max.Z; p.Z++)
for (p.Y = min.Y; p.Y <= max.Y; p.Y++)
for (p.X = min.X; p.X <= max.X; p.X++) {
v3s16 bp, relp;
getNodeBlockPosWithOffset(p, bp, relp);
if (bp != last_bp) {
last_block = map->getBlockNoCreateNoEx(bp);
last_bp = bp;
}
MapBlock *const block = last_block;
if (!block) {
v3s16 rowend(bp.X * MAP_BLOCKSIZE + MAP_BLOCKSIZE - 1, p.Y, p.Z);
aabb3f box = getNodeBox(p, BS);
box.addInternalBox(getNodeBox(rowend, BS));
cinfo.emplace_back(true, 0, p, box);
p.X = rowend.X;
continue;
}
if (!air_walkable && block->isAir()) {
any_position_valid = true;
p.X = bp.X * MAP_BLOCKSIZE + MAP_BLOCKSIZE - 1;
continue;
}
const MapNode n = block->getNodeNoCheck(relp);
if (n.getContent() != CONTENT_IGNORE) {
any_position_valid = true;
const ContentFeatures &f = nodedef->get(n);
if (!f.walkable)
continue;
int n_bouncy_value = abs(itemgroup_get(f.groups, "bouncy"));
u8 neighbors = n.getNeighbors(p, map);
nodeboxes.clear();
n.getCollisionBoxes(nodedef, &nodeboxes, neighbors);
v3f posf = intToFloat(p, BS);
for (auto box : nodeboxes) {
box.MinEdge += posf;
box.MaxEdge += posf;
cinfo.emplace_back(false, n_bouncy_value, p, box);
}
} else {
aabb3f box = getNodeBox(p, BS);
cinfo.emplace_back(true, 0, p, box);
}
}
return any_position_valid;
}
static void add_object_boxes(Environment *env,
const aabb3f &box_0, f32 dtime,
const v3f pos_f, const v3f speed_f, ActiveObject *self,
std::vector<NearbyCollisionInfo> &cinfo)
{
auto process_object = [&cinfo] (ActiveObject *object) {
if (object && object->collideWithObjects()) {
aabb3f box{{0.0f, 0.0f, 0.0f}};
if (object->getCollisionBox(&box))
cinfo.emplace_back(object, 0, box);
}
};
constexpr f32 tolerance = 1.5f * BS;
#if CHECK_CLIENT_BUILD()
ClientEnvironment *c_env = dynamic_cast<ClientEnvironment*>(env);
if (c_env) {
const f32 distance = speed_f.getLength() * dtime +
box_0.getExtent().getLength() + tolerance;
std::vector<DistanceSortedActiveObject> clientobjects;
c_env->getActiveObjects(pos_f, distance, clientobjects);
for (auto &clientobject : clientobjects) {
if (!self || (self != clientobject.obj &&
self != clientobject.obj->getParent())) {
process_object(clientobject.obj);
}
}
LocalPlayer *lplayer = c_env->getLocalPlayer();
auto *obj = (ClientActiveObject*) lplayer->getCAO();
if (!self || (self != obj && self != obj->getParent())) {
aabb3f lplayer_collisionbox = lplayer->getCollisionbox();
v3f lplayer_pos = lplayer->getPosition();
lplayer_collisionbox.MinEdge += lplayer_pos;
lplayer_collisionbox.MaxEdge += lplayer_pos;
cinfo.emplace_back(obj, 0, lplayer_collisionbox);
}
}
else
#endif
{
ServerEnvironment *s_env = dynamic_cast<ServerEnvironment*>(env);
if (s_env) {
auto include_obj_cb = [self, &process_object] (ServerActiveObject *obj) {
if (!obj->isGone() &&
(!self || (self != obj && self != obj->getParent()))) {
process_object(obj);
}
return false;
};
const v3f movement = speed_f * dtime;
const v3f min = pos_f + box_0.MinEdge - v3f(tolerance) + componentwise_min(movement, v3f());
const v3f max = pos_f + box_0.MaxEdge + v3f(tolerance) + componentwise_max(movement, v3f());
std::vector<ServerActiveObject*> s_objects;
s_env->getObjectsInArea(s_objects, aabb3f(min, max), include_obj_cb);
}
}
}
template <float v3f::*AX>
inline void collide_with(const aabb3f &box_mov, const aabb3f &box_stat,
v3f *pos_f, v3f *speed_f, v3f *accel_f, const v3f &aspeed_f, float bounce)
{
const float speed = aspeed_f.*AX;
if (speed) {
pos_f->*AX = speed < 0.0f
? (box_stat.MaxEdge.*AX - box_mov.MinEdge.*AX)
: (box_stat.MinEdge.*AX - box_mov.MaxEdge.*AX);
}
if (bounce < -1e-4f && fabsf(speed) > BS * 3.0f) {
speed_f->*AX *= bounce;
} else {
speed_f->*AX = 0;
accel_f->*AX = 0;
}
}
#define PROFILER_NAME(text) (dynamic_cast<ServerEnvironment*>(env) ? ("Server: " text) : ("Client: " text))
CollisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef,
const aabb3f &box_0,
f32 stepheight, f32 dtime,
v3f *pos_f, v3f *speed_f,
v3f accel_f, ActiveObject *self,
bool collide_with_objects,
StepUpMode step_up_mode)
{
static bool time_notification_done = false;
ScopeProfiler sp(g_profiler, PROFILER_NAME("collisionMoveSimple()"), SPT_AVG, PRECISION_MICRO);
CollisionMoveResult result;
if (*speed_f == v3f() && accel_f == v3f())
return result;
Calculate new velocity
*/
if (dtime > DTIME_LIMIT) {
if (!time_notification_done) {
time_notification_done = true;
warningstream << "collisionMoveSimple: maximum step interval exceeded,"
" lost movement details!"<<std::endl;
}
g_collision_problems_encountered = true;
dtime = DTIME_LIMIT;
} else {
time_notification_done = false;
}
v3f aspeed_f = *speed_f + accel_f * 0.5f * dtime;
aspeed_f = truncate(rangelimv(aspeed_f, -5000.0f, 5000.0f), 10000.0f);
thread_local std::vector<NearbyCollisionInfo> cinfo;
cinfo.clear();
{
v3f newpos_f = *pos_f + aspeed_f * dtime;
v3f minpos_f(
MYMIN(pos_f->X, newpos_f.X),
MYMIN(pos_f->Y, newpos_f.Y) + 0.01f * BS,
MYMIN(pos_f->Z, newpos_f.Z)
);
v3f maxpos_f(
MYMAX(pos_f->X, newpos_f.X),
MYMAX(pos_f->Y, newpos_f.Y),
MYMAX(pos_f->Z, newpos_f.Z)
);
v3s16 min = floatToInt(minpos_f + box_0.MinEdge, BS) - v3s16(1, 1, 1);
v3s16 max = floatToInt(maxpos_f + box_0.MaxEdge, BS) + v3s16(1, 1, 1);
bool any_position_valid = add_area_node_boxes(min, max, gamedef, env, cinfo);
if (!any_position_valid) {
*speed_f = v3f(0, 0, 0);
return result;
}
}
if (collide_with_objects) {
add_object_boxes(env, box_0, dtime, *pos_f, aspeed_f, self, cinfo);
}
for (int loopcount = 0;; loopcount++) {
if (loopcount >= 100) {
warningstream << "collisionMoveSimple: Loop count exceeded, aborting to avoid infinite loop" << std::endl;
g_collision_problems_encountered = true;
break;
}
aabb3f movingbox = box_0;
movingbox.MinEdge += *pos_f;
movingbox.MaxEdge += *pos_f;
CollisionAxis nearest_collided = COLLISION_AXIS_NONE;
f32 nearest_dtime = dtime;
int nearest_boxindex = -1;
for (u32 boxindex = 0; boxindex < cinfo.size(); boxindex++) {
const NearbyCollisionInfo &box_info = cinfo[boxindex];
if (box_info.is_step_up)
continue;
f32 dtime_tmp = nearest_dtime;
CollisionAxis collided = axisAlignedCollision(box_info.box,
movingbox, aspeed_f, &dtime_tmp);
if (collided == -1 || dtime_tmp >= nearest_dtime)
continue;
nearest_dtime = dtime_tmp;
nearest_collided = collided;
nearest_boxindex = boxindex;
}
if (nearest_collided == COLLISION_AXIS_NONE) {
*pos_f += aspeed_f * dtime;
*speed_f += accel_f * dtime;
*speed_f = truncate(rangelimv(*speed_f, -5000.0f, 5000.0f), 10000.0f);
break;
}
NearbyCollisionInfo &nearest_info = cinfo[nearest_boxindex];
const aabb3f& cbox = nearest_info.box;
bool step_up = false;
if (nearest_collided != COLLISION_AXIS_Y) {
aabb3f stepbox = movingbox;
float extra_dtime = nearest_dtime + 0.1f * fabsf(dtime - nearest_dtime);
stepbox.MinEdge.X += aspeed_f.X * extra_dtime;
stepbox.MinEdge.Z += aspeed_f.Z * extra_dtime;
stepbox.MaxEdge.X += aspeed_f.X * extra_dtime;
stepbox.MaxEdge.Z += aspeed_f.Z * extra_dtime;
step_up = (movingbox.MinEdge.Y < cbox.MaxEdge.Y) &&
(movingbox.MinEdge.Y + stepheight > cbox.MaxEdge.Y) &&
(!wouldCollideWithCeiling(cinfo, stepbox,
cbox.MaxEdge.Y - movingbox.MinEdge.Y,
0));
}
float bounce = -(float)nearest_info.bouncy / 100.0f;
if (nearest_dtime < 0) {
} else if (nearest_dtime > 0) {
aspeed_f = *speed_f + accel_f * 0.5f * nearest_dtime;
*pos_f += aspeed_f * nearest_dtime;
*speed_f += accel_f * nearest_dtime;
*speed_f = truncate(rangelimv(*speed_f, -5000.0f, 5000.0f), 10000.0f);
dtime -= nearest_dtime;
}
const v3f old_speed_f = *speed_f;
if (step_up && (step_up_mode == StepUpMode::LEGACY ||
(step_up_mode == StepUpMode::FLOATY && speed_f->Y <= 0.0f) ||
(step_up_mode == StepUpMode::RIGID && speed_f->Y == 0.0f))) {
nearest_info.is_step_up = true;
} else if (nearest_collided == COLLISION_AXIS_X) {
collide_with<&v3f::X>(box_0, cbox, pos_f, speed_f, &accel_f, aspeed_f, bounce);
} else if (nearest_collided == COLLISION_AXIS_Y) {
collide_with<&v3f::Y>(box_0, cbox, pos_f, speed_f, &accel_f, aspeed_f, bounce);
if (accel_f.Y == 0 && aspeed_f.Y < 0.0f) {
result.touching_ground = true;
result.standing_on_object = nearest_info.isObject();
}
} else {
assert(nearest_collided == COLLISION_AXIS_Z);
collide_with<&v3f::Z>(box_0, cbox, pos_f, speed_f, &accel_f, aspeed_f, bounce);
}
if (!nearest_info.is_unloaded && !step_up) {
CollisionInfo info;
info.axis = nearest_collided;
info.type = nearest_info.isObject() ? COLLISION_OBJECT : COLLISION_NODE;
info.node_p = nearest_info.position;
info.object = nearest_info.obj;
info.new_pos = *pos_f;
info.old_speed = old_speed_f;
info.new_speed = *speed_f;
result.collisions.push_back(info);
}
if (dtime < BS * 1e-10f)
break;
aspeed_f = *speed_f + accel_f * 0.5f * dtime;
aspeed_f = truncate(rangelimv(aspeed_f, -5000.0f, 5000.0f), 10000.0f);
}
Final touches: Step up stairs and ground detection (compat).
*/
aabb3f mbox = box_0;
mbox.MinEdge += *pos_f;
mbox.MaxEdge += *pos_f;
for (const auto &box_info : cinfo) {
const aabb3f &sbox = box_info.box;
`step_up == true` requires the object to intersect with the static box.
Hence, check whether that is still the case.
For compatibility reasons (ground detection), only X-Z are checked here.
*/
if (sbox.MaxEdge.X > mbox.MinEdge.X && sbox.MinEdge.X < mbox.MaxEdge.X &&
sbox.MaxEdge.Z > mbox.MinEdge.Z &&
sbox.MinEdge.Z < mbox.MaxEdge.Z) {
if (box_info.is_step_up && sbox.MaxEdge.Y > mbox.MinEdge.Y) {
pos_f->Y = sbox.MaxEdge.Y - box_0.MinEdge.Y;
mbox = box_0;
mbox.MinEdge += *pos_f;
mbox.MaxEdge += *pos_f;
}
if (std::fabs(sbox.MaxEdge.Y - mbox.MinEdge.Y) < 0.05f) {
result.touching_ground = true;
result.standing_on_object = box_info.isObject();
}
}
}
result.collides = !result.collisions.empty();
return result;
}
bool collision_check_intersection(Environment *env, IGameDef *gamedef,
const aabb3f &box_0, const v3f &pos_f, ActiveObject *self,
bool collide_with_objects)
{
ScopeProfiler sp(g_profiler, PROFILER_NAME("collision_check_intersection()"), SPT_AVG, PRECISION_MICRO);
std::vector<NearbyCollisionInfo> cinfo;
{
v3s16 min = floatToInt(pos_f + box_0.MinEdge, BS) - v3s16(1, 1, 1);
v3s16 max = floatToInt(pos_f + box_0.MaxEdge, BS) + v3s16(1, 1, 1);
bool any_position_valid = add_area_node_boxes(min, max, gamedef, env, cinfo);
if (!any_position_valid) {
return true;
}
}
if (collide_with_objects) {
v3f speed;
add_object_boxes(env, box_0, 0, pos_f, speed, self, cinfo);
}
Collision detection
*/
aabb3f checkbox = box_0;
checkbox.MinEdge += pos_f + (0.1f * BS);
checkbox.MaxEdge += pos_f - (0.1f * BS);
Go through every node and object box
*/
for (const NearbyCollisionInfo &box_info : cinfo) {
if (box_info.box.intersectsWithBox(checkbox))
return true;
}
return false;
}