package com.hypixel.hytale.server.core.modules.collision;
import com.hypixel.hytale.assetstore.event.LoadedAssetsEvent;
import com.hypixel.hytale.assetstore.map.IndexedLookupTableAssetMap;
import com.hypixel.hytale.common.plugin.PluginManifest;
import com.hypixel.hytale.component.Archetype;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.ResourceType;
import com.hypixel.hytale.component.spatial.KDTree;
import com.hypixel.hytale.component.spatial.SpatialResource;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.math.vector.Vector2d;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.server.core.asset.type.blockhitbox.BlockBoundingBoxes;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation;
import com.hypixel.hytale.server.core.entity.Entity;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.entity.entities.ProjectileComponent;
import com.hypixel.hytale.server.core.modules.collision.commands.HitboxCommand;
import com.hypixel.hytale.server.core.modules.entity.component.BoundingBox;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent;
import com.hypixel.hytale.server.core.modules.projectile.component.Projectile;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.server.core.util.Config;
import java.util.List;
import java.util.function.Predicate;
import java.util.logging.Level;
import javax.annotation.Nonnull;
public class CollisionModule extends JavaPlugin {
public static final PluginManifest MANIFEST = PluginManifest.corePlugin(CollisionModule.class).build();
public static final int VALIDATE_INVALID = -1;
public static final int VALIDATE_OK = 0;
public static final int VALIDATE_ON_GROUND = 1;
public static final int VALIDATE_TOUCH_CEIL = 2;
private static CollisionModule instance;
private ResourceType<EntityStore, SpatialResource<Ref<EntityStore>, EntityStore>> tangiableEntitySpatialComponent;
private double extentMax;
private double minimumThickness;
private final Config<CollisionModuleConfig> config;
public static CollisionModule get() {
return instance;
}
public CollisionModule(@Nonnull JavaPluginInit init) {
super(init);
this.config = this.withConfig("CollisionModule", CollisionModuleConfig.CODEC);
instance = this;
}
public CollisionModuleConfig getConfig() {
return this.config.get();
}
protected void setup() {
this.getCommandRegistry().registerCommand(new HitboxCommand());
this.getEventRegistry().register((Class)LoadedAssetsEvent.class, BlockBoundingBoxes.class, this::onLoadedAssetsEvent);
this.tangiableEntitySpatialComponent = this.getEntityStoreRegistry().registerSpatialResource(() -> new KDTree(Ref::isValid));
}
public ResourceType<EntityStore, SpatialResource<Ref<EntityStore>, EntityStore>> getTangiableEntitySpatialComponent() {
return this.tangiableEntitySpatialComponent;
}
private void onLoadedAssetsEvent(@Nonnull LoadedAssetsEvent<String, BlockBoundingBoxes, IndexedLookupTableAssetMap<String, BlockBoundingBoxes>> event) {
if (event.isInitial()) {
this.extentMax = 0.0;
this.minimumThickness = 1.7976931348623157E308;
}
for(BlockBoundingBoxes box : event.getLoadedAssets().values()) {
this.handleLoadedHitbox(box);
}
CollisionModuleConfig config = this.config.get();
if (config.hasMinimumThickness()) {
this.minimumThickness = config.getMinimumThickness();
}
this.getLogger().at(Level.INFO).log("Block extents for CollisionSystem is Max=" + this.extentMax + ", Min=" + this.minimumThickness);
}
private void handleLoadedHitbox(@Nonnull BlockBoundingBoxes box) {
BlockBoundingBoxes.RotatedVariantBoxes defaultBox = box.get(Rotation.None, Rotation.None, Rotation.None);
double maximumExtent = defaultBox.getBoundingBox().getMaximumExtent();
double blockExtent = 0.0;
if (maximumExtent > blockExtent) {
blockExtent = maximumExtent;
}
if (blockExtent > 1.0) {
this.getLogger().at(Level.FINE).log("Block Hitbox %s protrudes more than 1 unit (%s units) out of standard block and degrades performance", box.getId(), blockExtent);
}
if (blockExtent > this.extentMax) {
this.extentMax = blockExtent;
}
double thickness;
if (defaultBox.hasDetailBoxes()) {
thickness = 1.7976931348623157E308;
for(Box boundingBox : defaultBox.getDetailBoxes()) {
thickness = Math.min(thickness, boundingBox.getThickness());
}
} else {
thickness = defaultBox.getBoundingBox().getThickness();
}
if (thickness < 0.0) {
this.getLogger().at(Level.SEVERE).log("Hitbox for " + box.getId() + " has a negative size!");
} else {
if (thickness < this.minimumThickness) {
this.minimumThickness = thickness;
}
}
}
public static boolean findCollisions(@Nonnull Box collider, @Nonnull Vector3d pos, @Nonnull Vector3d v, @Nonnull CollisionResult result, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
return findCollisions(collider, pos, v, true, result, componentAccessor);
}
public static boolean findCollisions(@Nonnull Box collider, @Nonnull Vector3d pos, @Nonnull Vector3d v, boolean stopOnCollisionFound, @Nonnull CollisionResult result, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
World world = ((EntityStore)componentAccessor.getExternalData()).getWorld();
result.reset();
boolean isFarDistance = !isBelowMovementThreshold(v);
if (isFarDistance) {
findBlockCollisionsIterative(world, collider, pos, v, stopOnCollisionFound, result);
} else {
findBlockCollisionsShortDistance(world, collider, pos, v, result);
}
if (result.isCheckingForCharacterCollisions()) {
findCharacterCollisions(pos, v, result, componentAccessor);
}
result.process();
return isFarDistance;
}
public static void findBlockCollisionsIterative(@Nonnull World world, @Nonnull Box collider, @Nonnull Vector3d pos, @Nonnull Vector3d v, boolean stopOnCollisionFound, @Nonnull CollisionResult result) {
if (result.shouldLog()) {
result.getLogger().at(Level.INFO).log(">>>>>> Start findBlockCollisionIterative collider=[%s] pos=%s dir=%s", collider, Vector3d.formatShortString(pos), Vector3d.formatShortString(v));
}
CollisionConfig coll = result.getConfig();
coll.setWorld(world);
result.getMovingBoxBoxCollision().setCollider(collider).setMove(pos, v);
if (result.shouldLog()) {
result.getLogger().at(Level.INFO).log(">>>>>> Start collider=[%s] + offset[%s]", collider, v);
}
result.acquireCollisionModule();
collider.forEachBlock(pos, 1.0E-5, result, (x, y, z, aResult) -> aResult.accept((long)x, (long)y, (long)z));
if (result.shouldLog()) {
result.getLogger().at(Level.INFO).log(">>>> line collider=[%s] dir=%s len=%s", collider, Vector3d.formatShortString(v), v.length());
}
result.iterateBlocks(collider, pos, v, v.length(), stopOnCollisionFound);
coll.clear();
}
public static void findCharacterCollisions(@Nonnull Vector3d pos, @Nonnull Vector3d v, @Nonnull CollisionResult result, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
if (!isBelowMovementThreshold(v)) {
Vector3d coll = new Vector3d();
Vector2d minMax = new Vector2d();
List<Entity> collisionEntities = result.getCollisionEntities();
for(int i = 0; i < collisionEntities.size(); ++i) {
Entity entity = (Entity)collisionEntities.get(i);
Ref<EntityStore> ref = entity.getReference();
assert ref != null;
Archetype<EntityStore> archetype = componentAccessor.getArchetype(ref);
boolean isProjectile = archetype.contains(Projectile.getComponentType()) || archetype.contains(ProjectileComponent.getComponentType());
if (!isProjectile) {
if (archetype.contains(DeathComponent.getComponentType())) {
return;
}
TransformComponent entityTransformComponent = (TransformComponent)componentAccessor.getComponent(ref, TransformComponent.getComponentType());
assert entityTransformComponent != null;
BoundingBox entityBoundingBoxComponent = (BoundingBox)componentAccessor.getComponent(ref, BoundingBox.getComponentType());
assert entityBoundingBoxComponent != null;
Vector3d position = entityTransformComponent.getPosition();
Box boundingBox = entityBoundingBoxComponent.getBoundingBox();
if (boundingBox != null && CollisionMath.intersectVectorAABB(pos, v, position.getX(), position.getY(), position.getZ(), boundingBox, minMax)) {
coll.assign(pos).addScaled(v, minMax.x);
result.allocCharacterCollision().assign(coll, minMax.x, entity.getReference(), entity instanceof Player);
}
}
}
}
}
public static void findBlockCollisionsShortDistance(@Nonnull World world, @Nonnull Box collider, @Nonnull Vector3d pos, @Nonnull Vector3d v, @Nonnull CollisionResult result) {
result.reset();
result.getConfig().setWorld(world);
result.getConfig().extraData1 = pos;
BoxBlockIntersectionEvaluator boxBlockIntersectionEvaluator = result.getBoxBlockIntersection();
boxBlockIntersectionEvaluator.setBox(collider, pos).offsetPosition(v);
collider.forEachBlock(pos.x + v.x, pos.y + v.y, pos.z + v.z, 1.0E-5, result, (x, y, z, aResult) -> {
CollisionConfig coll = aResult.getConfig();
if (!coll.canCollide(x, y, z)) {
if (aResult.shouldLog()) {
String name = coll.blockType != null ? coll.blockType.getId().toString() : "null";
aResult.getLogger().at(Level.INFO).log("-- Short: Ignoring block at %s/%s/%s blockType=%s", x, y, z, name);
}
return true;
} else {
Vector3d _pos = (Vector3d)coll.extraData1;
if (coll.blockId == -2147483648) {
addImmediateCollision(_pos, aResult, coll, 0);
if (aResult.shouldLog()) {
aResult.getLogger().at(Level.INFO).log("-- Short: Stopping with invalid block at %s/%s/%s blockType=<invalid>", x, y, z);
}
return true;
} else {
int boundingBoxX = x + coll.getBoundingBoxOffsetX();
int boundingBoxY = y + coll.getBoundingBoxOffsetY();
int boundingBoxZ = z + coll.getBoundingBoxOffsetZ();
int numDetails = coll.getDetailCount();
BoxBlockIntersectionEvaluator blockBox = aResult.getBoxBlockIntersection();
int code = blockBox.intersectBoxComputeTouch(coll.getBoundingBox(), (double)boundingBoxX, (double)boundingBoxY, (double)boundingBoxZ);
boolean haveCollision = !CollisionMath.isDisjoint(code);
if (aResult.shouldLog()) {
String name = coll.blockType != null ? coll.blockType.getId().toString() : "null";
aResult.getLogger().at(Level.INFO).log("?? Block Test at %s/%s/%s numDet=%d haveColl=%s overlap=%s blockType=%s", x, y, z, numDetails, haveCollision, aResult.getBoxBlockIntersection().isOverlapping(), name);
}
if (numDetails <= 1) {
processCollision(aResult, _pos, blockBox, haveCollision, 0);
} else {
for(int i = 0; i < numDetails; ++i) {
code = blockBox.intersectBoxComputeTouch(coll.getBoundingBox(i), (double)boundingBoxX, (double)boundingBoxY, (double)boundingBoxZ);
haveCollision = !CollisionMath.isDisjoint(code);
processCollision(aResult, _pos, blockBox, haveCollision, i);
}
}
return true;
}
}
});
result.getConfig().clear();
}
protected static void processCollision(@Nonnull CollisionResult result, @Nonnull Vector3d pos, @Nonnull BoxBlockIntersectionEvaluator boxBlockIntersectionEvaluator, boolean haveCollision, int hitboxIndex) {
CollisionConfig coll = result.getConfig();
Predicate<CollisionConfig> isWalkable = coll.getBlockCollisionPredicate();
if (result.shouldLog()) {
result.getLogger().at(Level.INFO).log("?? Short: Further testing block haveCol=%s hitBoxIndex=%s onGround=%s touching=%s canCollide=%s canTrigger=%s", haveCollision, hitboxIndex, boxBlockIntersectionEvaluator.isOnGround(), boxBlockIntersectionEvaluator.isTouching(), coll.blockCanCollide, coll.blockCanTrigger);
}
if (boxBlockIntersectionEvaluator.isOnGround() && coll.blockCanCollide) {
haveCollision = coll.blockType == null || !isWalkable.test(coll);
if (!haveCollision) {
result.addSlide(boxBlockIntersectionEvaluator, hitboxIndex);
if (coll.blockCanTrigger) {
result.addTrigger(boxBlockIntersectionEvaluator, hitboxIndex);
}
if (result.shouldLog()) {
result.getLogger().at(Level.INFO).log("++ Short: Sliding block start=%s end=%s normal=%s", boxBlockIntersectionEvaluator.getCollisionStart(), boxBlockIntersectionEvaluator.getCollisionEnd(), Vector3d.formatShortString(boxBlockIntersectionEvaluator.getCollisionNormal()));
}
return;
}
if (result.shouldLog()) {
result.getLogger().at(Level.INFO).log("?? Short: Sliding block is unwalkable start=%s end=%s normal=%s", boxBlockIntersectionEvaluator.getCollisionStart(), boxBlockIntersectionEvaluator.getCollisionEnd(), Vector3d.formatShortString(boxBlockIntersectionEvaluator.getCollisionNormal()));
}
}
if (haveCollision && coll.blockCanCollide) {
addImmediateCollision(pos, result, coll, hitboxIndex);
if (result.shouldLog()) {
result.getLogger().at(Level.INFO).log("++ Short: Collision with block start=%s end=%s normal=%s", boxBlockIntersectionEvaluator.getCollisionStart(), boxBlockIntersectionEvaluator.getCollisionEnd(), Vector3d.formatShortString(boxBlockIntersectionEvaluator.getCollisionNormal()));
}
}
if (coll.blockCanTrigger && (haveCollision || boxBlockIntersectionEvaluator.isTouching())) {
if (result.shouldLog()) {
result.getLogger().at(Level.INFO).log("++ Short: Trigger block start=%s end=%s normal=%s", boxBlockIntersectionEvaluator.getCollisionStart(), boxBlockIntersectionEvaluator.getCollisionEnd(), Vector3d.formatShortString(boxBlockIntersectionEvaluator.getCollisionNormal()));
}
result.addTrigger(boxBlockIntersectionEvaluator, hitboxIndex);
}
}
public void findIntersections(@Nonnull World world, @Nonnull Box collider, @Nonnull Vector3d pos, @Nonnull CollisionResult result, boolean triggerBlocks, boolean intersections) {
if (!this.isDisabled()) {
result.reset();
result.getConfig().setWorld(world);
result.getConfig().extraData1 = triggerBlocks;
result.getConfig().extraData2 = intersections;
result.getBoxBlockIntersection().setBox(collider, pos).expandBox(0.01);
result.getBoxBlockIntersection().box.forEachBlock(pos, 1.0E-5, result, (x, y, z, aResult) -> {
CollisionConfig coll = aResult.getConfig();
if (!coll.canCollide(x, y, z)) {
return true;
} else {
int boundingBoxX = x + coll.getBoundingBoxOffsetX();
int boundingBoxY = y + coll.getBoundingBoxOffsetY();
int boundingBoxZ = z + coll.getBoundingBoxOffsetZ();
if (!aResult.getBoxBlockIntersection().isBoxIntersecting(coll.getBoundingBox(), (double)boundingBoxX, (double)boundingBoxY, (double)boundingBoxZ)) {
return true;
} else {
boolean _triggerBlocks = (Boolean)coll.extraData1;
boolean _intersections = (Boolean)coll.extraData2;
int numDetails = coll.getDetailCount();
if (numDetails <= 1) {
if (_triggerBlocks && coll.blockCanTrigger) {
aResult.addTrigger(aResult.getBoxBlockIntersection(), 0);
}
if (_intersections) {
aResult.addCollision(aResult.getBoxBlockIntersection(), 0);
}
} else {
for(int i = 0; i < numDetails; ++i) {
if (aResult.getBoxBlockIntersection().isBoxIntersecting(coll.getBoundingBox(i), (double)boundingBoxX, (double)boundingBoxY, (double)boundingBoxZ)) {
if (_triggerBlocks && coll.blockCanTrigger) {
aResult.addTrigger(aResult.getBoxBlockIntersection(), i);
}
if (_intersections) {
aResult.addCollision(aResult.getBoxBlockIntersection(), i);
}
}
}
}
return true;
}
}
});
result.getConfig().clear();
}
}
public int validatePosition(@Nonnull World world, @Nonnull Box collider, @Nonnull Vector3d pos, @Nonnull CollisionResult result) {
return this.isDisabled() ? 0 : this.validatePosition(world, collider, pos, (Object)null, (_this, collisionCode, collision, collisionConfig) -> true, false, result);
}
public <T> int validatePosition(@Nonnull World world, @Nonnull Box collider, @Nonnull Vector3d pos, int invalidBlockMaterials, T t, @Nonnull CollisionFilter<BoxBlockIntersectionEvaluator, T> predicate, @Nonnull CollisionResult result) {
if (this.isDisabled()) {
return 0;
} else {
int savedCollisionState = result.getCollisionByMaterial();
result.setCollisionByMaterial(invalidBlockMaterials);
int code = this.validatePosition(world, collider, pos, t, predicate, (invalidBlockMaterials & 16) == 0, result);
result.setCollisionByMaterial(savedCollisionState);
return code;
}
}
private <T> int validatePosition(@Nonnull World world, @Nonnull Box collider, @Nonnull Vector3d pos, T t, @Nonnull CollisionFilter<BoxBlockIntersectionEvaluator, T> predicate, boolean disableDamageBlocks, @Nonnull CollisionResult result) {
CollisionModuleConfig config = this.config.get();
result.getConfig().setWorld(world);
result.getConfig().dumpInvalidBlocks = config.isDumpInvalidBlocks();
result.getConfig().extraData1 = t;
result.getConfig().extraData2 = predicate;
result.getBoxBlockIntersection().setBox(collider, pos);
boolean saveCheckTriggerState = result.isCheckingTriggerBlocks();
boolean saveCheckDamageBlock = result.isCheckingDamageBlocks();
result.disableTriggerBlocks();
if (disableDamageBlocks) {
result.disableDamageBlocks();
}
result.validate = 0;
collider.forEachBlock(pos, 1.0E-5, result, (x, y, z, aResult) -> {
CollisionConfig coll = aResult.getConfig();
if (!coll.canCollide(x, y, z)) {
return true;
} else {
BoxBlockIntersectionEvaluator boxBlockIntersection = aResult.getBoxBlockIntersection();
int boundingBoxX = x + coll.getBoundingBoxOffsetX();
int boundingBoxY = y + coll.getBoundingBoxOffsetY();
int boundingBoxZ = z + coll.getBoundingBoxOffsetZ();
int code = boxBlockIntersection.intersectBoxComputeOnGround(coll.getBoundingBox(), (double)boundingBoxX, (double)boundingBoxY, (double)boundingBoxZ);
if (coll.blockId == -2147483648) {
if (CollisionMath.isOverlapping(code)) {
aResult.validate = -1;
return false;
} else {
return true;
}
} else if (CollisionMath.isDisjoint(code)) {
return true;
} else {
Box _collider = boxBlockIntersection.box;
Vector3d _pos = boxBlockIntersection.collisionPoint;
Object _t = coll.extraData1;
CollisionFilter<BoxBlockIntersectionEvaluator, Object> _predicate = (CollisionFilter)coll.extraData2;
int numDetails = coll.getDetailCount();
if (numDetails <= 1) {
if (!_predicate.test(_t, code, boxBlockIntersection, coll)) {
return true;
}
if (CollisionMath.isOverlapping(code)) {
if (coll.dumpInvalidBlocks) {
logOverlap(_pos, _collider, coll, coll.getBoundingBox(), x, y, z, 0, code);
}
aResult.validate = -1;
return false;
}
if (boxBlockIntersection.isOnGround()) {
aResult.validate |= 1;
}
if (boxBlockIntersection.touchesCeil()) {
aResult.validate |= 2;
}
} else {
for(int i = 0; i < numDetails; ++i) {
code = boxBlockIntersection.intersectBoxComputeOnGround(coll.getBoundingBox(i), (double)boundingBoxX, (double)boundingBoxY, (double)boundingBoxZ);
if (!CollisionMath.isDisjoint(code) && _predicate.test(_t, code, boxBlockIntersection, coll)) {
if (CollisionMath.isOverlapping(code)) {
if (coll.dumpInvalidBlocks) {
logOverlap(_pos, _collider, coll, coll.getBoundingBox(i), x, y, z, i, code);
}
aResult.validate = -1;
return false;
}
if (boxBlockIntersection.isOnGround()) {
aResult.validate |= 1;
}
if (boxBlockIntersection.touchesCeil()) {
aResult.validate |= 2;
}
}
}
}
return true;
}
}
});
if (saveCheckTriggerState) {
result.enableTriggerBlocks();
}
if (saveCheckDamageBlock) {
result.enableDamageBlocks();
}
result.getConfig().clear();
return result.validate;
}
private static void addImmediateCollision(@Nonnull Vector3d pos, @Nonnull CollisionResult result, @Nonnull CollisionConfig coll, int i) {
BlockCollisionData data = result.newCollision();
data.setStart(pos, 0.0);
data.setEnd(1.0, result.getBoxBlockIntersection().getCollisionNormal());
data.setBlockData(coll);
data.setDetailBoxIndex(i);
data.setTouchingOverlapping(false, true);
}
public static boolean isBelowMovementThreshold(@Nonnull Vector3d v) {
return v.squaredLength() < 1.0000000000000002E-10;
}
private static void logOverlap(@Nonnull Vector3d pos, @Nonnull Box collider, @Nonnull CollisionConfig coll, @Nonnull Box hitBox, int x, int y, int z, int index, int intersectType) {
get().getLogger().at(Level.WARNING).log("Overlapping blocks - code=%s%s%s index=%s pos=%s loc=%s/%s/%s id=%s mat=%s name=%s box=%s hitbox=%s|%s", (intersectType & 8) != 0 ? "X" : "", (intersectType & 16) != 0 ? "Y" : "", (intersectType & 32) != 0 ? "Z" : "", index, Vector3d.formatShortString(pos), x + coll.getBoundingBoxOffsetX(), y + coll.getBoundingBoxOffsetY(), z + coll.getBoundingBoxOffsetZ(), coll.blockId, coll.blockMaterial != null ? coll.blockMaterial.name() : "none", new Object[]{coll.blockType != null ? coll.blockType.getId() : "none", collider, Vector3d.formatShortString(hitBox.min), Vector3d.formatShortString(hitBox.max)});
}
}