Core/src/main/java/eu/univento/core/api/entity/pathfinding/AStar.java

323 lines
9.7 KiB
Java

/*
* Copyright (c) 2017 univento.eu - All rights reserved
* You are not allowed to use, distribute or modify this code
*/
package eu.univento.core.api.entity.pathfinding;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.material.Gate;
import java.util.*;
/**
* @author Adamk11s
* @version 1.0
*/
public class AStar {
private final int sx, sy, sz, ex, ey, ez;
private final World w;
private PathingResult result;
private HashMap<String, Tile> open = new HashMap<>();
private HashMap<String, Tile> closed = new HashMap<>();
private void addToOpenList(Tile t, boolean modify) {
if (open.containsKey(t.getUID())) {
if (modify) {
open.put(t.getUID(), t);
}
} else {
open.put(t.getUID(), t);
}
}
private void addToClosedList(Tile t) {
if (!closed.containsKey(t.getUID())) {
closed.put(t.getUID(), t);
}
}
private final int range;
private final String endUID;
public AStar(Location start, Location end, int range) throws InvalidPathException {
boolean s = true, e = true;
if (!(s = this.isLocationWalkable(start)) || !(e = this.isLocationWalkable(end))) {
throw new InvalidPathException(s, e);
}
this.w = start.getWorld();
this.sx = start.getBlockX();
this.sy = start.getBlockY();
this.sz = start.getBlockZ();
this.ex = end.getBlockX();
this.ey = end.getBlockY();
this.ez = end.getBlockZ();
this.range = range;
short sh = 0;
Tile t = new Tile(sh, sh, sh, null);
t.calculateBoth(sx, sy, sz, ex, ey, ez, true);
this.open.put(t.getUID(), t);
this.processAdjacentTiles(t);
StringBuilder b = new StringBuilder();
b.append(ex - sx).append(ey - sy).append(ez - sz);
this.endUID = b.toString();
}
public Location getEndLocation() {
return new Location(w, ex, ey, ez);
}
public PathingResult getPathingResult() {
return this.result;
}
boolean checkOnce = false;
private int abs(int i) {
return (i < 0 ? -i : i);
}
public ArrayList<Tile> iterate() {
if (!checkOnce) {
// invert the boolean flag
checkOnce ^= true;
if((abs(sx - ex) > range) || (abs(sy - ey) > range) || (abs(sz - ez) > range)){
this.result = PathingResult.NO_PATH;
return null;//jump out
}
}
// while not at end
Tile current = null;
while (canContinue()) {
// get lowest F cost square on open list
current = this.getLowestFTile();
// process tiles
this.processAdjacentTiles(current);
}
if (this.result != PathingResult.SUCCESS) {
return null;
} else {
// path found
LinkedList<Tile> routeTrace = new LinkedList<>();
Tile parent;
routeTrace.add(current);
assert current != null;
while ((parent = current.getParent()) != null) {
routeTrace.add(parent);
current = parent;
}
Collections.reverse(routeTrace);
return new ArrayList<>(routeTrace);
}
}
private boolean canContinue() {
// check if open list is empty, if it is no path has been found
if (open.size() == 0) {
this.result = PathingResult.NO_PATH;
return false;
} else {
if (closed.containsKey(this.endUID)) {
this.result = PathingResult.SUCCESS;
return false;
} else {
return true;
}
}
}
private Tile getLowestFTile() {
double f = 0;
Tile drop = null;
// get lowest F cost square
for (Tile t : open.values()) {
if (f == 0) {
t.calculateBoth(sx, sy, sz, ex, ey, ez, true);
f = t.getF();
drop = t;
} else {
t.calculateBoth(sx, sy, sz, ex, ey, ez, true);
double posF = t.getF();
if (posF < f) {
f = posF;
drop = t;
}
}
}
// drop from open list and add to closed
assert drop != null;
this.open.remove(drop.getUID());
this.addToClosedList(drop);
return drop;
}
private boolean isOnClosedList(Tile t) {
return closed.containsKey(t.getUID());
}
// pass in the current tile as the parent
private void processAdjacentTiles(Tile current) {
// set of possible walk to locations adjacent to current tile
HashSet<Tile> possible = new HashSet<>(26);
for (byte x = -1; x <= 1; x++) {
for (byte y = -1; y <= 1; y++) {
for (byte z = -1; z <= 1; z++) {
if (x == 0 && y == 0 && z == 0) {
continue;// don't check current square
}
Tile t = new Tile((short) (current.getX() + x), (short) (current.getY() + y), (short) (current.getZ() + z), current);
if (!t.isInRange(this.range)) {
// if block is out of bounds continue
continue;
}
if (x != 0 && z != 0 && (y == 0 || y == 1)) {
// check to stop jumping through diagonal blocks
Tile xOff = new Tile((short) (current.getX() + x), (short) (current.getY() + y), current.getZ(), current), zOff = new Tile(current.getX(),
(short) (current.getY() + y), (short) (current.getZ() + z), current);
if (!this.isTileWalkable(xOff) && !this.isTileWalkable(zOff)) {
continue;
}
}
if (this.isOnClosedList(t)) {
// ignore tile
continue;
}
// only process the tile if it can be walked on
if (this.isTileWalkable(t)) {
t.calculateBoth(sx, sy, sz, ex, ey, ez, true);
possible.add(t);
}
}
}
}
for (Tile t : possible) {
// get the reference of the object in the array
Tile openRef;
if ((openRef = this.isOnOpenList(t)) == null) {
// not on open list, so add
this.addToOpenList(t, false);
} else {
// is on open list, check if path to that square is better using
// G cost
if (t.getG() < openRef.getG()) {
// if current path is better, change parent
openRef.setParent(current);
// force updates of F, G and H values.
openRef.calculateBoth(sx, sy, sz, ex, ey, ez, true);
}
}
}
}
private Tile isOnOpenList(Tile t) {
return (open.containsKey(t.getUID()) ? open.get(t.getUID()) : null);
/*
* for (Tile o : open) { if (o.equals(t)) { return o; } } return null;
*/
}
private boolean isTileWalkable(Tile t) {
Location l = new Location(w, (sx + t.getX()), (sy + t.getY()), (sz + t.getZ()));
Block b = l.getBlock();
int i = b.getTypeId();
// lava, fire, wheat and ladders cannot be walked on, and of course air
// 85, 107 and 113 stops npcs climbing fences and fence gates
if (i != 10 && i != 11 && i != 51 && i != 59 && i != 65 && i != 0 && i != 85 && i != 107 && i != 113 && !canBlockBeWalkedThrough(i)) {
// make sure the blocks above are air
if (b.getRelative(0, 1, 0).getTypeId() == 107) {
// fench gate check, if closed continue
Gate g = new Gate(b.getRelative(0, 1, 0).getData());
return (g.isOpen() && (b.getRelative(0, 2, 0).getTypeId() == 0));
}
return (canBlockBeWalkedThrough(b.getRelative(0, 1, 0).getTypeId()) && b.getRelative(0, 2, 0).getTypeId() == 0);
} else {
return false;
}
}
private boolean isLocationWalkable(Location l) {
Block b = l.getBlock();
int i = b.getTypeId();
if (i != 10 && i != 11 && i != 51 && i != 59 && i != 65 && i != 0 && !canBlockBeWalkedThrough(i)) {
// make sure the blocks above are air or can be walked through
return (canBlockBeWalkedThrough(b.getRelative(0, 1, 0).getTypeId()) && b.getRelative(0, 2, 0).getTypeId() == 0);
} else {
return false;
}
}
private boolean canBlockBeWalkedThrough(int id) {
return (id == 0 || id == 6 || id == 50 || id == 63 || id == 30 || id == 31 || id == 32 || id == 37 || id == 38 || id == 39 || id == 40 || id == 55 || id == 66 || id == 75
|| id == 76 || id == 78);
}
public class InvalidPathException extends Exception {
private final boolean s, e;
public InvalidPathException(boolean s, boolean e) {
this.s = s;
this.e = e;
}
public String getErrorReason() {
StringBuilder sb = new StringBuilder();
if (!s) {
sb.append("Start Location was air. ");
}
if (!e) {
sb.append("End Location was air.");
}
return sb.toString();
}
public boolean isStartNotSolid() {
return (!s);
}
public boolean isEndNotSolid() {
return (!e);
}
}
}