Merging changes from private branch
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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(){
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)){
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user