Files
Mindustry/core/src/mindustry/ai/Pathfinder.java
2020-10-18 09:08:47 -04:00

521 lines
17 KiB
Java

package mindustry.ai;
import arc.*;
import arc.func.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import arc.util.async.*;
import mindustry.annotations.Annotations.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.world.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class Pathfinder implements Runnable{
private static final long maxUpdate = Time.millisToNanos(6);
private static final int updateFPS = 60;
private static final int updateInterval = 1000 / updateFPS;
private static final int impassable = -1;
private static final int fieldTimeout = 1000 * 60 * 2;
public static final int
fieldCore = 0,
fieldRally = 1;
public static final Seq<Prov<Flowfield>> fieldTypes = Seq.with(
EnemyCoreField::new,
RallyField::new
);
public static final int
costGround = 0,
costLegs = 1,
costWater = 2;
public static final Seq<PathCost> costTypes = Seq.with(
//ground
(team, tile) -> (PathTile.team(tile) == team.id || PathTile.team(tile) == 0) && PathTile.solid(tile) ? impassable : 1 +
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 6 : 0) +
(PathTile.deep(tile) ? 6000 : 0) +
(PathTile.damages(tile) ? 30 : 0),
//legs
(team, tile) -> PathTile.legSolid(tile) ? impassable : 1 +
(PathTile.solid(tile) ? 5 : 0),
//water
(team, tile) -> PathTile.solid(tile) || !PathTile.liquid(tile) ? 200 : 2 +
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
(PathTile.deep(tile) ? -1 : 0) +
(PathTile.damages(tile) ? 35 : 0)
);
//maps team, cost, type to flow field
Flowfield[][][] cache;
/** tile data, see PathTileStruct */
int[][] tiles = new int[0][0];
/** unordered array of path data for iteration only. DO NOT iterate or access this in the main thread. */
Seq<Flowfield> threadList = new Seq<>(), mainList = new Seq<>();
/** handles task scheduling on the update thread. */
TaskQueue queue = new TaskQueue();
/** Current pathfinding thread */
@Nullable Thread thread;
IntSeq tmpArray = new IntSeq();
public Pathfinder(){
clearCache();
Events.on(WorldLoadEvent.class, event -> {
stop();
//reset and update internal tile array
tiles = new int[world.width()][world.height()];
threadList = new Seq<>();
mainList = new Seq<>();
clearCache();
for(Tile tile : world.tiles){
tiles[tile.x][tile.y] = packTile(tile);
}
start();
});
Events.on(ResetEvent.class, event -> stop());
Events.on(TileChangeEvent.class, event -> updateTile(event.tile));
}
private void clearCache(){
cache = new Flowfield[256][5][5];
}
/** Packs a tile into its internal representation. */
private int packTile(Tile tile){
boolean nearLiquid = false, nearSolid = false, nearGround = false;
for(int i = 0; i < 4; i++){
Tile other = tile.nearby(i);
if(other != null){
if(other.floor().isLiquid) nearLiquid = true;
if(other.solid()) nearSolid = true;
if(!other.floor().isLiquid) nearGround = true;
}
}
return PathTile.get(
tile.build == null || !tile.solid() ? 0 : Math.min((int)(tile.build.health / 40), 80),
tile.getTeamID(),
tile.solid(),
tile.floor().isLiquid,
tile.staticDarkness() >= 2,
nearLiquid,
nearGround,
nearSolid,
tile.floor().isDeep(),
tile.floor().damageTaken > 0.00001f
);
}
/** Starts or restarts the pathfinding thread. */
private void start(){
stop();
thread = Threads.daemon(this);
}
/** Stops the pathfinding thread. */
private void stop(){
if(thread != null){
thread.interrupt();
thread = null;
}
queue.clear();
}
/** Update a tile in the internal pathfinding grid.
* Causes a complete pathfinding reclaculation. Main thread only. */
public void updateTile(Tile tile){
if(net.client()) return;
int x = tile.x, y = tile.y;
tile.getLinkedTiles(t -> {
if(Structs.inBounds(t.x, t.y, tiles)){
tiles[t.x][t.y] = packTile(t);
}
});
//can't iterate through array so use the map, which should not lead to problems
for(Flowfield path : mainList){
if(path != null){
synchronized(path.targets){
path.targets.clear();
path.getPositions(path.targets);
}
}
}
queue.post(() -> {
for(Flowfield data : threadList){
updateTargets(data, x, y);
}
});
}
/** Thread implementation. */
@Override
public void run(){
while(true){
if(net.client()) return;
try{
if(state.isPlaying()){
queue.run();
//total update time no longer than maxUpdate
for(Flowfield data : threadList){
updateFrontier(data, maxUpdate / threadList.size);
//TODO implement timeouts... or don't
/*
//remove flowfields that have 'timed out' so they can be garbage collected and no longer waste space
if(data.refreshRate > 0 && Time.timeSinceMillis(data.lastUpdateTime) > fieldTimeout){
//make sure it doesn't get removed twice
data.lastUpdateTime = Time.millis();
Team team = data.team;
Core.app.post(() -> {
//remove its used state
if(fieldMap[team.id] != null){
fieldMap[team.id].remove(data.target);
fieldMapUsed[team.id].remove(data.target);
}
//remove from main thread list
mainList.remove(data);
});
queue.post(() -> {
//remove from this thread list with a delay
threadList.remove(data);
});
}*/
}
}
try{
Thread.sleep(updateInterval);
}catch(InterruptedException e){
//stop looping when interrupted externally
return;
}
}catch(Throwable e){
e.printStackTrace();
}
}
}
public Flowfield getField(Team team, int costType, int fieldType){
if(cache[team.id][costType][fieldType] == null){
Flowfield field = fieldTypes.get(fieldType).get();
field.team = team;
field.cost = costTypes.get(costType);
field.targets.clear();
field.getPositions(field.targets);
cache[team.id][costType][fieldType] = field;
queue.post(() -> registerPath(field));
}
return cache[team.id][costType][fieldType];
}
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path){
if(tile == null) return null;
//uninitialized flowfields are not applicable
if(!path.initialized){
return tile;
}
//if refresh rate is positive, queue a refresh
if(path.refreshRate > 0 && Time.timeSinceMillis(path.lastUpdateTime) > path.refreshRate){
path.lastUpdateTime = Time.millis();
tmpArray.clear();
path.getPositions(tmpArray);
synchronized(path.targets){
//make sure the position actually changed
if(!(path.targets.size == 1 && tmpArray.size == 1 && path.targets.first() == tmpArray.first())){
path.targets.clear();
path.getPositions(path.targets);
//queue an update
queue.post(() -> updateTargets(path));
}
}
}
int[][] values = path.weights;
int value = values[tile.x][tile.y];
Tile current = null;
int tl = 0;
for(Point2 point : Geometry.d8){
int dx = tile.x + point.x, dy = tile.y + point.y;
Tile other = world.tile(dx, dy);
if(other == null) continue;
if(values[dx][dy] < value && (current == null || values[dx][dy] < tl) && path.passable(dx, dy) &&
!(point.x != 0 && point.y != 0 && (!path.passable(tile.x + point.x, tile.y) || !path.passable(tile.x, tile.y + point.y)))){ //diagonal corner trap
current = other;
tl = values[dx][dy];
}
}
if(current == null || tl == impassable) return tile;
return current;
}
/**
* Clears the frontier, increments the search and sets up all flow sources.
* This only occurs for active teams.
*/
private void updateTargets(Flowfield path, int x, int y){
if(!Structs.inBounds(x, y, path.weights)) return;
if(path.weights[x][y] == 0){
//this was a previous target
path.frontier.clear();
}else if(!path.frontier.isEmpty()){
//skip if this path is processing
return;
}
//update cost of the tile TODO maybe only update the cost when it's not passable
path.weights[x][y] = path.cost.getCost(path.team, tiles[x][y]);
//clear frontier to prevent contamination
path.frontier.clear();
updateTargets(path);
}
/** Increments the search and sets up flow sources. Does not change the frontier. */
private void updateTargets(Flowfield path){
//increment search, but do not clear the frontier
path.search++;
synchronized(path.targets){
//add targets
for(int i = 0; i < path.targets.size; i++){
int pos = path.targets.get(i);
int tx = Point2.x(pos), ty = Point2.y(pos);
path.weights[tx][ty] = 0;
path.searches[tx][ty] = path.search;
path.frontier.addFirst(pos);
}
}
}
private void preloadPath(Flowfield path){
path.targets.clear();
path.getPositions(path.targets);
registerPath(path);
updateFrontier(path, -1);
}
/**
* TODO wrong docs
* Created a new flowfield that aims to get to a certain target for a certain team.
* Pathfinding thread only.
*/
private void registerPath(Flowfield path){
path.lastUpdateTime = Time.millis();
path.setup(tiles.length, tiles[0].length);
threadList.add(path);
//add to main thread's list of paths
Core.app.post(() -> {
mainList.add(path);
//TODO
//if(fieldMap[team.id] != null){
// fieldMap[team.id].put(target, path);
//}
});
//fill with impassables by default
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
path.weights[x][y] = impassable;
}
}
//add targets
for(int i = 0; i < path.targets.size; i++){
int pos = path.targets.get(i);
path.weights[Point2.x(pos)][Point2.y(pos)] = 0;
path.frontier.addFirst(pos);
}
}
/** Update the frontier for a path. Pathfinding thread only. */
private void updateFrontier(Flowfield path, long nsToRun){
long start = Time.nanos();
while(path.frontier.size > 0 && (nsToRun < 0 || Time.timeSinceNanos(start) <= nsToRun)){
Tile tile = world.tile(path.frontier.removeLast());
if(tile == null || path.weights == null) return; //something went horribly wrong, bail
int cost = path.weights[tile.x][tile.y];
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
if(path.frontier.size >= world.width() * world.height()){
path.frontier.clear();
return;
}
if(cost != impassable){
for(Point2 point : Geometry.d4){
int dx = tile.x + point.x, dy = tile.y + point.y;
if(dx < 0 || dy < 0 || dx >= tiles.length || dy >= tiles[0].length) continue;
int otherCost = path.cost.getCost(path.team, tiles[dx][dy]);
if((path.weights[dx][dy] > cost + otherCost || path.searches[dx][dy] < path.search) && otherCost != impassable){
path.frontier.addFirst(Point2.pack(dx, dy));
path.weights[dx][dy] = cost + otherCost;
path.searches[dx][dy] = (short)path.search;
}
}
}
}
}
public static class EnemyCoreField extends Flowfield{
@Override
protected void getPositions(IntSeq out){
for(Tile other : indexer.getEnemy(team, BlockFlag.core)){
out.add(other.pos());
}
//spawn points are also enemies.
if(state.rules.waves && team == state.rules.defaultTeam){
for(Tile other : spawner.getSpawns()){
out.add(other.pos());
}
}
}
}
public static class RallyField extends Flowfield{
@Override
protected void getPositions(IntSeq out){
for(Tile other : indexer.getAllied(team, BlockFlag.rally)){
out.add(other.pos());
}
}
}
public static class PositionTarget extends Flowfield{
public final Position position;
public PositionTarget(Position position){
this.position = position;
this.refreshRate = 900;
}
@Override
public void getPositions(IntSeq out){
out.add(Point2.pack(World.toTile(position.getX()), World.toTile(position.getY())));
}
}
/**
* Data for a flow field to some set of destinations.
* Concrete subclasses must specify a way to fetch costs and destinations.
* */
public static abstract class Flowfield{
/** Refresh rate in milliseconds. Return any number <= 0 to disable. */
protected int refreshRate;
/** Team this path is for. Set before using. */
protected Team team = Team.derelict;
/** Function for calculating path cost. Set before using. */
protected PathCost cost = costTypes.get(costGround);
/** costs of getting to a specific tile */
public int[][] weights;
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */
int[][] searches;
/** search frontier, these are Pos objects */
IntQueue frontier = new IntQueue();
/** all target positions; these positions have a cost of 0, and must be synchronized on! */
final IntSeq targets = new IntSeq();
/** current search ID */
int search = 1;
/** last updated time */
long lastUpdateTime;
/** whether this flow field is ready to be used */
boolean initialized;
void setup(int width, int height){
this.weights = new int[width][height];
this.searches = new int[width][height];
this.frontier.ensureCapacity((width + height) * 3);
this.initialized = true;
}
protected boolean passable(int x, int y){
return cost.getCost(team, pathfinder.tiles[x][y]) != impassable;
}
/** Gets targets to pathfind towards. This must run on the main thread. */
protected abstract void getPositions(IntSeq out);
}
interface PathCost{
int getCost(Team traversing, int tile);
}
/** Holds a copy of tile data for a specific tile position. */
@Struct
class PathTileStruct{
//scaled block health
@StructField(8) int health;
//team of block, if applicable (0 by default)
@StructField(8) int team;
//general solid state
boolean solid;
//whether this block is a liquid that boats can move on
boolean liquid;
//whether this block is solid for leg units that can move over some solid blocks
boolean legSolid;
//whether this block is near liquids
boolean nearLiquid;
//whether this block is near a solid floor tile
boolean nearGround;
//whether this block is near a solid object
boolean nearSolid;
//whether this block is deep / drownable
boolean deep;
//whether the floor damages
boolean damages;
}
}