Merging changes from private branch

This commit is contained in:
Anuken
2025-04-04 11:47:35 -04:00
parent cf5c6d0905
commit b7dbe54d76
161 changed files with 2484 additions and 1137 deletions
+144 -42
View File
@@ -7,6 +7,8 @@ import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.entities.Units.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
@@ -14,6 +16,7 @@ import mindustry.gen.*;
import mindustry.logic.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
@@ -28,11 +31,13 @@ public class BlockIndexer{
private int quadWidth, quadHeight;
/** Stores all ore quadrants on the map. Maps ID to qX to qY to a list of tiles with that ore. */
private IntSeq[][][] ores;
private IntSeq[][][] ores, wallOres;
/** Stores all damaged tile entities by team. */
private Seq<Building>[] damagedTiles = new Seq[Team.all.length];
/** All ores present on the map - can be wall or floor. */
private Seq<Item> allPresentOres = new Seq<>();
/** All ores available on this map. */
private ObjectIntMap<Item> allOres = new ObjectIntMap<>();
private ObjectIntMap<Item> allOres = new ObjectIntMap<>(), allWallOres = new ObjectIntMap<>();
/** Stores teams that are present here as tiles. */
private Seq<Team> activeTeams = new Seq<>(Team.class);
/** Maps teams to a map of flagged tiles by flag. */
@@ -41,6 +46,8 @@ public class BlockIndexer{
private boolean[] blocksPresent;
/** Array used for returning and reusing. */
private Seq<Building> breturnArray = new Seq<>(Building.class);
/** Maps block flag to a list of floor tiles that have it. */
private Seq<Tile>[] floorMap;
public BlockIndexer(){
clearFlags();
@@ -53,15 +60,23 @@ public class BlockIndexer{
addIndex(event.tile);
});
Events.on(TileFloorChangeEvent.class, event -> {
removeFloorIndex(event.tile, event.previous);
addFloorIndex(event.tile, event.floor);
});
Events.on(WorldLoadEvent.class, event -> {
damagedTiles = new Seq[Team.all.length];
flagMap = new Seq[Team.all.length][BlockFlag.all.length];
floorMap = new Seq[BlockFlag.all.length];
activeTeams = new Seq<>(Team.class);
clearFlags();
allOres.clear();
allWallOres.clear();
ores = new IntSeq[content.items().size][][];
wallOres = new IntSeq[content.items().size][][];
quadWidth = Mathf.ceil(world.width() / (float)quadrantSize);
quadHeight = Mathf.ceil(world.height() / (float)quadrantSize);
blocksPresent = new boolean[content.blocks().size];
@@ -78,28 +93,67 @@ public class BlockIndexer{
for(Tile tile : world.tiles){
process(tile);
var drop = tile.drop();
addFloorIndex(tile, tile.floor());
if(drop != null){
int qx = (tile.x / quadrantSize);
int qy = (tile.y / quadrantSize);
//add position of quadrant to list
if(tile.block() == Blocks.air){
if(ores[drop.id] == null){
ores[drop.id] = new IntSeq[quadWidth][quadHeight];
}
if(ores[drop.id][qx][qy] == null){
ores[drop.id][qx][qy] = new IntSeq(false, 16);
}
Item drop;
int qx = tile.x / quadrantSize, qy = tile.y / quadrantSize;
if(tile.block() == Blocks.air){
if((drop = tile.drop()) != null){
//add position of quadrant to list
if(ores[drop.id] == null) ores[drop.id] = new IntSeq[quadWidth][quadHeight];
if(ores[drop.id][qx][qy] == null) ores[drop.id][qx][qy] = new IntSeq(false, 16);
ores[drop.id][qx][qy].add(tile.pos());
allOres.increment(drop);
}
}else if((drop = tile.wallDrop()) != null){
//add position of quadrant to list
if(wallOres[drop.id] == null) wallOres[drop.id] = new IntSeq[quadWidth][quadHeight];
if(wallOres[drop.id][qx][qy] == null) wallOres[drop.id][qx][qy] = new IntSeq(false, 16);
wallOres[drop.id][qx][qy].add(tile.pos());
allWallOres.increment(drop);
}
}
updatePresentOres();
});
}
public Seq<Item> getAllPresentOres(){
return allPresentOres;
}
private void updatePresentOres(){
allPresentOres.clear();
for(Item item : content.items()){
if(hasOre(item) || hasWallOre(item)){
allPresentOres.add(item);
}
}
}
private void removeFloorIndex(Tile tile, Floor floor){
if(floor.flags.size == 0) return;
for(var flag : floor.flags.array){
getFlaggedFloors(flag).remove(tile);
}
}
private void addFloorIndex(Tile tile, Floor floor){
if(floor.flags.size == 0 || !floor.shouldIndex(tile)) return;
for(var flag : floor.flags.array){
getFlaggedFloors(flag).add(tile);
}
}
public Seq<Tile> getFlaggedFloors(BlockFlag flag){
if(floorMap[flag.ordinal()] == null){
floorMap[flag.ordinal()] = new Seq<>(false);
}
return floorMap[flag.ordinal()];
}
public void removeIndex(Tile tile){
var team = tile.team();
if(tile.build != null && tile.isCenter()){
@@ -143,30 +197,37 @@ public class BlockIndexer{
public void addIndex(Tile tile){
process(tile);
var drop = tile.drop();
if(drop != null && ores != null){
int qx = tile.x / quadrantSize;
int qy = tile.y / quadrantSize;
Item drop = tile.drop(), wallDrop = tile.wallDrop();
if(drop == null && wallDrop == null) return;
int qx = tile.x / quadrantSize, qy = tile.y / quadrantSize;
int pos = tile.pos();
if(ores[drop.id] == null){
ores[drop.id] = new IntSeq[quadWidth][quadHeight];
}
if(ores[drop.id][qx][qy] == null){
ores[drop.id][qx][qy] = new IntSeq(false, 16);
}
int pos = tile.pos();
var seq = ores[drop.id][qx][qy];
if(tile.block() == Blocks.air){
//add the index if it is a valid new spot to mine at
if(!seq.contains(pos)){
seq.add(pos);
allOres.increment(drop);
if(tile.block() == Blocks.air){
if(drop != null){ //floor
if(ores[drop.id] == null) ores[drop.id] = new IntSeq[quadWidth][quadHeight];
if(ores[drop.id][qx][qy] == null) ores[drop.id][qx][qy] = new IntSeq(false, 16);
if(ores[drop.id][qx][qy].addUnique(pos)){
int old = allOres.increment(drop); //increment ore count only if not already counted
if(old == 0) updatePresentOres();
}
}else if(seq.contains(pos)){ //otherwise, it likely became blocked, remove it
seq.removeValue(pos);
allOres.increment(drop, -1);
}
if(wallDrop != null && wallOres != null && wallOres[wallDrop.id] != null && wallOres[wallDrop.id][qx][qy] != null && wallOres[wallDrop.id][qx][qy].removeValue(pos)){ //wall
int old = allWallOres.increment(wallDrop, -1);
if(old == 1) updatePresentOres();
}
}else{
if(wallDrop != null){ //wall
if(wallOres[wallDrop.id] == null) wallOres[wallDrop.id] = new IntSeq[quadWidth][quadHeight];
if(wallOres[wallDrop.id][qx][qy] == null) wallOres[wallDrop.id][qx][qy] = new IntSeq(false, 16);
if(wallOres[wallDrop.id][qx][qy].addUnique(pos)){
int old = allWallOres.increment(wallDrop); //increment ore count only if not already counted
if(old == 0) updatePresentOres();
}
}
if(drop != null && ores != null && ores[drop.id] != null&& ores[drop.id][qx][qy] != null && ores[drop.id][qx][qy].removeValue(pos)){ //floor
int old = allOres.increment(drop, -1);
if(old == 1) updatePresentOres();
}
}
@@ -194,6 +255,11 @@ public class BlockIndexer{
return allOres.get(item) > 0;
}
/** @return whether this item is present on this map as a wall ore. */
public boolean hasWallOre(Item item){
return allWallOres.get(item) > 0;
}
/** Returns all damaged tiles by team. */
public Seq<Building> getDamaged(Team team){
if(damagedTiles[team.id] == null){
@@ -348,7 +414,7 @@ public class BlockIndexer{
breturnArray.size = 0;
}
public Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
public Building findEnemyTile(Team team, float x, float y, float range, BuildingPriorityf priority, Boolf<Building> pred){
Building target = null;
float targetDist = 0;
@@ -362,10 +428,10 @@ public class BlockIndexer{
//if a block has the same priority, the closer one should be targeted
float dist = candidate.dst(x, y) - candidate.hitSize() / 2f;
if(target == null ||
//if its closer and is at least equal priority
(dist < targetDist && candidate.block.priority >= target.block.priority) ||
// block has higher priority (so range doesnt matter)
(candidate.block.priority > target.block.priority)){
//if it is closer and is at least equal priority
(dist < targetDist && priority.priority(candidate) >= priority.priority(target)) ||
// block has higher priority (so range doesn't matter)
priority.priority(candidate) > priority.priority(target)){
target = candidate;
targetDist = dist;
}
@@ -374,6 +440,10 @@ public class BlockIndexer{
return target;
}
public Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
return findEnemyTile(team, x, y, range, UnitSorts.buildingDefault, pred);
}
public Building findTile(Team team, float x, float y, float range, Boolf<Building> pred){
return findTile(team, x, y, range, pred, false);
}
@@ -432,11 +502,43 @@ public class BlockIndexer{
return null;
}
/** Find the closest ore wall relative to a position. */
public Tile findClosestWallOre(float xp, float yp, Item item){
//(stolen from foo's client :))))
if(wallOres[item.id] != null){
float minDst = 0f;
Tile closest = null;
for(int qx = 0; qx < quadWidth; qx++){
for(int qy = 0; qy < quadHeight; qy++){
var arr = wallOres[item.id][qx][qy];
if(arr != null && arr.size > 0){
Tile tile = world.tile(arr.first());
if(tile.block() != Blocks.air){
float dst = Mathf.dst2(xp, yp, tile.worldx(), tile.worldy());
if(closest == null || dst < minDst){
closest = tile;
minDst = dst;
}
}
}
}
}
return closest;
}
return null;
}
/** Find the closest ore block relative to a position. */
public Tile findClosestOre(Unit unit, Item item){
return findClosestOre(unit.x, unit.y, item);
}
/** Find the closest ore block relative to a position. */
public Tile findClosestWallOre(Unit unit, Item item){
return findClosestWallOre(unit.x, unit.y, item);
}
private void process(Tile tile){
var team = tile.team();
//only process entity changes with centered tiles
@@ -1082,21 +1082,6 @@ public class ControlPathfinder implements Runnable{
return raycast(unit.team().id, unit.type.pathCost, x1, y1, x2, y2);
}
@Deprecated
public int nextTargetId(){
return 0;
}
@Deprecated
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){
return getPathPosition(unit, pathId, destination, out, null);
}
@Deprecated
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){
return getPathPosition(unit, destination, destination, out, noResultFound);
}
public boolean getPathPosition(Unit unit, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){
return getPathPosition(unit, destination, destination, out, noResultFound);
}
+179 -90
View File
@@ -5,6 +5,7 @@ import arc.func.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.TaskQueue;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.core.*;
@@ -16,11 +17,14 @@ import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.meta.*;
import java.util.*;
import static mindustry.Vars.*;
import static mindustry.world.meta.BlockFlag.*;
public class Pathfinder implements Runnable{
private static final long maxUpdate = Time.millisToNanos(8);
private static final int neverRefresh = Integer.MAX_VALUE;
private static final int updateFPS = 60;
private static final int updateInterval = 1000 / updateFPS;
@@ -30,51 +34,66 @@ public class Pathfinder implements Runnable{
static final int impassable = -1;
public static final int
fieldCore = 0;
fieldCore = 0,
maxFields = 10;
public static final Seq<Prov<Flowfield>> fieldTypes = Seq.with(
EnemyCoreField::new
EnemyCoreField::new
);
public static final int
costGround = 0,
costLegs = 1,
costNaval = 2,
costHover = 3;
costGround = 0,
costLegs = 1,
costNaval = 2,
costNeoplasm = 3,
costNone = 4,
costHover = 5,
maxCosts = 8;
public static final Seq<PathCost> costTypes = Seq.with(
//ground
(team, tile) ->
(PathTile.allDeep(tile) || ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || 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),
//ground
(team, tile) ->
(PathTile.allDeep(tile) || ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || 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.deep(tile) ? 6000 : 0) + //leg units can now drown
(PathTile.solid(tile) ? 5 : 0),
//legs
(team, tile) ->
PathTile.legSolid(tile) ? impassable : 1 +
(PathTile.deep(tile) ? 6000 : 0) + //leg units can now drown
(PathTile.solid(tile) ? 5 : 0),
//water
(team, tile) ->
(!PathTile.liquid(tile) ? 6000 : 1) +
PathTile.health(tile) * 5 +
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
(PathTile.deep(tile) ? 0 : 1) +
(PathTile.damages(tile) ? 35 : 0),
//water
(team, tile) ->
(!PathTile.liquid(tile) ? 6000 : 1) +
PathTile.health(tile) * 5 +
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
(PathTile.deep(tile) ? 0 : 1) +
(PathTile.damages(tile) ? 35 : 0),
//hover
(team, tile) ->
(((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0)
//neoplasm veins
(team, tile) ->
(PathTile.deep(tile) || (PathTile.team(tile) == 0 && PathTile.solid(tile))) ? impassable : 1 +
(PathTile.health(tile) * 3) +
(PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 2 : 0),
//none (flat cost)
(team, tile) -> 1,
//hover
(team, tile) ->
(((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0)
);
/** tile data, see PathTileStruct - kept as a separate array for threading reasons */
int[] tiles = new int[0];
int[] tiles = {};
/** maps team, cost, type to flow field*/
Flowfield[][][] cache;
@@ -86,6 +105,8 @@ public class Pathfinder implements Runnable{
@Nullable Thread thread;
IntSeq tmpArray = new IntSeq();
boolean needsRefresh;
public Pathfinder(){
clearCache();
@@ -100,6 +121,7 @@ public class Pathfinder implements Runnable{
mainList = new Seq<>();
clearCache();
for(int i = 0; i < tiles.length; i++){
Tile tile = world.tiles.geti(i);
tiles[i] = packTile(tile);
@@ -153,10 +175,36 @@ public class Pathfinder implements Runnable{
}
}
});
Events.run(Trigger.afterGameUpdate, () -> {
//only refresh periodically (every 2 frames) to batch flowfield updates
//TODO: is it worth switching to a timestamp based system instead that updates every X milliseconds?
if(needsRefresh && Core.graphics.getFrameId() % 2 == 0){
needsRefresh = false;
//can't iterate through array so use the map, which should not lead to problems
for(Flowfield path : mainList){
//paths with a refresh rate should not be updated by tiles changing
if(path != null && path.needsRefresh()){
synchronized(path.targets){
//TODO: this is super slow and forces a refresh for every tile changed!
path.updateTargetPositions();
}
}
}
//mark every flow field as dirty, so it updates when it's done
queue.post(() -> {
for(Flowfield data : threadList){
data.dirty = true;
}
});
}
});
}
private void clearCache(){
cache = new Flowfield[256][5][5];
cache = new Flowfield[256][maxCosts][maxFields];
}
/** Packs a tile into its internal representation. */
@@ -185,19 +233,19 @@ public class Pathfinder implements Runnable{
int tid = tile.getTeamID();
return PathTile.get(
tile.build == null || !solid || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures
solid,
tile.floor().isLiquid,
tile.legSolid(),
nearLiquid,
nearGround,
nearSolid,
nearLegSolid,
tile.floor().isDeep(),
tile.floor().damageTaken > 0.00001f,
allDeep,
tile.block().teamPassable
tile.build == null || !solid || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures
solid,
tile.floor().isLiquid,
tile.legSolid(),
nearLiquid,
nearGround,
nearSolid,
nearLegSolid,
tile.floor().isDeep(),
tile.floor().damageTaken > 0.00001f,
allDeep,
tile.block().teamPassable
);
}
@@ -223,6 +271,7 @@ public class Pathfinder implements Runnable{
thread = null;
}
queue.clear();
needsRefresh = false;
}
/** Update a tile in the internal pathfinding grid.
@@ -237,23 +286,10 @@ public class Pathfinder implements Runnable{
}
});
//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.updateTargetPositions();
}
}
}
//mark every flow field as dirty, so it updates when it's done
queue.post(() -> {
for(Flowfield data : threadList){
data.dirty = true;
}
});
controlPath.updateTile(tile);
//queue a refresh sometime in the future
needsRefresh = true;
}
/** Thread implementation. */
@@ -307,48 +343,55 @@ public class Pathfinder implements Runnable{
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path){
return getTargetTile(tile, path, true);
}
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path, boolean diagonals){
if(tile == null) return null;
//uninitialized flowfields are not applicable
if(!path.initialized){
//also ignore paths with no targets, there is no destination
if(!path.initialized || path.targets.size == 0){
return tile;
}
//if refresh rate is positive, queue a refresh
if(path.refreshRate > 0 && Time.timeSinceMillis(path.lastUpdateTime) > path.refreshRate){
if(path.refreshRate > 0 && path.refreshRate != neverRefresh && Time.timeSinceMillis(path.lastUpdateTime) > path.refreshRate && path.frontier.size == 0){
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.updateTargetPositions();
path.updateTargetPositions();
//queue an update
queue.post(() -> updateTargets(path));
}
//queue an update
queue.post(() -> updateTargets(path));
}
}
//use complete weights if possible; these contain a complete flow field that is not being updated
int[] values = path.hasComplete ? path.completeWeights : path.weights;
int apos = tile.array();
int res = path.resolution;
int ww = path.width;
int apos = tile.x/res + tile.y/res * ww;
int value = values[apos];
var points = diagonals ? Geometry.d8 : Geometry.d4;
Tile current = null;
int tl = 0;
for(Point2 point : Geometry.d8){
int dx = tile.x + point.x, dy = tile.y + point.y;
for(Point2 point : points){
int dx = tile.x + point.x * res, dy = tile.y + point.y * res;
Tile other = world.tile(dx, dy);
if(other == null) continue;
int packed = world.packArray(dx, dy);
int packed = dx/res + dy/res * ww;
if(values[packed] < value && (current == null || values[packed] < tl) && path.passable(packed) &&
!(point.x != 0 && point.y != 0 && (!path.passable(world.packArray(tile.x + point.x, tile.y)) || !path.passable(world.packArray(tile.x, tile.y + point.y))))){ //diagonal corner trap
!(point.x != 0 && point.y != 0 && (!path.passable(((tile.x + point.x)/res + tile.y/res*ww)) || !path.passable((tile.x/res + (tile.y + point.y)/res*ww))))){ //diagonal corner trap
current = other;
tl = values[packed];
}
@@ -365,13 +408,21 @@ public class Pathfinder implements Runnable{
//increment search, but do not clear the frontier
path.search++;
//search overflow; reset everything.
if(path.search >= Short.MAX_VALUE){
Arrays.fill(path.searches, (short)0);
path.search = 1;
}
synchronized(path.targets){
//add targets
for(int i = 0; i < path.targets.size; i++){
int pos = path.targets.get(i);
if(pos >= path.weights.length) continue;
path.weights[pos] = 0;
path.searches[pos] = path.search;
path.searches[pos] = (short)path.search;
path.frontier.addFirst(pos);
}
}
@@ -390,7 +441,7 @@ public class Pathfinder implements Runnable{
*/
private void registerPath(Flowfield path){
path.lastUpdateTime = Time.millis();
path.setup(tiles.length);
path.setup();
threadList.add(path);
@@ -398,9 +449,7 @@ public class Pathfinder implements Runnable{
Core.app.post(() -> mainList.add(path));
//fill with impassables by default
for(int i = 0; i < tiles.length; i++){
path.weights[i] = impassable;
}
Arrays.fill(path.weights, impassable);
//add targets
for(int i = 0; i < path.targets.size; i++){
@@ -416,6 +465,7 @@ public class Pathfinder implements Runnable{
long start = Time.nanos();
int counter = 0;
int w = path.width, h = path.height;
while(path.frontier.size > 0){
int tile = path.frontier.removeLast();
@@ -423,7 +473,7 @@ public class Pathfinder implements Runnable{
int cost = path.weights[tile];
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
if(path.frontier.size >= world.width() * world.height()){
if(path.frontier.size >= w * h){
path.frontier.clear();
return;
}
@@ -431,12 +481,12 @@ public class Pathfinder implements Runnable{
if(cost != impassable){
for(Point2 point : Geometry.d4){
int dx = (tile % wwidth) + point.x, dy = (tile / wwidth) + point.y;
int dx = (tile % w) + point.x, dy = (tile / w) + point.y;
if(dx < 0 || dy < 0 || dx >= wwidth || dy >= wheight) continue;
if(dx < 0 || dy < 0 || dx >= w || dy >= h) continue;
int newPos = tile + point.x + point.y * wwidth;
int otherCost = path.cost.getCost(path.team.id, tiles[newPos]);
int newPos = dx + dy * w;
int otherCost = path.getCost(tiles, newPos);
if((path.weights[newPos] > cost + otherCost || path.searches[newPos] < path.search) && otherCost != impassable){
path.frontier.addFirst(newPos);
@@ -523,7 +573,7 @@ public class Pathfinder implements Runnable{
* 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. */
/** Refresh rate in milliseconds. <= 0 to disable. */
protected int refreshRate;
/** Team this path is for. Set before using. */
protected Team team = Team.derelict;
@@ -537,12 +587,16 @@ public class Pathfinder implements Runnable{
/** costs of getting to a specific tile */
public int[] weights;
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */
public int[] searches;
public short[] searches;
/** the last "complete" weights of this tilemap. */
public int[] completeWeights;
/** Scaling factor. For example, resolution = 2 means tiles are twice as large. */
public final int resolution;
public final int width, height;
/** search frontier, these are Pos objects */
IntQueue frontier = new IntQueue();
final 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 */
@@ -552,14 +606,44 @@ public class Pathfinder implements Runnable{
/** whether this flow field is ready to be used */
boolean initialized;
void setup(int length){
public Flowfield(){
this(1);
}
public Flowfield(int resolution){
this.resolution = resolution;
this.width = Mathf.ceil((float)wwidth / resolution);
this.height = Mathf.ceil((float)wheight / resolution);
}
void setup(){
int length = width * height;
this.weights = new int[length];
this.searches = new int[length];
this.searches = new short[length];
this.completeWeights = new int[length];
this.frontier.ensureCapacity((length) / 4);
this.initialized = true;
}
public int getCost(int[] tiles, int pos){
return cost.getCost(team.id, tiles[pos]);
}
public boolean hasTargets(){
return targets.size > 0;
}
/** @return the next tile to travel to for this flowfield. Main thread only. */
public @Nullable Tile getNextTile(Tile from, boolean diagonals){
return pathfinder.getTargetTile(from, this, diagonals);
}
/** @return the next tile to travel to for this flowfield. Main thread only. */
public @Nullable Tile getNextTile(Tile from){
return pathfinder.getTargetTile(from, this);
}
public boolean hasCompleteWeights(){
return hasComplete && completeWeights != null;
}
@@ -569,6 +653,11 @@ public class Pathfinder implements Runnable{
getPositions(targets);
}
/** @return whether this flow field should be refreshed after the current block update */
public boolean needsRefresh(){
return refreshRate == 0;
}
protected boolean passable(int pos){
int amount = cost.getCost(team.id, pathfinder.tiles[pos]);
//edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable
+6 -7
View File
@@ -17,7 +17,6 @@ import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.logic.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.defense.turrets.BaseTurret.*;
import mindustry.world.blocks.defense.turrets.*;
import mindustry.world.blocks.storage.*;
@@ -35,7 +34,7 @@ public class RtsAI{
//in order of priority??
static final BlockFlag[] flags = {BlockFlag.generator, BlockFlag.factory, BlockFlag.core, BlockFlag.battery, BlockFlag.drill};
static final ObjectFloatMap<Building> weights = new ObjectFloatMap<>();
static final boolean debug = OS.hasProp("mindustry.debug");
static final boolean debug = OS.hasProp("mindustry.debug") && false;
final Interval timer = new Interval(10);
final TeamData data;
@@ -210,12 +209,12 @@ public class RtsAI{
//defendTarget = aggressor;
defendPos = new Vec2(aggressor.x, aggressor.y);
defendTarget = aggressor;
}else if(false){ //TODO currently ignored, no use defending against nothing
//}else if(false){ //TODO currently ignored, no use defending against nothing
//should it even go there if there's no aggressor found?
Tile closest = defend.findClosestEdge(units.first(), Tile::solid);
if(closest != null){
defendPos = new Vec2(closest.worldx(), closest.worldy());
}
// Tile closest = defend.findClosestEdge(units.first(), Tile::solid);
// if(closest != null){
// defendPos = new Vec2(closest.worldx(), closest.worldy());
// }
}else{
float mindst = Float.MAX_VALUE;
Building build = null;
-7
View File
@@ -3,7 +3,6 @@ package mindustry.ai;
import arc.*;
import arc.func.*;
import arc.scene.style.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ai.types.*;
import mindustry.ctype.*;
@@ -13,10 +12,6 @@ import mindustry.input.*;
/** Defines a pattern of behavior that an RTS-controlled unit should follow. Shows up in the command UI. */
public class UnitCommand extends MappableContent{
/** @deprecated now a content type, use the methods in Vars.content instead */
@Deprecated
public static final Seq<UnitCommand> all = new Seq<>();
public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand, loopPayloadCommand;
/** Name of UI icon (from Icon class). */
@@ -39,8 +34,6 @@ public class UnitCommand extends MappableContent{
this.icon = icon;
this.controller = controller == null ? u -> null : controller;
all.add(this);
}
public UnitCommand(String name, String icon, Binding keybind, Func<Unit, AIController> controller){
+2 -9
View File
@@ -2,30 +2,23 @@ package mindustry.ai;
import arc.*;
import arc.scene.style.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.input.*;
public class UnitStance extends MappableContent{
/** @deprecated now a content type, use the methods in Vars.content instead */
@Deprecated
public static final Seq<UnitStance> all = new Seq<>();
public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram;
/** Name of UI icon (from Icon class). */
public final String icon;
public String icon;
/** Key to press for this stance. */
public @Nullable Binding keybind = null;
public @Nullable Binding keybind;
public UnitStance(String name, String icon, Binding keybind){
super(name);
this.icon = icon;
this.keybind = keybind;
all.add(this);
}
public String localized(){
+7 -13
View File
@@ -82,9 +82,7 @@ public class WaveSpawner{
eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> {
for(int i = 0; i < spawnedf; i++){
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1);
unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
spawnEffect(unit);
spawnUnit(group, spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
}
});
}else{
@@ -95,9 +93,7 @@ public class WaveSpawner{
for(int i = 0; i < spawnedf; i++){
Tmp.v1.rnd(spread);
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1);
unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
spawnEffect(unit);
spawnUnit(group, spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
}
});
}
@@ -106,6 +102,11 @@ public class WaveSpawner{
Time.run(121f, () -> spawning = false);
}
public void spawnUnit(SpawnGroup group, float x, float y){
group.createUnit(group.team == null ? state.rules.waveTeam : group.team, x, y,
Angles.angle(x, y, world.width()/2f * tilesize, world.height()/2f * tilesize), state.wave - 1, this::spawnEffect);
}
public void doShockwave(float x, float y){
Fx.spawnShockwave.at(x, y, state.rules.dropZoneRadius);
Damage.damage(state.rules.waveTeam, x, y, state.rules.dropZoneRadius, 99999999f, true);
@@ -217,15 +218,8 @@ public class WaveSpawner{
/** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */
public void spawnEffect(Unit unit){
spawnEffect(unit, unit.angleTo(world.width()/2f * tilesize, world.height()/2f * tilesize));
}
/** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */
public void spawnEffect(Unit unit, float rotation){
unit.rotation = rotation;
unit.apply(StatusEffects.unmoving, 30f);
unit.apply(StatusEffects.invincible, 60f);
unit.add();
unit.unloaded();
Events.fire(new UnitSpawnEvent(unit));
+3 -2
View File
@@ -118,9 +118,10 @@ public class BuilderAI extends AIController{
Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)));
if(valid){
float range = Math.min(unit.type.buildRange - 20f, 100f);
//move toward the plan
moveTo(req.tile(), unit.type.buildRange - 20f, 20f);
moving = !unit.within(req.tile(), unit.type.buildRange - 10f);
moveTo(req.tile(), range - 10f, 20f);
moving = !unit.within(req.tile(), range);
}else{
//discard invalid plan
unit.plans.removeFirst();
+2 -2
View File
@@ -201,7 +201,7 @@ public class CommandAI extends AIController{
}
targetPos.set(attackTarget);
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.pathType() != Pathfinder.costLegs && stance != UnitStance.ram){
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.type.pathCostId != ControlPathfinder.costIdLegs && stance != UnitStance.ram){
Tile best = build.findClosestEdge(unit, Tile::solid);
if(best != null){
targetPos.set(best);
@@ -470,7 +470,7 @@ public class CommandAI extends AIController{
@Override
public boolean retarget(){
//retarget faster when there is an explicit target
return attackTarget != null ? timer.get(timerTarget, 10) : timer.get(timerTarget, 20);
return timer.get(timerTarget, attackTarget != null ? 10f : 20f);
}
public boolean hasCommand(){
+1 -1
View File
@@ -28,7 +28,7 @@ public class DefenderAI extends AIController{
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
//Sort by max health and closer target.
var result = Units.closest(unit.team, x, y, Math.max(range, 400f), u -> !u.dead() && u.type != unit.type && u.targetable(unit.team) && u.type.playerControllable,
var result = Units.closest(unit.team, x, y, Math.max(range, 400f), u -> !u.dead() && u.type != unit.type && u.targetable(unit.team) && u.playerControllable(),
(u, tx, ty) -> -u.maxHealth + Mathf.dst2(u.x, u.y, tx, ty) / 6400f);
if(result != null) return result;
-1
View File
@@ -15,7 +15,6 @@ public class HugAI extends AIController{
@Override
public void updateMovement(){
Building core = unit.closestEnemyCore();
if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){
+5 -8
View File
@@ -1,6 +1,5 @@
package mindustry.ai.types;
import mindustry.content.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.type.*;
@@ -17,7 +16,7 @@ public class MinerAI extends AIController{
public void updateMovement(){
Building core = unit.closestCore();
if(!(unit.canMine()) || core == null) return;
if(!unit.canMine() || core == null) return;
if(!unit.validMine(unit.mineTile)){
unit.mineTile(null);
@@ -40,19 +39,17 @@ public class MinerAI extends AIController{
mining = false;
}else{
if(timer.get(timerTarget3, 60) && targetItem != null){
ore = indexer.findClosestOre(unit, targetItem);
ore = null;
if(unit.type.mineFloor) ore = indexer.findClosestOre(unit, targetItem);
if(ore == null && unit.type.mineWalls) ore = indexer.findClosestWallOre(unit, targetItem);
}
if(ore != null){
moveTo(ore, unit.type.mineRange / 2f, 20f);
if(ore.block() == Blocks.air && unit.within(ore, unit.type.mineRange)){
if(unit.within(ore, unit.type.mineRange) && unit.validMine(ore)){
unit.mineTile = ore;
}
if(ore.block() != Blocks.air){
mining = false;
}
}
}
}else{