// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2022
// www.magicbane.com
package engine.math;
import engine.InterestManagement.WorldGrid;
import engine.gameManager.ZoneManager;
import engine.net.client.msg.PlaceAssetMsg.PlacementInfo;
import engine.objects.*;
import engine.server.MBServerStatics;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.LinkedBlockingQueue;
/**
* This class contains all methods of storing bounds
* information within MagicBane and performing collision
* detection against them.
*
* These objects are essentially an AABB, given rotations
* in MagicBane for placed objects come in a quantum of 90.
*/
public class Bounds {
private static final LinkedBlockingQueue boundsPool = new LinkedBlockingQueue<>();
public static HashMap meshBoundsCache = new HashMap<>();
private Vector2f origin = new Vector2f();
private Vector2f halfExtents = new Vector2f();
private float rotation;
private float rotationDegrees = 0;
private Quaternion quaternion;
private boolean flipExtents;
private ArrayList regions = new ArrayList<>();
private ArrayList colliders = new ArrayList<>();
// Default constructor
public Bounds() {
origin.zero();
halfExtents.zero();
rotation = 0.0f;
flipExtents = false;
}
public static Bounds borrow() {
Bounds outBounds;
outBounds = boundsPool.poll();
if (outBounds == null)
outBounds = new Bounds();
return outBounds;
}
// Identity Bounds at location
public static void zero(Bounds bounds) {
bounds.origin.zero();
bounds.halfExtents.zero();
bounds.rotation = 0.0f;
bounds.flipExtents = false;
}
public static boolean collide(Vector3fImmutable location, Bounds targetBounds) {
if (targetBounds == null)
return false;
boolean collisionState = false;
Bounds identityBounds = Bounds.borrow();
identityBounds.setBounds(location);
collisionState = collide(targetBounds, identityBounds, 0.0f);
identityBounds.release();
return collisionState;
}
public static boolean collide(Vector3fImmutable location, Building targetBuilding) {
boolean collisionState = false;
Bounds targetBounds = targetBuilding.getBounds();
if (targetBounds == null)
return false;
Bounds identityBounds = Bounds.borrow();
identityBounds.setBounds(location);
collisionState = collide(targetBounds, identityBounds, 0.1f);
identityBounds.release();
return collisionState;
}
public static boolean collide(Bounds sourceBounds, Bounds targetBounds, float threshold) {
float deltaX;
float deltaY;
float extentX;
float extentY;
float sourceExtentX;
float sourceExtentY;
float targetExtentX;
float targetExtentY;
deltaX = Math.abs(sourceBounds.origin.x - targetBounds.origin.x);
deltaY = Math.abs(sourceBounds.origin.y - targetBounds.origin.y);
if (sourceBounds.flipExtents) {
sourceExtentX = sourceBounds.halfExtents.y;
sourceExtentY = sourceBounds.halfExtents.x;
} else {
sourceExtentX = sourceBounds.halfExtents.x;
sourceExtentY = sourceBounds.halfExtents.y;
}
if (targetBounds.flipExtents) {
targetExtentX = targetBounds.halfExtents.y;
targetExtentY = targetBounds.halfExtents.x;
} else {
targetExtentX = targetBounds.halfExtents.x;
targetExtentY = targetBounds.halfExtents.y;
}
extentX = sourceExtentX + targetExtentX;
extentY = sourceExtentY + targetExtentY;
// Return false on overlapping edge cases
if ((Math.abs(deltaX + threshold) < extentX))
if ((Math.abs(deltaY + threshold) < extentY))
return true;
return false;
}
public static boolean collide(PlacementInfo sourceInfo, Building targetBuilding) {
Bounds sourceBounds;
Bounds targetBounds;
boolean collisionState = false;
// Early exit sanity check. Can't quite collide against nothing
if ((sourceInfo == null) || (targetBuilding == null))
return false;
sourceBounds = Bounds.borrow();
sourceBounds.setBounds(sourceInfo);
// WARNING: DO NOT EVER RELEASE THESE WORLDOBJECT BOUNDS
// THEY ARE NOT IMMUTABLE
targetBounds = targetBuilding.getBounds();
// If target building has no bounds, we certainly cannot collide.
// Note: We remove and release bounds objects to the pool when
// buildings are destroyed.
if (targetBounds == null)
return false;
collisionState = collide(sourceBounds, targetBounds, .1f);
// Release bounds and return collision state
sourceBounds.release();
return collisionState;
}
public static boolean collide(Bounds bounds, Vector3fImmutable start, Vector3fImmutable end) {
boolean collide = false;
for (Colliders collider : bounds.colliders) {
collide = linesTouching(collider.startX, collider.startY, collider.endX, collider.endY, start.x, start.z, end.x, end.z);
if (collide)
break;
}
return collide;
}
//used for wall collision with players.
public static Vector3fImmutable PlayerBuildingCollisionPoint(PlayerCharacter player, Vector3fImmutable start, Vector3fImmutable end) {
Vector3fImmutable collidePoint = null;
//player can fly over walls when at max altitude. skip collision checks.
if (player.getAltitude() >= 60)
return null;
float distance = player.getLoc().distance2D(end);
// Players should not be able to move more than 2000 units at a time, stop them dead in their tracks if they do. (hacks)
if (distance > 2000)
return player.getLoc();
HashSet awoList = WorldGrid.getObjectsInRangePartial(player, distance + 1000, MBServerStatics.MASK_BUILDING);
float collideDistance = 0;
float lastDistance = -1;
for (AbstractWorldObject awo : awoList) {
Building building = (Building) awo;
//player is inside building region, skip collision check. we only do collision from the outside.
if (player.region != null && player.region.parentBuildingID == building.getObjectUUID())
continue;
if (building.getBounds().colliders == null)
continue;
for (Colliders collider : building.getBounds().colliders) {
//links are what link together buildings, allow players to run through them only if they are in a building already.
if (collider.isLink() && player.region != null)
continue;
if (collider.getDoorID() != 0 && building.isDoorOpen(collider.getDoorID()))
continue;
Vector3fImmutable tempCollidePoint = lineIntersection(collider.startX, collider.startY, collider.endX, collider.endY, start.x, start.z, end.x, end.z);
//didnt collide, skip distance checks.
if (tempCollidePoint == null)
continue;
//first collision detection, inititialize all variables.
if (lastDistance == -1) {
collideDistance = start.distance2D(tempCollidePoint);
lastDistance = collideDistance;
collidePoint = tempCollidePoint;
} else
//get closest collide point.
collideDistance = start.distance2D(tempCollidePoint);
if (collideDistance < lastDistance) {
lastDistance = collideDistance;
collidePoint = tempCollidePoint;
}
}
}
//
if (collidePoint != null) {
if (collideDistance >= 2)
collidePoint = player.getFaceDir().scaleAdd(-2f, new Vector3fImmutable((float) collidePoint.getX(), end.y, (float) collidePoint.getZ()));
else
collidePoint = player.getLoc();
}
return collidePoint;
}
public static boolean linesTouching(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) {
float denominator = ((x2 - x1) * (y4 - y3)) - ((y2 - y1) * (x4 - x3));
float numerator1 = ((y1 - y3) * (x4 - x3)) - ((x1 - x3) * (y4 - y3));
float numerator2 = ((y1 - y3) * (x2 - x1)) - ((x1 - x3) * (y2 - y1));
// Detect coincident lines (has a problem, read below)
if (denominator == 0)
return numerator1 == 0 && numerator2 == 0;
float r = numerator1 / denominator;
float s = numerator2 / denominator;
return (r >= 0 && r <= 1) && (s >= 0 && s <= 1);
}
public static Vector3fImmutable lineIntersection(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) {
// calculate the distance to intersection point
float uA = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
float uB = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
// if uA and uB are between 0-1, lines are colliding
if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1) {
return new Vector3fImmutable(x1 + (uA * (x2 - x1)), 0, y1 + (uA * (y2 - y1)));
}
return null;
}
private static boolean calculateFlipExtents(Bounds bounds) {
int degrees;
double radian = 0;
if (bounds.quaternion != null) {
radian = bounds.quaternion.angleY;
}
degrees = (int) Math.toDegrees(radian);
bounds.rotationDegrees = degrees;
if (degrees < 0)
degrees += 360;
return (degrees >= 85 && degrees <= 95) ||
(degrees >= 265 && degrees <= 275);
}
public static Vector3f getRotatedPoint(Vector3f point, float centerX, float centerZ, float angle) {
//TRANSLATE TO ORIGIN
float x1 = point.getX() - centerX;
float y1 = point.getZ() - centerZ;
//APPLY ROTATION
float temp_x1 = (float) (x1 * Math.cos(angle) - y1 * Math.sin(angle));
float temp_z1 = (float) (x1 * Math.sin(angle) + y1 * Math.cos(angle));
temp_x1 += centerX;
temp_z1 += centerZ;
return new Vector3f(temp_x1, point.y, temp_z1);
}
public void release() {
Bounds.zero(this);
boundsPool.add(this);
}
// Method detects overlap of two given Bounds objects.
// Just your generic AABB collision algorythm.
public void setBounds(Vector2f origin, Vector2f extents, float rotation) {
this.origin.set(origin);
this.halfExtents.set(extents);
this.rotation = rotation;
this.flipExtents = Bounds.calculateFlipExtents(this);
}
public void setBounds(PlacementInfo sourceInfo) {
Blueprint sourceBlueprint;
sourceBlueprint = Blueprint.getBlueprint(sourceInfo.getBlueprintUUID());
this.origin.set(sourceInfo.getLoc().x, sourceInfo.getLoc().z);
this.halfExtents.set(sourceBlueprint.getExtents());
this.quaternion = new Quaternion(sourceInfo.getRot().x, sourceInfo.getRot().y, sourceInfo.getRot().z, sourceInfo.getW());
this.rotation = sourceInfo.getRot().y;
this.flipExtents = Bounds.calculateFlipExtents(this);
}
public void setBounds(Bounds sourceBounds) {
origin.set(sourceBounds.origin);
halfExtents.set(sourceBounds.halfExtents);
this.rotation = sourceBounds.rotation;
this.flipExtents = sourceBounds.flipExtents;
}
public void setBounds(AbstractCharacter sourcePlayer) {
this.origin.set(sourcePlayer.getLoc().x, sourcePlayer.getLoc().z);
this.halfExtents.set(.5f, .5f);
this.rotation = 0;
this.flipExtents = false;
}
public void setBounds(Vector3fImmutable sourceLocation) {
this.origin.set(sourceLocation.x, sourceLocation.z);
this.halfExtents.set(.5f, .5f);
this.rotation = 0;
this.flipExtents = false;
}
public void setBounds(Vector3fImmutable sourceLocation, float halfExtent) {
this.origin.set(sourceLocation.x, sourceLocation.z);
this.halfExtents.set(halfExtent, halfExtent);
this.rotation = 0;
this.flipExtents = false;
}
public void setBounds(Building building) {
Blueprint blueprint;
MeshBounds meshBounds;
int halfExtentX;
int halfExtentY;
// Need a blueprint for proper bounds
blueprint = building.getBlueprint();
this.quaternion = new Quaternion(building.getRot().x, building.getRot().y, building.getRot().z, building.getw());
// Calculate Bounds for non-blueprint objects
if (blueprint == null) {
// If a mesh is a non-blueprint structure then we calculate
// it's bounding box based upon defaults from original source
// lookup.
meshBounds = meshBoundsCache.get(building.getMeshUUID());
this.origin.set(building.getLoc().x, building.getLoc().z);
// Magicbane uses halfExtents
if (meshBounds == null) {
halfExtentX = 1;
halfExtentY = 1;
} else {
float halfExtent = Math.max((meshBounds.maxX - meshBounds.minX) / 2, (meshBounds.maxZ - meshBounds.minZ) / 2);
halfExtentX = Math.round(halfExtent);
halfExtentY = Math.round(halfExtent);
}
// The rotation is reset after the new aabb is calculated.
this.rotation = building.getRot().y;
// Caclculate and set the new half halfExtents for the rotated bounding box
// and reset the rotation to 0 for this bounds.
this.halfExtents.set(halfExtentX, (halfExtentY));
this.rotation = 0;
this.setRegions(building);
this.setColliders(building);
return;
}
this.origin.set(building.getLoc().x, building.getLoc().z);
this.rotation = building.getRot().y;
this.halfExtents.set(blueprint.getExtents());
this.flipExtents = Bounds.calculateFlipExtents(this);
this.setRegions(building);
this.setColliders(building);
}
public void modify(float x, float y, float extents) {
this.origin.x = x;
this.origin.y = y;
this.halfExtents.x = extents;
this.halfExtents.y = extents;
}
/**
* @return the origin
*/
public Vector2f getOrigin() {
return origin;
}
/**
* @return the halfExtents
*/
public Vector2f getHalfExtents() {
return halfExtents;
}
/**
* @return the rotation
*/
public float getRotation() {
return rotation;
}
/**
* @param rotation the rotation to set
*/
public void setRotation(float rotation) {
this.rotation = rotation;
}
public void setColliders(Building building) {
//Collidables are for player movement collision
ArrayList tempList = StaticColliders._staticColliders.get(building.getMeshUUID());
ArrayList tempColliders = new ArrayList<>();
if (tempList != null) {
for (StaticColliders staticCollider : tempList) {
ArrayList regionPoints = new ArrayList<>();
Vector3f colliderStart = new Vector3f(staticCollider.getStartX(), 0, staticCollider.getStartY());
Vector3f colliderEnd = new Vector3f(staticCollider.getEndX(), 0, staticCollider.getEndY());
Vector3f worldStart = ZoneManager.convertLocalToWorld(building, colliderStart, this);
Vector3f worldEnd = ZoneManager.convertLocalToWorld(building, colliderEnd, this);
tempColliders.add(new Colliders(worldStart.x, worldStart.z, worldEnd.x, worldEnd.z, staticCollider.getDoorID(), staticCollider.isLink()));
}
}
this.colliders = tempColliders;
}
public ArrayList getRegions() {
return regions;
}
public void setRegions(Building building) {
//Collidables are for player movement collision
ArrayList tempList = BuildingRegions._staticRegions.get(building.getMeshUUID());
ArrayList tempRegions = new ArrayList<>();
if (tempList != null) {
for (BuildingRegions buildingRegion : tempList) {
ArrayList regionPoints = new ArrayList<>();
Vector3f centerPoint = ZoneManager.convertLocalToWorld(building, buildingRegion.center, this);
for (Vector3f point : buildingRegion.getRegionPoints()) {
Vector3f rotatedPoint = ZoneManager.convertLocalToWorld(building, point, this);
regionPoints.add(rotatedPoint);
}
tempRegions.add(new Regions(regionPoints, buildingRegion.getLevel(), buildingRegion.getRoom(), buildingRegion.isOutside(), buildingRegion.isExitRegion(), buildingRegion.isStairs(), centerPoint, building.getObjectUUID()));
}
}
this.regions = tempRegions;
}
public void setRegions(ArrayList regions) {
this.regions = regions;
}
public float getRotationDegrees() {
return rotationDegrees;
}
public boolean isFlipExtents() {
return flipExtents;
}
public Quaternion getQuaternion() {
return quaternion;
}
}