Files
Mindustry/core/src/mindustry/input/Placement.java
2021-12-01 16:01:49 -05:00

432 lines
16 KiB
Java

package mindustry.input;
import arc.*;
import arc.func.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.pooling.*;
import mindustry.entities.units.*;
import mindustry.world.*;
import mindustry.world.blocks.distribution.*;
import static mindustry.Vars.*;
public class Placement{
private static final Seq<BuildPlan> plans1 = new Seq<>();
private static final Seq<Point2> tmpPoints = new Seq<>(), tmpPoints2 = new Seq<>();
private static final NormalizeResult result = new NormalizeResult();
private static final NormalizeDrawResult drawResult = new NormalizeDrawResult();
private static final Bresenham2 bres = new Bresenham2();
private static final Seq<Point2> points = new Seq<>();
//for pathfinding
private static final IntFloatMap costs = new IntFloatMap();
private static final IntIntMap parents = new IntIntMap();
private static final IntSet closed = new IntSet();
/** Normalize a diagonal line into points. */
public static Seq<Point2> pathfindLine(boolean conveyors, int startX, int startY, int endX, int endY){
Pools.freeAll(points);
points.clear();
if(conveyors && Core.settings.getBool("conveyorpathfinding")){
if(astar(startX, startY, endX, endY)){
return points;
}else{
return normalizeLine(startX, startY, endX, endY);
}
}else{
return bres.lineNoDiagonal(startX, startY, endX, endY, Pools.get(Point2.class, Point2::new), points);
}
}
/** Normalize two points into one straight line, no diagonals. */
public static Seq<Point2> normalizeLine(int startX, int startY, int endX, int endY){
Pools.freeAll(points);
points.clear();
if(Math.abs(startX - endX) > Math.abs(startY - endY)){
//go width
for(int i = 0; i <= Math.abs(startX - endX); i++){
points.add(Pools.obtain(Point2.class, Point2::new).set(startX + i * Mathf.sign(endX - startX), startY));
}
}else{
//go height
for(int i = 0; i <= Math.abs(startY - endY); i++){
points.add(Pools.obtain(Point2.class, Point2::new).set(startX, startY + i * Mathf.sign(endY - startY)));
}
}
return points;
}
public static Seq<Point2> upgradeLine(int startX, int startY, int endX, int endY){
closed.clear();
Pools.freeAll(points);
points.clear();
var build = world.build(startX, startY);
points.add(Pools.obtain(Point2.class, Point2::new).set(startX, startY));
while(build instanceof ChainedBuilding chain && (build.tile.x != endX || build.tile.y != endY) && closed.add(build.id)){
if(chain.next() == null) return pathfindLine(true, startX, startY, endX, endY);
build = chain.next();
points.add(Pools.obtain(Point2.class, Point2::new).set(build.tile.x, build.tile.y));
}
return points;
}
/** Calculates optimal node placement for nodes with spacing. Used for bridges and power nodes. */
public static void calculateNodes(Seq<Point2> points, Block block, int rotation, Boolf2<Point2, Point2> overlapper){
var base = tmpPoints2;
var result = tmpPoints.clear();
base.selectFrom(points, p -> p == points.first() || p == points.peek() || Build.validPlace(block, player.team(), p.x, p.y, rotation));
boolean addedLast = false;
outer:
for(int i = 0; i < base.size;){
var point = base.get(i);
result.add(point);
if(i == base.size - 1) addedLast = true;
//find the furthest node that overlaps this one
for(int j = base.size - 1; j > i; j--){
var other = base.get(j);
boolean over = overlapper.get(point, other);
if(over){
//add node to list and start searching for node that overlaps the next one
i = j;
continue outer;
}
}
//if it got here, that means nothing was found. try to proceed to the next node anyway
i ++;
}
if(!addedLast && !base.isEmpty()) result.add(base.peek());
points.clear();
points.addAll(result);
}
public static boolean isSidePlace(Seq<BuildPlan> plans){
return plans.size > 1 && Mathf.mod(Tile.relativeTo(plans.first().x, plans.first().y, plans.get(1).x, plans.get(1).y) - plans.first().rotation, 2) == 1;
}
public static void calculateBridges(Seq<BuildPlan> plans, ItemBridge bridge){
if(isSidePlace(plans)) return;
//check for orthogonal placement + unlocked state
if(!(plans.first().x == plans.peek().x || plans.first().y == plans.peek().y) || !bridge.unlockedNow()){
return;
}
Boolf<BuildPlan> placeable = plan -> (plan.placeable(player.team())) ||
(plan.tile() != null && plan.tile().block() == plan.block); //don't count the same block as inaccessible
var result = plans1.clear();
var team = player.team();
var rotated = plans.first().tile() != null && plans.first().tile().absoluteRelativeTo(plans.peek().x, plans.peek().y) == Mathf.mod(plans.first().rotation + 2, 4);
outer:
for(int i = 0; i < plans.size;){
var cur = plans.get(i);
result.add(cur);
//gap found
if(i < plans.size - 1 && placeable.get(cur) && !placeable.get(plans.get(i + 1))){
//find the closest valid position within range
for(int j = i + 1; j < plans.size; j++){
var other = plans.get(j);
//out of range now, set to current position and keep scanning forward for next occurrence
if(!bridge.positionsValid(cur.x, cur.y, other.x, other.y)){
//add 'missed' conveyors
for(int k = i + 1; k < j; k++){
result.add(plans.get(k));
}
i = j;
continue outer;
}else if(other.placeable(team)){
//found a link, assign bridges
cur.block = bridge;
other.block = bridge;
if(rotated){
other.config = new Point2(cur.x - other.x, cur.y - other.y);
}else{
cur.config = new Point2(other.x - cur.x, other.y - cur.y);
}
i = j;
continue outer;
}
}
//if it got here, that means nothing was found. this likely means there's a bunch of stuff at the end; add it and bail out
for(int j = i + 1; j < plans.size; j++){
result.add(plans.get(j));
}
break;
}else{
i ++;
}
}
plans.set(result);
}
public static void calculateBridges(Seq<BuildPlan> plans, DirectionBridge bridge, boolean hasJunction){
if(isSidePlace(plans)) return;
//check for orthogonal placement + unlocked state
if(!(plans.first().x == plans.peek().x || plans.first().y == plans.peek().y) || !bridge.unlockedNow()){
return;
}
//TODO for chains of ducts, do not count consecutives in a different rotation as 'placeable'
Boolf<BuildPlan> placeable = plan ->
!(!hasJunction && plan.build() != null && plan.build().block == plan.block && plan.rotation != plan.build().rotation) &&
(plan.placeable(player.team()) ||
(plan.tile() != null && plan.tile().block() == plan.block)); //don't count the same block as inaccessible
var result = plans1.clear();
var team = player.team();
outer:
for(int i = 0; i < plans.size;){
var cur = plans.get(i);
result.add(cur);
//gap found
if(i < plans.size - 1 && placeable.get(cur) && !placeable.get(plans.get(i + 1))){
//find the closest valid position within range
for(int j = i + 2; j < plans.size; j++){
var other = plans.get(j);
//out of range now, set to current position and keep scanning forward for next occurrence
if(!bridge.positionsValid(cur.x, cur.y, other.x, other.y)){
//add 'missed' conveyors
for(int k = i + 1; k < j; k++){
result.add(plans.get(k));
}
i = j;
continue outer;
}else if(other.placeable(team)){
//found a link, assign bridges
cur.block = bridge;
other.block = bridge;
i = j;
continue outer;
}
}
//if it got here, that means nothing was found. this likely means there's a bunch of stuff at the end; add it and bail out
for(int j = i + 1; j < plans.size; j++){
result.add(plans.get(j));
}
break;
}else{
i ++;
}
}
plans.set(result);
}
private static float tileHeuristic(Tile tile, Tile other){
Block block = control.input.block;
if((!other.block().alwaysReplace && !(block != null && block.canReplace(other.block()))) || other.floor().isDeep()){
return 20;
}else{
if(parents.containsKey(tile.pos())){
Tile prev = world.tile(parents.get(tile.pos(), 0));
if(tile.relativeTo(prev) != other.relativeTo(tile)){
return 8;
}
}
}
return 1;
}
private static float distanceHeuristic(int x1, int y1, int x2, int y2){
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
private static boolean validNode(Tile tile, Tile other){
Block block = control.input.block;
if(block != null && block.canReplace(other.block())){
return true;
}else{
return other.block().alwaysReplace;
}
}
private static boolean astar(int startX, int startY, int endX, int endY){
Tile start = world.tile(startX, startY);
Tile end = world.tile(endX, endY);
if(start == end || start == null || end == null) return false;
costs.clear();
closed.clear();
parents.clear();
int nodeLimit = 1000;
int totalNodes = 0;
PQueue<Tile> queue = new PQueue<>(10, (a, b) -> Float.compare(costs.get(a.pos(), 0f) + distanceHeuristic(a.x, a.y, end.x, end.y), costs.get(b.pos(), 0f) + distanceHeuristic(b.x, b.y, end.x, end.y)));
queue.add(start);
boolean found = false;
while(!queue.empty() && totalNodes++ < nodeLimit){
Tile next = queue.poll();
float baseCost = costs.get(next.pos(), 0f);
if(next == end){
found = true;
break;
}
closed.add(Point2.pack(next.x, next.y));
for(Point2 point : Geometry.d4){
int newx = next.x + point.x, newy = next.y + point.y;
Tile child = world.tile(newx, newy);
if(child != null && validNode(next, child)){
if(closed.add(child.pos())){
parents.put(child.pos(), next.pos());
costs.put(child.pos(), tileHeuristic(next, child) + baseCost);
queue.add(child);
}
}
}
}
if(!found) return false;
int total = 0;
points.add(Pools.obtain(Point2.class, Point2::new).set(endX, endY));
Tile current = end;
while(current != start && total++ < nodeLimit){
if(current == null) return false;
int newPos = parents.get(current.pos(), -1);
if(newPos == -1) return false;
points.add(Pools.obtain(Point2.class, Point2::new).set(Point2.x(newPos), Point2.y(newPos)));
current = world.tile(newPos);
}
points.reverse();
return true;
}
/**
* Normalizes a placement area and returns the result, ready to be used for drawing a rectangle.
* Returned x2 and y2 will <i>always</i> be greater than x and y.
* @param block block that will be drawn
* @param startx starting X coordinate
* @param starty starting Y coordinate
* @param endx ending X coordinate
* @param endy ending Y coordinate
* @param snap whether to snap to a line
* @param maxLength maximum length of area
*/
public static NormalizeDrawResult normalizeDrawArea(Block block, int startx, int starty, int endx, int endy, boolean snap, int maxLength, float scaling){
normalizeArea(startx, starty, endx, endy, 0, snap, maxLength);
float offset = block.offset;
drawResult.x = result.x * tilesize;
drawResult.y = result.y * tilesize;
drawResult.x2 = result.x2 * tilesize;
drawResult.y2 = result.y2 * tilesize;
drawResult.x -= block.size * scaling * tilesize / 2;
drawResult.x2 += block.size * scaling * tilesize / 2;
drawResult.y -= block.size * scaling * tilesize / 2;
drawResult.y2 += block.size * scaling * tilesize / 2;
drawResult.x += offset;
drawResult.y += offset;
drawResult.x2 += offset;
drawResult.y2 += offset;
return drawResult;
}
/**
* Normalizes a placement area and returns the result.
* Returned x2 and y2 will <i>always</i> be greater than x and y.
* @param tilex starting X coordinate
* @param tiley starting Y coordinate
* @param endx ending X coordinate
* @param endy ending Y coordinate
* @param snap whether to snap to a line
* @param rotation placement rotation
* @param maxLength maximum length of area
*/
public static NormalizeResult normalizeArea(int tilex, int tiley, int endx, int endy, int rotation, boolean snap, int maxLength){
if(snap){
if(Math.abs(tilex - endx) > Math.abs(tiley - endy)){
endy = tiley;
}else{
endx = tilex;
}
}
if(Math.abs(endx - tilex) > maxLength){
endx = Mathf.sign(endx - tilex) * maxLength + tilex;
}
if(Math.abs(endy - tiley) > maxLength){
endy = Mathf.sign(endy - tiley) * maxLength + tiley;
}
int dx = endx - tilex, dy = endy - tiley;
if(Math.abs(dx) > Math.abs(dy)){
if(dx >= 0){
rotation = 0;
}else{
rotation = 2;
}
}else if(Math.abs(dx) < Math.abs(dy)){
if(dy >= 0){
rotation = 1;
}else{
rotation = 3;
}
}
if(endx < tilex){
int t = endx;
endx = tilex;
tilex = t;
}
if(endy < tiley){
int t = endy;
endy = tiley;
tiley = t;
}
result.x2 = endx;
result.y2 = endy;
result.x = tilex;
result.y = tiley;
result.rotation = rotation;
return result;
}
public static class NormalizeDrawResult{
public float x, y, x2, y2;
}
public static class NormalizeResult{
public int x, y, x2, y2, rotation;
}
}