Renderer/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java
2025-05-04 13:58:08 -04:00

1198 lines
46 KiB
Java

package electrosphere.server.pathfinding.voxel;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.stream.Collectors;
import org.joml.Vector3d;
import org.joml.Vector3i;
import electrosphere.client.block.BlockChunkData;
import electrosphere.server.datacell.ServerWorldData;
import electrosphere.server.datacell.interfaces.VoxelCellManager;
import electrosphere.server.physics.terrain.manager.ServerTerrainChunk;
import electrosphere.util.math.HashUtils;
/**
* finds a path between two points given a voxel manager
*/
public class VoxelPathfinder {
/**
* Maximum distance to allow searching
*/
public static final double MAX_DIST = 1000;
/**
* Default maximum cost to tolerate
*/
public static final int DEFAULT_MAX_COST = 12000;
/**
* Maximum distance to scan for a walkable position
*/
public static final double DEFAULT_MAX_TARGET_SCAN_DIST = 3;
/**
* The heuristic lookup table
*/
private int[][][] heuristic = new int[3][3][3];
/**
* Finds a path between two points given a voxel cell manager
* @param voxelCellManager The voxel cell manager
* @param startPoint The start point
* @param endPoint The end point
* @param maxDist
* @return The path if it is solvable, null otherwise
*/
public List<Vector3d> findPath(VoxelCellManager voxelCellManager, Vector3d startPoint, Vector3d endPoint, long maxCost){
List<Vector3d> rVal = null;
if(startPoint.distance(endPoint) > MAX_DIST){
throw new Error("Distance is outside range provided! " + startPoint.distance(endPoint) + " vs " + MAX_DIST);
}
//create sets
PriorityQueue<PathfinderNode> openSet = new PriorityQueue<PathfinderNode>();
Map<Long,PathfinderNode> openSetLookup = new HashMap<Long,PathfinderNode>();
Map<Long,PathfinderNode> closetSet = new HashMap<Long,PathfinderNode>();
//add starting node
PathfinderNode startingNode = new PathfinderNode(
ServerWorldData.convertRealToChunkSpace(startPoint.x), ServerWorldData.convertRealToChunkSpace(startPoint.y), ServerWorldData.convertRealToChunkSpace(startPoint.z),
ServerWorldData.convertRealToVoxelSpace(startPoint.x), ServerWorldData.convertRealToVoxelSpace(startPoint.y), ServerWorldData.convertRealToVoxelSpace(startPoint.z),
0, 0, 0
);
openSet.add(startingNode);
//structures used throughout iteration
Vector3i worldPos = new Vector3i();
Vector3i voxelPos = new Vector3i();
worldPos.set(ServerWorldData.convertRealToChunkSpace(endPoint));
voxelPos.set(ServerWorldData.convertRealToVoxelSpace(endPoint));
long goalHash = HashUtils.hashVoxel(
worldPos.x,worldPos.y,worldPos.z,
voxelPos.x,voxelPos.y,voxelPos.z
);
//set heuristic
this.setHeuristic(startPoint, endPoint);
//main A* loop
this.aStar(voxelCellManager, openSet, openSetLookup, closetSet, endPoint, goalHash, maxCost);
//string pulling
List<PathfinderNode> stringPulled = this.stringPull(voxelCellManager, closetSet, goalHash);
rVal = stringPulled.stream().map(
(PathfinderNode node) -> {
Vector3i currWorld = new Vector3i(
node.worldX,
node.worldY,
node.worldZ
);
Vector3i currVoxel = new Vector3i(
node.voxelX,
node.voxelY,
node.voxelZ
);
float weight = voxelCellManager.getVoxelWeightAtLocalPosition(currWorld,currVoxel);
// int type = voxelCellManager.getVoxelTypeAtLocalPosition(currWorld,currVoxel);
if(weight < 0){
weight = 0;
}
currVoxel.y++;
float aboveWeight = voxelCellManager.getVoxelWeightAtLocalPosition(currWorld,currVoxel);
float total = (weight - aboveWeight);
float percentage = weight / total;
return new Vector3d(
ServerWorldData.convertVoxelToRealSpace(node.voxelX, node.worldX),
ServerWorldData.convertVoxelToRealSpace(node.voxelY, node.worldY) + percentage,
ServerWorldData.convertVoxelToRealSpace(node.voxelZ, node.worldZ)
);
}
).collect(Collectors.toList());
return rVal;
}
/**
* Sets the heuristic
* @param start The start pos
* @param end The end pos
*/
private void setHeuristic(Vector3d start, Vector3d end){
//clear out array
for(int x = 0; x < 3; x++){
for(int y = 0; y < 3; y++){
for(int z = 0; z < 3; z++){
this.heuristic[x][y][z] = 1;
}
}
}
if(start.x > end.x){
for(int x = 0; x < 3; x++){
for(int y = 0; y < 3; y++){
this.heuristic[0][x][y] = this.heuristic[0][x][y] + 1;
this.heuristic[1][x][y] = this.heuristic[1][x][y] + 3;
this.heuristic[2][x][y] = this.heuristic[2][x][y] + 5;
}
}
} else {
for(int x = 0; x < 3; x++){
for(int y = 0; y < 3; y++){
this.heuristic[0][x][y] = this.heuristic[0][x][y] + 5;
this.heuristic[1][x][y] = this.heuristic[1][x][y] + 3;
this.heuristic[2][x][y] = this.heuristic[2][x][y] + 1;
}
}
}
if(start.y > end.y){
for(int x = 0; x < 3; x++){
for(int y = 0; y < 3; y++){
this.heuristic[x][0][y] = this.heuristic[x][0][y] + 1;
this.heuristic[x][1][y] = this.heuristic[x][1][y] + 3;
this.heuristic[x][2][y] = this.heuristic[x][2][y] + 5;
}
}
} else {
for(int x = 0; x < 3; x++){
for(int y = 0; y < 3; y++){
this.heuristic[x][0][y] = this.heuristic[x][0][y] + 5;
this.heuristic[x][1][y] = this.heuristic[x][1][y] + 3;
this.heuristic[x][2][y] = this.heuristic[x][2][y] + 1;
}
}
}
if(start.z > end.z){
for(int x = 0; x < 3; x++){
for(int y = 0; y < 3; y++){
this.heuristic[x][y][0] = this.heuristic[x][y][0] + 1;
this.heuristic[x][y][1] = this.heuristic[x][y][1] + 3;
this.heuristic[x][y][2] = this.heuristic[x][y][2] + 5;
}
}
} else {
for(int x = 0; x < 3; x++){
for(int y = 0; y < 3; y++){
this.heuristic[x][y][0] = this.heuristic[x][y][0] + 5;
this.heuristic[x][y][1] = this.heuristic[x][y][1] + 3;
this.heuristic[x][y][2] = this.heuristic[x][y][2] + 1;
}
}
}
}
/**
* Solves for the A* closed set
* @param voxelCellManager The voxel manager
* @param openSet The open set
* @param openSetLookup The open set lookup table
* @param closetSet The closet set
* @param goalPos The goal position
* @param goalHash The goal hash
* @param maxCost The max allowable cost
*/
private void aStar(
VoxelCellManager voxelCellManager,
PriorityQueue<PathfinderNode> openSet,
Map<Long,PathfinderNode> openSetLookup,
Map<Long,PathfinderNode> closetSet,
Vector3d goalPos,
long goalHash,
long maxCost
){
//tracks whether we've found the goal or not
boolean foundGoal = false;
int countConsidered = 0;
int chunkPosX = 0;
int chunkPosY = 0;
int chunkPosZ = 0;
int voxelPosX = 0;
int voxelPosY = 0;
int voxelPosZ = 0;
while(openSet.size() > 0 && !foundGoal){
//pull from open set
PathfinderNode currentNode = openSet.poll();
long currentCost = currentNode.cost;
openSetLookup.remove(currentNode.hash);
closetSet.put(currentNode.hash, currentNode);
countConsidered++;
//scan all neighbors
for(int x = -1; x <= 1; x++){
if(foundGoal){
continue;
}
for(int y = -1; y <= 1; y++){
if(foundGoal){
continue;
}
for(int z = -1; z <= 1; z++){
if(foundGoal){
continue;
}
if(x == 0 && y == 0 && z == 0){
continue;
}
//calculate chunk offsets
voxelPosX = (currentNode.voxelX + x);
voxelPosY = (currentNode.voxelY + y);
voxelPosZ = (currentNode.voxelZ + z);
if(voxelPosX < 0){
voxelPosX = -1;
} else {
voxelPosX = voxelPosX / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPosY < 0){
voxelPosY = -1;
} else {
voxelPosY = voxelPosY / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPosZ < 0){
voxelPosZ = -1;
} else {
voxelPosZ = voxelPosZ / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
//update world position
chunkPosX = currentNode.worldX + voxelPosX;
chunkPosY = currentNode.worldY + voxelPosY;
chunkPosZ = currentNode.worldZ + voxelPosZ;
voxelPosX = (currentNode.voxelX + x + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
voxelPosY = (currentNode.voxelY + y + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
voxelPosZ = (currentNode.voxelZ + z + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
//error/bounds check
if(chunkPosX < 0 || chunkPosY < 0 || chunkPosZ < 0){
continue;
}
//
//checking if this is the goal
//
//calculte hash for neighbor pos
long newHash = HashUtils.hashVoxel(
chunkPosX,chunkPosY,chunkPosZ,
voxelPosX,voxelPosY,voxelPosZ
);
//check if found goal
if(newHash == goalHash){
foundGoal = true;
PathfinderNode newNode = new PathfinderNode(
chunkPosX, chunkPosY, chunkPosZ,
voxelPosX, voxelPosY, voxelPosZ,
0, newHash, currentNode.hash
);
closetSet.put(goalHash, newNode);
continue;
}
//
//creating a new node
//
//it's a solid block
if(!this.isWalkable(voxelCellManager, new Vector3i(chunkPosX,chunkPosY,chunkPosZ), new Vector3i(voxelPosX,voxelPosY,voxelPosZ))){
continue;
}
//calculate new cost
//TODO: apply heuristic here
Vector3d currPosReal = new Vector3d(
ServerWorldData.convertVoxelToRealSpace(voxelPosX, chunkPosX),
ServerWorldData.convertVoxelToRealSpace(voxelPosY, chunkPosY),
ServerWorldData.convertVoxelToRealSpace(voxelPosZ, chunkPosZ)
);
long newCost = currentCost + (int)currPosReal.distance(goalPos);
//check cost boundary
if(newCost > maxCost){
continue;
}
//push to open set
if(!closetSet.containsKey(newHash) && !openSetLookup.containsKey(newHash)){
PathfinderNode newNode = new PathfinderNode(
chunkPosX, chunkPosY, chunkPosZ,
voxelPosX, voxelPosY, voxelPosZ,
newCost, newHash, currentNode.hash
);
openSet.add(newNode);
openSetLookup.put(newHash, newNode);
}
}
}
}
if(openSet.size() < 1){
throw new Error("Open set ran out of nodes! " + countConsidered);
}
}
if(!foundGoal){
throw new Error("Failed to find goal " + countConsidered);
}
}
/**
* Steps through the A* solver to a given number of closet set items
* @param voxelCellManager The voxel manager
* @param startPoint The start point
* @param endPoint The end point
* @param maxCost The max allowable cost
* @param closetSetSize The size of the closed set to stop at
* @return The closed set from iteration through A*
*/
public List<PathfinderNode> aStarStep(VoxelCellManager voxelCellManager, Vector3d startPoint, Vector3d endPoint, long maxCost, int closetSetSize){
if(startPoint.distance(endPoint) > MAX_DIST){
throw new Error("Distance is outside range provided! " + startPoint.distance(endPoint) + " vs " + MAX_DIST);
}
List<PathfinderNode> rVal = new LinkedList<PathfinderNode>();
//create sets
PriorityQueue<PathfinderNode> openSet = new PriorityQueue<PathfinderNode>();
Map<Long,PathfinderNode> openSetLookup = new HashMap<Long,PathfinderNode>();
Map<Long,PathfinderNode> closetSet = new HashMap<Long,PathfinderNode>();
//add starting node
PathfinderNode startingNode = new PathfinderNode(
ServerWorldData.convertRealToChunkSpace(startPoint.x), ServerWorldData.convertRealToChunkSpace(startPoint.y), ServerWorldData.convertRealToChunkSpace(startPoint.z),
ServerWorldData.convertRealToVoxelSpace(startPoint.x), ServerWorldData.convertRealToVoxelSpace(startPoint.y), ServerWorldData.convertRealToVoxelSpace(startPoint.z),
0, 0, 0
);
openSet.add(startingNode);
//structures used throughout iteration
int chunkPosX = 0;
int chunkPosY = 0;
int chunkPosZ = 0;
int voxelPosX = 0;
int voxelPosY = 0;
int voxelPosZ = 0;
Vector3i endWorldPos = ServerWorldData.convertRealToChunkSpace(endPoint);
Vector3i endVoxelPos = ServerWorldData.convertRealToVoxelSpace(endPoint);
long goalHash = HashUtils.hashVoxel(
endWorldPos.x,endWorldPos.y,endWorldPos.z,
endVoxelPos.x,endVoxelPos.y,endVoxelPos.z
);
//set heuristic
this.setHeuristic(startPoint, endPoint);
//tracks whether we've found the goal or not
boolean foundGoal = false;
int countConsidered = 0;
while(openSet.size() > 0 && !foundGoal && rVal.size() < closetSetSize + 1){
//pull from open set
PathfinderNode currentNode = openSet.poll();
long currentCost = currentNode.cost;
openSetLookup.remove(currentNode.hash);
closetSet.put(currentNode.hash, currentNode);
rVal.add(currentNode);
countConsidered++;
//scan all neighbors
for(int x = -1; x <= 1; x++){
if(foundGoal){
continue;
}
for(int y = -1; y <= 1; y++){
if(foundGoal){
continue;
}
for(int z = -1; z <= 1; z++){
if(foundGoal){
continue;
}
if(x == 0 && y == 0 && z == 0){
continue;
}
//calculate chunk offsets
voxelPosX = (currentNode.voxelX + x);
voxelPosY = (currentNode.voxelY + y);
voxelPosZ = (currentNode.voxelZ + z);
if(voxelPosX < 0){
voxelPosX = -1;
} else {
voxelPosX = voxelPosX / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPosY < 0){
voxelPosY = -1;
} else {
voxelPosY = voxelPosY / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPosZ < 0){
voxelPosZ = -1;
} else {
voxelPosZ = voxelPosZ / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
//update world position
chunkPosX = currentNode.worldX + voxelPosX;
chunkPosY = currentNode.worldY + voxelPosY;
chunkPosZ = currentNode.worldZ + voxelPosZ;
voxelPosX = (currentNode.voxelX + x + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
voxelPosY = (currentNode.voxelY + y + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
voxelPosZ = (currentNode.voxelZ + z + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
//error/bounds check
if(chunkPosX < 0 || chunkPosY < 0 || chunkPosZ < 0){
continue;
}
//
//checking if this is the goal
//
//calculte hash for neighbor pos
long newHash = HashUtils.hashVoxel(
chunkPosX,chunkPosY,chunkPosZ,
voxelPosX,voxelPosY,voxelPosZ
);
//check if found goal
if(newHash == goalHash){
foundGoal = true;
PathfinderNode newNode = new PathfinderNode(
chunkPosX, chunkPosY, chunkPosZ,
voxelPosX, voxelPosY, voxelPosZ,
0, newHash, currentNode.hash
);
closetSet.put(goalHash, newNode);
rVal.add(currentNode);
continue;
}
//
//creating a new node
//
//it's a solid block
if(!this.isWalkable(voxelCellManager, new Vector3i(chunkPosX,chunkPosY,chunkPosZ), new Vector3i(voxelPosX,voxelPosY,voxelPosZ))){
continue;
}
//calculate new cost
//TODO: apply heuristic here
Vector3d currPosReal = new Vector3d(
ServerWorldData.convertVoxelToRealSpace(voxelPosX, chunkPosX),
ServerWorldData.convertVoxelToRealSpace(voxelPosY, chunkPosY),
ServerWorldData.convertVoxelToRealSpace(voxelPosZ, chunkPosZ)
);
long newCost = currentCost + (int)currPosReal.distance(endPoint);
//check cost boundary
if(newCost > maxCost){
continue;
}
//push to open set
if(!closetSet.containsKey(newHash) && !openSetLookup.containsKey(newHash)){
PathfinderNode newNode = new PathfinderNode(
chunkPosX, chunkPosY, chunkPosZ,
voxelPosX, voxelPosY, voxelPosZ,
newCost, newHash, currentNode.hash
);
openSet.add(newNode);
openSetLookup.put(newHash, newNode);
}
}
}
}
if(openSet.size() < 1){
throw new Error("Open set ran out of nodes! " + countConsidered);
}
}
return rVal;
}
/**
* Steps through the A* solver to a given number of closet set items
* @param voxelCellManager The voxel manager
* @param startPoint The start point
* @param endPoint The end point
* @param maxCost The max allowable cost
* @param closetSetSize The size of the closed set to stop at
* @return The closed set from iteration through A*
*/
public List<PathfinderNode> aStarStepOpen(VoxelCellManager voxelCellManager, Vector3d startPoint, Vector3d endPoint, long maxCost, int closetSetSize){
if(startPoint.distance(endPoint) > MAX_DIST){
throw new Error("Distance is outside range provided! " + startPoint.distance(endPoint) + " vs " + MAX_DIST);
}
List<PathfinderNode> rVal = new LinkedList<PathfinderNode>();
//create sets
PriorityQueue<PathfinderNode> openSet = new PriorityQueue<PathfinderNode>();
Map<Long,PathfinderNode> openSetLookup = new HashMap<Long,PathfinderNode>();
Map<Long,PathfinderNode> closetSet = new HashMap<Long,PathfinderNode>();
//add starting node
PathfinderNode startingNode = new PathfinderNode(
ServerWorldData.convertRealToChunkSpace(startPoint.x), ServerWorldData.convertRealToChunkSpace(startPoint.y), ServerWorldData.convertRealToChunkSpace(startPoint.z),
ServerWorldData.convertRealToVoxelSpace(startPoint.x), ServerWorldData.convertRealToVoxelSpace(startPoint.y), ServerWorldData.convertRealToVoxelSpace(startPoint.z),
0, 0, 0
);
openSet.add(startingNode);
//structures used throughout iteration
int chunkPosX = 0;
int chunkPosY = 0;
int chunkPosZ = 0;
int voxelPosX = 0;
int voxelPosY = 0;
int voxelPosZ = 0;
Vector3i endWorldPos = ServerWorldData.convertRealToChunkSpace(endPoint);
Vector3i endVoxelPos = ServerWorldData.convertRealToVoxelSpace(endPoint);
long goalHash = HashUtils.hashVoxel(
endWorldPos.x,endWorldPos.y,endWorldPos.z,
endVoxelPos.x,endVoxelPos.y,endVoxelPos.z
);
//set heuristic
this.setHeuristic(startPoint, endPoint);
//tracks whether we've found the goal or not
boolean foundGoal = false;
int countConsidered = 0;
int iteration = 0;
while(openSet.size() > 0 && !foundGoal && iteration < closetSetSize){
//pull from open set
PathfinderNode currentNode = openSet.poll();
long currentCost = currentNode.cost;
openSetLookup.remove(currentNode.hash);
closetSet.put(currentNode.hash, currentNode);
countConsidered++;
//scan all neighbors
for(int x = -1; x <= 1; x++){
if(foundGoal){
continue;
}
for(int y = -1; y <= 1; y++){
if(foundGoal){
continue;
}
for(int z = -1; z <= 1; z++){
if(foundGoal){
continue;
}
if(x == 0 && y == 0 && z == 0){
continue;
}
//calculate chunk offsets
voxelPosX = (currentNode.voxelX + x);
voxelPosY = (currentNode.voxelY + y);
voxelPosZ = (currentNode.voxelZ + z);
if(voxelPosX < 0){
voxelPosX = -1;
} else {
voxelPosX = voxelPosX / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPosY < 0){
voxelPosY = -1;
} else {
voxelPosY = voxelPosY / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPosZ < 0){
voxelPosZ = -1;
} else {
voxelPosZ = voxelPosZ / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
//update world position
chunkPosX = currentNode.worldX + voxelPosX;
chunkPosY = currentNode.worldY + voxelPosY;
chunkPosZ = currentNode.worldZ + voxelPosZ;
voxelPosX = (currentNode.voxelX + x + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
voxelPosY = (currentNode.voxelY + y + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
voxelPosZ = (currentNode.voxelZ + z + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
//error/bounds check
if(chunkPosX < 0 || chunkPosY < 0 || chunkPosZ < 0){
continue;
}
//
//checking if this is the goal
//
//calculte hash for neighbor pos
long newHash = HashUtils.hashVoxel(
chunkPosX,chunkPosY,chunkPosZ,
voxelPosX,voxelPosY,voxelPosZ
);
//check if found goal
if(newHash == goalHash){
foundGoal = true;
PathfinderNode newNode = new PathfinderNode(
chunkPosX, chunkPosY, chunkPosZ,
voxelPosX, voxelPosY, voxelPosZ,
0, newHash, currentNode.hash
);
closetSet.put(goalHash, newNode);
continue;
}
//
//creating a new node
//
//it's a solid block
if(!this.isWalkable(voxelCellManager, new Vector3i(chunkPosX,chunkPosY,chunkPosZ), new Vector3i(voxelPosX,voxelPosY,voxelPosZ))){
continue;
}
//calculate new cost
//TODO: apply heuristic here
Vector3d currPosReal = new Vector3d(
ServerWorldData.convertVoxelToRealSpace(voxelPosX, chunkPosX),
ServerWorldData.convertVoxelToRealSpace(voxelPosY, chunkPosY),
ServerWorldData.convertVoxelToRealSpace(voxelPosZ, chunkPosZ)
);
long newCost = currentCost + (int)currPosReal.distance(endPoint);
//check cost boundary
if(newCost > maxCost){
continue;
}
//push to open set
if(!closetSet.containsKey(newHash) && !openSetLookup.containsKey(newHash)){
PathfinderNode newNode = new PathfinderNode(
chunkPosX, chunkPosY, chunkPosZ,
voxelPosX, voxelPosY, voxelPosZ,
newCost, newHash, currentNode.hash
);
openSet.add(newNode);
openSetLookup.put(newHash, newNode);
}
}
}
}
if(openSet.size() < 1){
throw new Error("Open set ran out of nodes! " + countConsidered);
}
iteration++;
}
rVal.addAll(openSet);
return rVal;
}
/**
* Performs string pulling on the closed set to get the optimized path
* @param voxelCellManager The voxel cell manager
* @param closetSet The closed set
* @param goalHash The goal hash
* @return The list of minimal points
*/
private List<PathfinderNode> stringPull(
VoxelCellManager voxelCellManager,
Map<Long,PathfinderNode> closetSet,
long goalHash
){
List<PathfinderNode> rVal = new LinkedList<PathfinderNode>();
//reverse the raw path
List<PathfinderNode> pathRaw = new LinkedList<PathfinderNode>();
PathfinderNode current = closetSet.get(goalHash);
if(current == null){
throw new Error("End node not stored in closed set!");
}
while(current.prevNode != current.hash){
pathRaw.add(current);
current = closetSet.get(current.prevNode);
if(current == null){
throw new Error("Node undefined!");
}
}
Collections.reverse(pathRaw);
int i = 0;
while(i < pathRaw.size() - 1){
rVal.add(pathRaw.get(i));
int j = i + 1;
for(; j < pathRaw.size() - 1; j++){
PathfinderNode from = pathRaw.get(i);
PathfinderNode to = pathRaw.get(j);
if(this.hasLineOfSight(voxelCellManager, from, to)) {
break;
}
}
i = j;
}
return rVal;
}
/**
* Performs LOS checks for string pulling
* @param voxelCellManager The voxel cell manager
* @param from The cell to start string pulling from
* @param to THe cell to start string pulling towards
* @return true if we can keep string pulling, false otherwise
*/
private boolean hasLineOfSight(
VoxelCellManager voxelCellManager,
PathfinderNode from,
PathfinderNode to
){
double x0 = ServerWorldData.convertVoxelToRealSpace(from.voxelX,from.worldX);
double y0 = ServerWorldData.convertVoxelToRealSpace(from.voxelY,from.worldY);
double z0 = ServerWorldData.convertVoxelToRealSpace(from.voxelZ,from.worldZ);
double x1 = ServerWorldData.convertVoxelToRealSpace(to.voxelX,to.worldX);
double y1 = ServerWorldData.convertVoxelToRealSpace(to.voxelY,to.worldY);
double z1 = ServerWorldData.convertVoxelToRealSpace(to.voxelZ,to.worldZ);
double dx = Math.abs(x1 - x0);
double dy = Math.abs(y1 - x0);
double dz = Math.abs(z1 - z0);
int sx = Double.compare(x1, x0);
int sy = Double.compare(y1, y0);
int sz = Double.compare(z1, z0);
Vector3d realPos = new Vector3d();
double err1, err2;
if(dx >= dy && dx >= dz){
err1 = 2 * dy - dx;
err2 = 2 * dz - dx;
for(; x0 != x1; x0 += sx){
realPos.set(x0,y0,z0);
if(!this.isWalkable(
voxelCellManager,
ServerWorldData.convertRealToChunkSpace(realPos),
ServerWorldData.convertRealToVoxelSpace(realPos)
)){
return false;
}
if(err1 > 0){
y0 += sy;
err1 -= 2 * dx;
}
if(err2 > 0){
z0 += sz;
err2 -= 2 * dx;
}
err1 += 2 * dy;
err2 += 2 * dz;
}
} else if(dy >= dx && dy >= dz){
err1 = 2 * dx - dy;
err2 = 2 * dz - dy;
for(; y0 != y1; y0 += sy){
realPos.set(x0,y0,z0);
if(!this.isWalkable(
voxelCellManager,
ServerWorldData.convertRealToChunkSpace(realPos),
ServerWorldData.convertRealToVoxelSpace(realPos)
)){
return false;
}
if(err1 > 0){
x0 += sx;
err1 -= 2 * dy;
}
if(err2 > 0){
z0 += sz;
err2 -= 2 * dy;
}
err1 += 2 * dx;
err2 += 2 * dz;
}
} else {
err1 = 2 * dy - dz;
err2 = 2 * dx - dz;
for(; z0 != z1; z0 += sz){
realPos.set(x0,y0,z0);
if(!this.isWalkable(
voxelCellManager,
ServerWorldData.convertRealToChunkSpace(realPos),
ServerWorldData.convertRealToVoxelSpace(realPos)
)){
return false;
}
if(err1 > 0){
y0 += sy;
err1 -= 2 * dz;
}
if(err2 > 0){
x0 += sx;
err2 -= 2 * dz;
}
err1 += 2 * dy;
err2 += 2 * dx;
}
}
return true;
}
/**
* Checks if a voxel is passable
* @param voxelCellManager The voxel cell manager
* @param chunkPos The position of the chunk
* @param voxelPos The position of the boxel
* @return true if it is passable, false otherwise
*/
private boolean isWalkable(VoxelCellManager voxelCellManager, Vector3i chunkPos, Vector3i voxelPos){
int voxelType = voxelCellManager.getVoxelTypeAtLocalPosition(chunkPos, voxelPos);
float voxelWeight = voxelCellManager.getVoxelWeightAtLocalPosition(chunkPos, voxelPos);
//get the type of voxel above the current voxel
int aboveChunk = chunkPos.y + ((voxelPos.y + 1) / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET);
int aboveVoxel = ((voxelPos.y + 1) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET);
ServerTerrainChunk chunk = voxelCellManager.getChunkAtPosition(chunkPos.x, aboveChunk, chunkPos.z);
int aboveType = chunk.getType(voxelPos.x, aboveVoxel, voxelPos.z);
float aboveWeight = chunk.getWeight(voxelPos.x, aboveVoxel, voxelPos.z);
//checks
boolean standingOnGround = voxelType != ServerTerrainChunk.VOXEL_TYPE_AIR && voxelWeight > 0;
boolean aboveIsAir = (aboveType == ServerTerrainChunk.VOXEL_TYPE_AIR || aboveWeight <= 0);
//check blocks as well
BlockChunkData blockChunkData = null;
Vector3i blockPos = new Vector3i();
Vector3i currChunk = new Vector3i(chunkPos);
Vector3i offsets = new Vector3i();
int numBlocks = 0;
for(int x = 0; x < 4; x++){
for(int y = 0; y < 4; y++){
for(int z = 0; z < 4; z++){
blockPos.set(voxelPos);
currChunk.set(chunkPos);
offsets.set(
x,
y + BlockChunkData.BLOCKS_PER_UNIT_DISTANCE,
z
);
VoxelPathfinder.clampBlockOffsets(blockPos, currChunk, offsets);
blockChunkData = voxelCellManager.getBlocksAtPosition(currChunk);
if(blockChunkData.getType(
blockPos.x,
blockPos.y,
blockPos.z
) != BlockChunkData.BLOCK_TYPE_EMPTY){
numBlocks++;
}
}
}
}
boolean blocksChestHeight = numBlocks > 10;
return standingOnGround && aboveIsAir && !blocksChestHeight;
}
/**
* Scans for the nearest walkable position to the specified target point
* @param voxelCellManager The voxel cell manager
* @param targetPoint The target point to scan from
* @param maxScanRadius The maximum radius to scan for walkable positions
* @return The closest point that is walkable, or null if no point is found
*/
public Vector3d scanNearestWalkable(VoxelCellManager voxelCellManager, Vector3d targetPoint, double maxScanRadius){
int radius = 1;
int scanned = 0;
Vector3i originVoxel = ServerWorldData.convertRealToVoxelSpace(targetPoint);
Vector3i originChunk = ServerWorldData.convertRealToChunkSpace(targetPoint);
//check if supplied point is valid
if(this.isWalkable(voxelCellManager, originChunk, originVoxel)){
return targetPoint;
}
Vector3i currVoxel = new Vector3i();
Vector3i currChunk = new Vector3i();
Vector3d realPos;
Vector3i offsets = new Vector3i();
while(true){
scanned = 0;
for(int x = -radius; x <= radius; x++){
for(int y = -radius; y <= radius; y++){
currVoxel.set(originVoxel);
currChunk.set(originChunk);
offsets.set(-radius,x,y);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
return realPos;
}
}
currVoxel.set(originVoxel);
currChunk.set(originChunk);
offsets.set(radius,x,y);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
return realPos;
}
}
currVoxel.set(originVoxel);
currChunk.set(originChunk);
offsets.set(x,-radius,y);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
return realPos;
}
}
currVoxel.set(originVoxel);
currChunk.set(originChunk);
offsets.set(x,radius,y);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
return realPos;
}
}
currVoxel.set(originVoxel);
currChunk.set(originChunk);
offsets.set(x,y,-radius);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
return realPos;
}
}
currVoxel.set(originVoxel);
currChunk.set(originChunk);
offsets.set(x,y,radius);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
return realPos;
}
}
}
}
if(scanned == 0){
break;
}
radius++;
}
return null;
}
/**
* Clamps offsets to valid voxel positions
* @param voxelPos The existing voxel position
* @param chunkPos The existing chunk position
* @param offsets The offsets to apply
*/
private static void clampVoxelOffsets(Vector3i voxelPos, Vector3i chunkPos, Vector3i offsets){
//calculate chunk offsets
voxelPos.x = (voxelPos.x + offsets.x);
voxelPos.y = (voxelPos.y + offsets.y);
voxelPos.z = (voxelPos.z + offsets.z);
if(voxelPos.x < 0){
voxelPos.x = -1;
} else {
voxelPos.x = voxelPos.x / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPos.y < 0){
voxelPos.y = -1;
} else {
voxelPos.y = voxelPos.y / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPos.z < 0){
voxelPos.z = -1;
} else {
voxelPos.z = voxelPos.z / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
//update world position
chunkPos.x = chunkPos.x + voxelPos.x;
chunkPos.y = chunkPos.y + voxelPos.y;
chunkPos.z = chunkPos.z + voxelPos.z;
voxelPos.x = (voxelPos.x + offsets.x + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
voxelPos.y = (voxelPos.y + offsets.y + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
voxelPos.z = (voxelPos.z + offsets.z + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
/**
* Clamps offsets to valid block positions
* @param blockPos The existing block position
* @param chunkPos The existing chunk position
* @param offsets The offsets to apply
*/
private static void clampBlockOffsets(Vector3i blockPos, Vector3i chunkPos, Vector3i offsets){
//calculate chunk offsets
blockPos.x = (blockPos.x + offsets.x);
blockPos.y = (blockPos.y + offsets.y);
blockPos.z = (blockPos.z + offsets.z);
if(blockPos.x < 0){
blockPos.x = -1;
} else {
blockPos.x = blockPos.x / BlockChunkData.CHUNK_DATA_WIDTH;
}
if(blockPos.y < 0){
blockPos.y = -1;
} else {
blockPos.y = blockPos.y / BlockChunkData.CHUNK_DATA_WIDTH;
}
if(blockPos.z < 0){
blockPos.z = -1;
} else {
blockPos.z = blockPos.z / BlockChunkData.CHUNK_DATA_WIDTH;
}
//update world position
chunkPos.x = chunkPos.x + blockPos.x;
chunkPos.y = chunkPos.y + blockPos.y;
chunkPos.z = chunkPos.z + blockPos.z;
blockPos.x = (blockPos.x + offsets.x + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH;
blockPos.y = (blockPos.y + offsets.y + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH;
blockPos.z = (blockPos.z + offsets.z + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH;
}
/**
* A node to use during searching
*/
public static class PathfinderNode implements Comparable<PathfinderNode> {
/**
* The world x position
*/
int worldX;
/**
* The world y position
*/
int worldY;
/**
* The world z position
*/
int worldZ;
/**
* The voxel x position
*/
int voxelX;
/**
* The voxel y position
*/
int voxelY;
/**
* The voxel z position
*/
int voxelZ;
/**
* Cost to get to this node
*/
long cost = 0;
/**
* The hash of this node
*/
long hash = 0;
/**
* The previous node
*/
long prevNode = 0;
public PathfinderNode(
int worldX, int worldY, int worldZ,
int voxelX, int voxelY, int voxelZ,
long cost, long hash, long prevNode
){
this.worldX = worldX;
this.worldY = worldY;
this.worldZ = worldZ;
this.voxelX = voxelX;
this.voxelY = voxelY;
this.voxelZ = voxelZ;
this.cost = cost;
this.hash = hash;
this.prevNode = prevNode;
}
@Override
public int compareTo(PathfinderNode o) {
return (int)(this.cost - o.cost);
}
/**
* Gets the position of the node
* @return The position of the node
*/
public Vector3d getPosition(){
return new Vector3d(
ServerWorldData.convertVoxelToRealSpace(voxelX, worldX),
ServerWorldData.convertVoxelToRealSpace(voxelY, worldY),
ServerWorldData.convertVoxelToRealSpace(voxelZ, worldZ)
);
}
}
}