diff --git a/src/main/java/bwapi/Bullet.java b/src/main/java/bwapi/Bullet.java index d935b78f..367bdebf 100644 --- a/src/main/java/bwapi/Bullet.java +++ b/src/main/java/bwapi/Bullet.java @@ -2,8 +2,6 @@ import bwapi.ClientData.BulletData; -import java.util.Objects; - /** * An object representing a bullet or missile spawned from an attack. *

diff --git a/src/main/java/bwapi/Cache.java b/src/main/java/bwapi/Cache.java index 1905f9f3..dfd871a1 100644 --- a/src/main/java/bwapi/Cache.java +++ b/src/main/java/bwapi/Cache.java @@ -1,5 +1,14 @@ package bwapi; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; class Cache { private int frame = -1; @@ -35,4 +44,83 @@ class BooleanCache extends Cache{} class OrderCache extends Cache{} class UnitTypeCache extends Cache{} class UpgradeTypeCache extends Cache{} -class TechTypeCache extends Cache{} \ No newline at end of file +class TechTypeCache extends Cache{} + +class ConnectedUnitCache { + private int lastUpdate = -1; + private final Map> connectedUnits = new HashMap<>(); + private final Function condition; + private final Game game; + + ConnectedUnitCache(final Game game, final Function condition) { + this.game = game; + this.condition = condition; + } + + /** + * Lazily update connectedUnits. Only users of the calls pay for it, and only + * pay once per frame. + * Avoids previous O(n^2) implementation which would be costly for + * lategame carrier fights + */ + List getConnected(final Unit unit) { + final int frame = game.getFrameCount(); + if (lastUpdate < frame) { + connectedUnits.values().forEach(List::clear); + for (final Unit u : game.getAllUnits()) { + final Unit owner = condition.apply(u); + if (owner != null) { + connectedUnits.computeIfAbsent(owner, k -> new ArrayList<>()).add(u); + } + } + lastUpdate = frame; + } + List connected = connectedUnits.get(unit); + return connected == null ? + Collections.emptyList() : + Collections.unmodifiableList(connected); + } + + void reset() { + lastUpdate = -1; + connectedUnits.clear(); + } +} + +/** + * Lazily filters and caches an unmodifiable List at most once per frame. + * Only callers of the corresponding getter pay for the filtering, and at most once per frame. + */ +class UnitListCache { + private final Game game; + private final Supplier> source; + private final Predicate filter; + private int lastFrame = -1; + private List cached = Collections.emptyList(); + + UnitListCache(final Game game, final Predicate filter) { + this(game, game::getAllUnits, filter); + } + + UnitListCache(final Game game, final Supplier> source, final Predicate filter) { + this.game = game; + this.source = source; + this.filter = filter; + } + + List get() { + final int frame = game.getFrameCount(); + if (lastFrame != frame) { + cached = source.get().stream() + .filter(filter) + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + lastFrame = frame; + } + return cached; + } + + void reset() { + lastFrame = -1; + cached = Collections.emptyList(); + } +} \ No newline at end of file diff --git a/src/main/java/bwapi/Color.java b/src/main/java/bwapi/Color.java index 61d666b8..2db12b34 100644 --- a/src/main/java/bwapi/Color.java +++ b/src/main/java/bwapi/Color.java @@ -2,7 +2,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.Objects; /** * The Color object is used in drawing routines to specify the color to use. diff --git a/src/main/java/bwapi/CommandTemp.java b/src/main/java/bwapi/CommandTemp.java index d7836ffa..b7b3bcf1 100644 --- a/src/main/java/bwapi/CommandTemp.java +++ b/src/main/java/bwapi/CommandTemp.java @@ -330,7 +330,7 @@ void execute(boolean isCurrentFrame) { case Cancel_Train_Slot: if (command.extra != 0) { if (eventType == EventType.Resource) { - UnitType unitType = unit.getTrainingQueue().get(command.extra); + UnitType unitType = unit.getTrainingQueueAt(command.extra); player.self().minerals.setOrAdd(unitType.mineralPrice(), frame); player.self().gas.setOrAdd(unitType.gasPrice(), frame); @@ -351,7 +351,7 @@ void execute(boolean isCurrentFrame) { case Cancel_Train: { switch (eventType) { case Resource: { - UnitType unitType = unit.getTrainingQueue().get(unit.getTrainingQueueCount() - 1); + UnitType unitType = unit.getTrainingQueueAt(unit.getTrainingQueueCount() - 1); player.self().minerals.setOrAdd(unitType.mineralPrice(), frame); player.self().gas.setOrAdd(unitType.gasPrice(), frame); @@ -366,14 +366,14 @@ void execute(boolean isCurrentFrame) { case Order: { unit.self().trainingQueueCount.setOrAdd(-1, frame); - UnitType unitType = unit.getTrainingQueue().get(unit.getTrainingQueueCount()); + UnitType unitType = unit.getTrainingQueueAt(unit.getTrainingQueueCount()); player.self().supplyUsed[unitType.getRace().id] .setOrAdd(-unitType.supplyRequired(), frame); if (unit.getTrainingQueueCount() == 0) { unit.self().buildType.set(UnitType.None, frame); } else { - UnitType ut = unit.getTrainingQueue().get(unit.getTrainingQueueCount() - 1); + UnitType ut = unit.getTrainingQueueAt(unit.getTrainingQueueCount() - 1); // Actual time decreases, but we'll let it be the buildTime until latency catches up. unit.self().remainingTrainTime.set(ut.buildTime(), frame); unit.self().buildType.set(ut, frame); diff --git a/src/main/java/bwapi/ConnectedUnitCache.java b/src/main/java/bwapi/ConnectedUnitCache.java deleted file mode 100644 index cb241709..00000000 --- a/src/main/java/bwapi/ConnectedUnitCache.java +++ /dev/null @@ -1,44 +0,0 @@ -package bwapi; - -import java.util.*; -import java.util.function.Function; - -class ConnectedUnitCache { - private int lastUpdate = -1; - private final Map> connectedUnits = new HashMap<>(); - private final Function condition; - private final Game game; - - ConnectedUnitCache(final Game game, final Function condition) { - this.game = game; - this.condition = condition; - } - - /** - * Lazily update connectedUnits. Only users of the calls pay for it, and only - * pay once per frame. - * Avoids previous O(n^2) implementation which would be costly for - * lategame carrier fights - */ - List getConnected(final Unit unit) { - final int frame = game.getFrameCount(); - if (lastUpdate < frame) { - connectedUnits.values().forEach(List::clear); - for (final Unit u : game.getAllUnits()) { - final Unit owner = condition.apply(u); - if (owner != null) { - connectedUnits.computeIfAbsent(owner, k -> new ArrayList<>()).add(u); - } - } - lastUpdate = frame; - } - List connected = connectedUnits.get(unit); - return connected == null ? - Collections.emptyList() : - Collections.unmodifiableList(connected); - } - void reset() { - lastUpdate = -1; - connectedUnits.clear(); - } -} \ No newline at end of file diff --git a/src/main/java/bwapi/Force.java b/src/main/java/bwapi/Force.java index bc3027d9..34507150 100644 --- a/src/main/java/bwapi/Force.java +++ b/src/main/java/bwapi/Force.java @@ -3,7 +3,6 @@ import bwapi.ClientData.ForceData; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; /** diff --git a/src/main/java/bwapi/Game.java b/src/main/java/bwapi/Game.java index 6e853aee..254623f0 100644 --- a/src/main/java/bwapi/Game.java +++ b/src/main/java/bwapi/Game.java @@ -42,7 +42,7 @@ public class Game { private static final int REGION_DATA_SIZE = 5000; - private final Set visibleUnits = new HashSet<>(); + private final BitSet visibleUnits = new BitSet(); private List allUnits; private final ClientData clientData; @@ -102,6 +102,10 @@ public class Game { final ConnectedUnitCache loadedUnitsCache = new ConnectedUnitCache(this, Unit::getTransport); final ConnectedUnitCache interceptorsCache = new ConnectedUnitCache(this, Unit::getCarrier); final ConnectedUnitCache larvaCache = new ConnectedUnitCache(this, Unit::getHatchery); + private final UnitListCache minerals = new UnitListCache(this, u -> u.getType().isMineralField()); + private final UnitListCache geysers = new UnitListCache(this, u -> u.getType() == Resource_Vespene_Geyser); + private final UnitListCache neutralUnits = new UnitListCache(this, u -> u.getPlayer().equals(neutral())); + private final UnitListCache selfPylons = new UnitListCache(this, () -> self().getUnits(), u -> u.getType() == Protoss_Pylon); final SideEffectQueue sideEffects = new SideEffectQueue(); @@ -155,6 +159,10 @@ void init() { loadedUnitsCache.reset(); interceptorsCache.reset(); larvaCache.reset(); + minerals.reset(); + geysers.reset(); + neutralUnits.reset(); + selfPylons.reset(); final int forceCount = gameData().getForceCount(); forces = new Force[forceCount]; @@ -312,20 +320,24 @@ void unitCreate(final int id) { void unitShow(final int id) { unitCreate(id); - visibleUnits.add(id); + visibleUnits.set(id); } void unitHide(final int id) { - visibleUnits.remove(id); + visibleUnits.clear(id); } void onFrame(final int frame) { if (frame > 0) { - allUnits = visibleUnits.stream() - .map(i -> units[i]) - .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + final List visible = new ArrayList<>(visibleUnits.cardinality()); + for (int i = visibleUnits.nextSetBit(0); i >= 0; i = visibleUnits.nextSetBit(i + 1)) { + visible.add(units[i]); + } + allUnits = Collections.unmodifiableList(visible); + } + for (final Unit u : getAllUnits()) { + u.updatePosition(frame); } - getAllUnits().forEach(u -> u.updatePosition(frame)); updatePlayers(); } @@ -368,9 +380,7 @@ public List getAllUnits() { * @return List containing @minerals */ public List getMinerals() { - return getAllUnits().stream() - .filter(u -> u.getType().isMineralField()) - .collect(Collectors.toList()); + return minerals.get(); } /** @@ -379,9 +389,7 @@ public List getMinerals() { * @return List containing @geysers */ public List getGeysers() { - return getAllUnits().stream() - .filter(u -> u.getType() == Resource_Vespene_Geyser) - .collect(Collectors.toList()); + return geysers.get(); } /** @@ -391,9 +399,7 @@ public List getGeysers() { * @return List containing all neutral units. */ public List getNeutralUnits() { - return getAllUnits().stream() - .filter(u -> u.getPlayer().equals(neutral())) - .collect(Collectors.toList()); + return neutralUnits.get(); } /** @@ -690,16 +696,16 @@ public List getUnitsOnTile(final int tileX, final int tileY) { * given build tile. */ public List getUnitsOnTile(final int tileX, final int tileY, final UnitFilter pred) { - final TilePosition tile = new TilePosition(tileX, tileY); - if (!tile.isValid(this)) { + final int x = tileX * TilePosition.SIZE_IN_PIXELS; + final int y = tileY * TilePosition.SIZE_IN_PIXELS; + if (tileX < 0 || tileY < 0 || x >= mapPixelWidth() || y >= mapPixelHeight()) { return Collections.emptyList(); } - final Position p = tile.toPosition(); - return getUnitsInRectangle(p.x, p.y, p.x + 32, p.y + 32, pred); + return getUnitsInRectangle(x, y, x + 32, y + 32, pred); } public List getUnitsInRectangle(final int left, final int top, final int right, final int bottom) { - return getUnitsInRectangle(left, top, right, bottom, u -> true); + return getUnitsInRectangle(left, top, right, bottom, null); } /** @@ -714,13 +720,17 @@ public List getUnitsInRectangle(final int left, final int top, final int r * given rectangle bounds. */ public List getUnitsInRectangle(final int left, final int top, final int right, final int bottom, final UnitFilter pred) { - return getAllUnits().stream() - .filter(u -> left <= u.getRight() && top <= u.getBottom() && right >= u.getLeft() && bottom >= u.getTop() && pred.test(u)) - .collect(Collectors.toList()); + final List result = new ArrayList<>(); + for (final Unit u : getAllUnits()) { + if (Unit.overlapsRectangle(u, left, top, right, bottom) && (pred == null || pred.test(u))) { + result.add(u); + } + } + return result; } public List getUnitsInRectangle(final Position leftTop, final Position rightBottom) { - return getUnitsInRectangle(leftTop.x, leftTop.y, rightBottom.x, rightBottom.y, u -> true); + return getUnitsInRectangle(leftTop.x, leftTop.y, rightBottom.x, rightBottom.y, null); } public List getUnitsInRectangle(final Position leftTop, final Position rightBottom, final UnitFilter pred) { @@ -1018,7 +1028,7 @@ public boolean hasPowerPrecise(final int x, final int y) { * @return true if the type at the given position will have power, false if the type at the given position will be unpowered. */ public boolean hasPowerPrecise(final int x, final int y, final UnitType unitType) { - return isValidPosition(x, y) && hasPower(x, y, unitType, self().getUnits().stream().filter(u -> u.getType() == Protoss_Pylon).collect(Collectors.toList())); + return isValidPosition(x, y) && hasPower(x, y, unitType, selfPylons.get()); } public boolean hasPowerPrecise(final Position position) { @@ -1103,13 +1113,13 @@ public boolean canBuildHere(final TilePosition position, final UnitType type) { */ public boolean canBuildHere(final TilePosition position, final UnitType type, final Unit builder, final boolean checkExplored) { // lt = left top, rb = right bottom - final TilePosition lt = builder != null && type.isAddon() ? - position.add(new TilePosition(4, 1)) : // addon build offset - position; - final TilePosition rb = lt.add(type.tileSize()); + final int ltX = builder != null && type.isAddon() ? position.x + 4 : position.x; + final int ltY = builder != null && type.isAddon() ? position.y + 1 : position.y; + final int rbX = ltX + type.tileWidth(); + final int rbY = ltY + type.tileHeight(); // Map limit check - if (!lt.isValid(this) || !(rb.toPosition().subtract(new Position(1, 1)).isValid(this))) { + if (!isValidTile(ltX, ltY) || !isValidPosition(rbX * TilePosition.SIZE_IN_PIXELS - 1, rbY * TilePosition.SIZE_IN_PIXELS - 1)) { return false; } @@ -1117,7 +1127,8 @@ public boolean canBuildHere(final TilePosition position, final UnitType type, fi //matches one of them (and the type is still vespene geyser) if (type.isRefinery()) { for (final Unit g : getGeysers()) { - if (g.getTilePosition().equals(lt)) { + final TilePosition tp = g.getTilePosition(); + if (tp.x == ltX && tp.y == ltY) { return !g.isVisible() || g.getType() == Resource_Vespene_Geyser; } } @@ -1125,8 +1136,8 @@ public boolean canBuildHere(final TilePosition position, final UnitType type, fi } // Tile buildability check - for (int x = lt.x; x < rb.x; ++x) { - for (int y = lt.y; y < rb.y; ++y) { + for (int x = ltX; x < rbX; ++x) { + for (int y = ltY; y < rbY; ++y) { // Check if tile is buildable/unoccupied and explored. if (!isBuildable(x, y) || (checkExplored && !isExplored(x, y))) { return false; @@ -1134,29 +1145,39 @@ public boolean canBuildHere(final TilePosition position, final UnitType type, fi } } + final int targPosX = ltX * TilePosition.SIZE_IN_PIXELS + (type.tileWidth() * TilePosition.SIZE_IN_PIXELS) / 2; + final int targPosY = ltY * TilePosition.SIZE_IN_PIXELS + (type.tileHeight() * TilePosition.SIZE_IN_PIXELS) / 2; + // Check if builder is capable of reaching the building site if (builder != null) { - if (!builder.getType().isBuilding()) { - if (!builder.hasPath(lt.toPosition().add(type.tileSize().toPosition().divide(2)))) { + final UnitType builderType = builder.getType(); + if (!builderType.isBuilding()) { + if (!builder.hasPath(new Position(targPosX, targPosY))) { return false; } - } else if (!builder.getType().isFlyingBuilding() && type != Zerg_Nydus_Canal && !type.isFlagBeacon()) { + } else if (!builderType.isFlyingBuilding() && type != Zerg_Nydus_Canal && !type.isFlagBeacon()) { return false; } } // Ground getUnit dimension check if (type != Special_Start_Location) { - final Position targPos = lt.toPosition().add(type.tileSize().toPosition().divide(2)); - final List unitsInRect = getUnitsInRectangle( - targPos.subtract(new Position(type.dimensionLeft(), type.dimensionUp())), - targPos.add(new Position(type.dimensionRight(), type.dimensionDown())), - u -> !u.isFlying() && !u.isLoaded() && (builder != u || type == Zerg_Nydus_Canal)); - - for (final Unit u : unitsInRect) { - // Addons can be placed over units that can move, pushing them out of the way - if (!(type.isAddon() && u.getType().canMove())) { - return false; + final int rectLeft = targPosX - type.dimensionLeft(); + final int rectTop = targPosY - type.dimensionUp(); + final int rectRight = targPosX + type.dimensionRight(); + final int rectBottom = targPosY + type.dimensionDown(); + final boolean isAddon = type.isAddon(); + final boolean isNydus = type == Zerg_Nydus_Canal; + + for (final Unit u : getAllUnits()) { + if (Unit.overlapsRectangle(u, rectLeft, rectTop, rectRight, rectBottom) + && !u.isFlying() + && !u.isLoaded() + && (builder != u || isNydus)) { + // Addons can be placed over units that can move, pushing them out of the way + if (!(isAddon && u.getType().canMove())) { + return false; + } } } @@ -1164,8 +1185,8 @@ public boolean canBuildHere(final TilePosition position, final UnitType type, fi // Note: Zerg structures that don't require creep can still be placed on creep final boolean needsCreep = type.requiresCreep(); if (type.getRace() != Zerg || needsCreep) { - for (int x = lt.x; x < rb.x; ++x) { - for (int y = lt.y; y < rb.y; ++y) { + for (int x = ltX; x < rbX; ++x) { + for (int y = ltY; y < rbY; ++y) { if (needsCreep != hasCreep(x, y)) { return false; } @@ -1174,7 +1195,7 @@ public boolean canBuildHere(final TilePosition position, final UnitType type, fi } // Power Check - if (type.requiresPsi() && !hasPower(lt, type)) { + if (type.requiresPsi() && !hasPower(ltX, ltY, type)) { return false; } @@ -1187,19 +1208,19 @@ public boolean canBuildHere(final TilePosition position, final UnitType type, fi if ((isVisible(tp) || isVisible(tp.x + 1, tp.y)) && !m.exists()) { continue; // tile position is visible, but mineral is not => mineral does not exist } - if (tp.x > lt.x - 5 && - tp.y > lt.y - 4 && - tp.x < lt.x + 7 && - tp.y < lt.y + 6) { + if (tp.x > ltX - 5 && + tp.y > ltY - 4 && + tp.x < ltX + 7 && + tp.y < ltY + 6) { return false; } } for (final Unit g : getStaticGeysers()) { final TilePosition tp = g.getInitialTilePosition(); - if (tp.x > lt.x - 7 && - tp.y > lt.y - 5 && - tp.x < lt.x + 7 && - tp.y < lt.y + 6) { + if (tp.x > ltX - 7 && + tp.y > ltY - 5 && + tp.x < ltX + 7 && + tp.y < ltY + 6) { return false; } } @@ -1209,7 +1230,7 @@ public boolean canBuildHere(final TilePosition position, final UnitType type, fi // then lands at the new location before building the addon), so we need to do similar checks for the // location that the building will be when it builds the addon. if (builder != null && !builder.getType().isAddon() && type.isAddon()) { - return canBuildHere(lt.subtract(new TilePosition(4, 1)), builder.getType(), builder, checkExplored); + return canBuildHere(new TilePosition(ltX - 4, ltY - 1), builder.getType(), builder, checkExplored); } //if the build site passes all these tests, return true. @@ -1282,7 +1303,7 @@ public boolean canMake(final UnitType type, final Unit builder) { } // Check if there is room - if (builder.getInterceptorCount() + builder.getTrainingQueue().size() >= max_amt) { + if (builder.getInterceptorCount() + builder.getTrainingQueueCount() >= max_amt) { return false; } break; @@ -1295,7 +1316,7 @@ public boolean canMake(final UnitType type, final Unit builder) { } // check if there is room - if (builder.getScarabCount() + builder.getTrainingQueue().size() >= max_amt) { + if (builder.getScarabCount() + builder.getTrainingQueueCount() >= max_amt) { return false; } break; diff --git a/src/main/java/bwapi/PerformanceMetric.java b/src/main/java/bwapi/PerformanceMetric.java index b296c93f..c2f7ff0a 100644 --- a/src/main/java/bwapi/PerformanceMetric.java +++ b/src/main/java/bwapi/PerformanceMetric.java @@ -1,7 +1,5 @@ package bwapi; -import com.sun.jna.platform.win32.Kernel32; - import java.text.DecimalFormat; import java.util.ArrayList; @@ -9,7 +7,7 @@ * Aggregates labeled time series data. */ public class PerformanceMetric { - public class RunningTotal { + public static class RunningTotal { private int samples = 0; private double last = 0d; private double mean = 0d; diff --git a/src/main/java/bwapi/Player.java b/src/main/java/bwapi/Player.java index e2d8710b..283d57f1 100644 --- a/src/main/java/bwapi/Player.java +++ b/src/main/java/bwapi/Player.java @@ -3,8 +3,6 @@ import bwapi.ClientData.PlayerData; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; import static bwapi.UnitType.*; import static bwapi.UpgradeType.*; @@ -28,7 +26,9 @@ public class Player implements Comparable { private final PlayerType playerType; private final TilePosition startLocation; private final Color color; + private final UnitListCache units; private PlayerSelf self = null; + PlayerSelf self() { if (self == null) { self = new PlayerSelf(); @@ -36,6 +36,10 @@ PlayerSelf self() { return self; } + boolean hasLatCom() { + return self != null && game.isLatComEnabled(); + } + Player(final PlayerData playerData, final int id, final Game game) { this.playerData = playerData; this.game = game; @@ -44,6 +48,7 @@ PlayerSelf self() { this.playerType = PlayerType.idToEnum[playerData.getType()]; this.startLocation = new TilePosition(playerData.getStartLocationX(), playerData.getStartLocationY()); this.color = new Color(playerData.getColor()); + this.units = new UnitListCache(game, u -> equals(u.getPlayer())); } /** @@ -77,9 +82,7 @@ public String getName() { * This does not include units that are loaded into transports, @Bunkers, @Refineries, @Assimilators, or @Extractors. */ public List getUnits() { - return game.getAllUnits().stream() - .filter(u -> equals(u.getPlayer())) - .collect(Collectors.toList()); + return units.get(); } /** @@ -208,8 +211,8 @@ public boolean leftGame() { */ public int minerals() { int minerals = playerData.getMinerals(); - if (game.isLatComEnabled() && self().minerals.valid(game.getFrameCount())) { - return minerals + self().minerals.get(); + if (hasLatCom() && self.minerals.valid(game.getFrameCount())) { + return minerals + self.minerals.get(); } return minerals; } @@ -223,8 +226,8 @@ public int minerals() { */ public int gas() { int gas = playerData.getGas(); - if (game.isLatComEnabled() && self().gas.valid(game.getFrameCount())) { - return gas + self().gas.get(); + if (hasLatCom() && self.gas.valid(game.getFrameCount())) { + return gas + self.gas.get(); } return gas; } @@ -367,8 +370,8 @@ public int supplyUsed(Race race) { return 0; } int supplyUsed = playerData.getSupplyUsed(race.id); - if (game.isLatComEnabled() && self().supplyUsed[race.id].valid(game.getFrameCount())) { - return supplyUsed + self().supplyUsed[race.id].get(); + if (hasLatCom() && self.supplyUsed[race.id].valid(game.getFrameCount())) { + return supplyUsed + self.supplyUsed[race.id].get(); } return supplyUsed; } @@ -519,8 +522,8 @@ public boolean hasResearched(final TechType tech) { * @see #hasResearched */ public boolean isResearching(final TechType tech) { - if (game.isLatComEnabled() && self().isResearching[tech.id].valid(game.getFrameCount())) { - return self().isResearching[tech.id].get(); + if (hasLatCom() && self.isResearching[tech.id].valid(game.getFrameCount())) { + return self.isResearching[tech.id].get(); } return playerData.isResearching(tech.id); } @@ -533,8 +536,8 @@ public boolean isResearching(final TechType tech) { * @see Unit#upgrade */ public boolean isUpgrading(final UpgradeType upgrade) { - if (game.isLatComEnabled() && self().isUpgrading[upgrade.id].valid(game.getFrameCount())) { - return self().isUpgrading[upgrade.id].get(); + if (hasLatCom() && self.isUpgrading[upgrade.id].valid(game.getFrameCount())) { + return self.isUpgrading[upgrade.id].get(); } return playerData.isUpgrading(upgrade.id); } diff --git a/src/main/java/bwapi/Point.java b/src/main/java/bwapi/Point.java index 28bff09c..50f50f92 100644 --- a/src/main/java/bwapi/Point.java +++ b/src/main/java/bwapi/Point.java @@ -35,7 +35,7 @@ public double getDistance(T point) { return Math.sqrt(dx * dx + dy * dy); } - private static int getApproxDistance(final int x1, final int y1, final int x2, final int y2) { + static int getApproxDistance(final int x1, final int y1, final int x2, final int y2) { int max = Math.abs(x1 - x2); int min = Math.abs(y1 - y2); if (max < min) { diff --git a/src/main/java/bwapi/Region.java b/src/main/java/bwapi/Region.java index 25bbb83f..d8543755 100644 --- a/src/main/java/bwapi/Region.java +++ b/src/main/java/bwapi/Region.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; /** * Region objects are created by Starcraft: Broodwar to contain several tiles with the same diff --git a/src/main/java/bwapi/TechType.java b/src/main/java/bwapi/TechType.java index 23e549bb..9b1e0507 100644 --- a/src/main/java/bwapi/TechType.java +++ b/src/main/java/bwapi/TechType.java @@ -187,8 +187,13 @@ public enum TechType { static final TechType[] idToEnum = new TechType[46 + 1]; + private List whatUses; + static { - Arrays.stream(TechType.values()).forEach(v -> idToEnum[v.id] = v); + for (final TechType v : TechType.values()) { + idToEnum[v.id] = v; + v.whatUses = Collections.unmodifiableList(Arrays.asList(techWhatUses[v.id])); + } } public final int id; @@ -294,7 +299,7 @@ public boolean targetsPosition() { * @return Set of UnitTypes that can use this ability when researched. */ public List whatUses() { - return Collections.unmodifiableList(Arrays.asList(techWhatUses[id])); + return whatUses; } /** diff --git a/src/main/java/bwapi/Unit.java b/src/main/java/bwapi/Unit.java index 53a3a0da..66278573 100644 --- a/src/main/java/bwapi/Unit.java +++ b/src/main/java/bwapi/Unit.java @@ -59,8 +59,11 @@ public class Unit implements Comparable { private int lastCommandFrame; private UnitCommand lastCommand; - // Don't make non-latcom users pay for latcom in memory usage + // Don't make non-latcom users (or uncommanded/enemy/neutral units) pay for latcom in memory usage. + // self() is only called by CommandTemp when mutating latency-compensated state; getters check hasLatCom() + // so we don't allocate 37 Cache objects per unit merely by reading unit properties. private UnitSelf self = null; + UnitSelf self() { if (self == null) { self = new UnitSelf(); @@ -68,12 +71,17 @@ UnitSelf self() { return self; } + boolean hasLatCom() { + return self != null && game.isLatComEnabled(); + } + Unit(final UnitData unitData, int id, final Game game) { this.unitData = unitData; this.game = game; - updatePosition(0); + position = new Position(unitData.getPositionX(), unitData.getPositionY()); + lastPositionUpdate = 0; initialType = getType(); initialResources = getResources(); @@ -153,8 +161,8 @@ public Player getPlayer() { * @see #getInitialType */ public UnitType getType() { - if (game.isLatComEnabled() && self().type.valid(game.getFrameCount())) { - return self().type.get(); + if (hasLatCom() && self.type.valid(game.getFrameCount())) { + return self.type.get(); } return UnitType.idToEnum[unitData.getType()]; } @@ -310,8 +318,8 @@ public int getBottom() { */ public int getHitPoints() { int hitpoints = unitData.getHitPoints(); - if (game.isLatComEnabled() && self().hitPoints.valid(game.getFrameCount())) { - return hitpoints + self().hitPoints.get(); + if (hasLatCom() && self.hitPoints.valid(game.getFrameCount())) { + return hitpoints + self.hitPoints.get(); } return hitpoints; } @@ -337,8 +345,8 @@ public int getShields() { */ public int getEnergy() { int energy = unitData.getEnergy(); - if (game.isLatComEnabled() && self().energy.valid(game.getFrameCount())) { - return energy + self().energy.get(); + if (hasLatCom() && self.energy.valid(game.getFrameCount())) { + return energy + self.energy.get(); } return energy; } @@ -388,26 +396,41 @@ public int getDistance(final Position target) { } /////// Compute distance + final UnitType type = getType(); + final Position pos = this.position != null ? this.position : getPosition(); + final int left, right, top, bottom; + if (type != null && pos != null) { + left = pos.x - type.dimensionLeft(); + right = pos.x + type.dimensionRight(); + top = pos.y - type.dimensionUp(); + bottom = pos.y + type.dimensionDown(); + } else { + left = getLeft(); + right = getRight(); + top = getTop(); + bottom = getBottom(); + } + // compute x distance - int xDist = getLeft() - target.x; + int xDist = left - target.x; if (xDist < 0) { - xDist = target.x - (getRight() + 1); + xDist = target.x - (right + 1); if (xDist < 0) { xDist = 0; } } // compute y distance - int yDist = getTop() - target.y; + int yDist = top - target.y; if (yDist < 0) { - yDist = target.y - (getBottom() + 1); + yDist = target.y - (bottom + 1); if (yDist < 0) { yDist = 0; } } // compute actual distance - return Position.Origin.getApproxDistance(new Position(xDist, yDist)); + return Point.getApproxDistance(0, 0, xDist, yDist); } public int getDistance(final Unit target) { @@ -423,32 +446,57 @@ public int getDistance(final Unit target) { /////// Compute distance + final UnitType thisType = getType(); + final Position thisPos = this.position != null ? this.position : getPosition(); + final int thisLeft, thisRight, thisTop, thisBottom; + if (thisType != null && thisPos != null) { + thisLeft = thisPos.x - thisType.dimensionLeft(); + thisRight = thisPos.x + thisType.dimensionRight(); + thisTop = thisPos.y - thisType.dimensionUp(); + thisBottom = thisPos.y + thisType.dimensionDown(); + } else { + thisLeft = getLeft(); + thisRight = getRight(); + thisTop = getTop(); + thisBottom = getBottom(); + } + // retrieve left/top/right/bottom values for calculations - final int left = target.getLeft() - 1; - final int top = target.getTop() - 1; - final int right = target.getRight() + 1; - final int bottom = target.getBottom() + 1; + final UnitType targetType = target.getType(); + final Position targetPos = target.position != null ? target.position : target.getPosition(); + final int left, top, right, bottom; + if (targetType != null && targetPos != null) { + left = targetPos.x - targetType.dimensionLeft() - 1; + top = targetPos.y - targetType.dimensionUp() - 1; + right = targetPos.x + targetType.dimensionRight() + 1; + bottom = targetPos.y + targetType.dimensionDown() + 1; + } else { + left = target.getLeft() - 1; + top = target.getTop() - 1; + right = target.getRight() + 1; + bottom = target.getBottom() + 1; + } // compute x distance - int xDist = getLeft() - right; + int xDist = thisLeft - right; if (xDist < 0) { - xDist = left - getRight(); + xDist = left - thisRight; if (xDist < 0) { xDist = 0; } } // compute y distance - int yDist = getTop() - bottom; + int yDist = thisTop - bottom; if (yDist < 0) { - yDist = top - getBottom(); + yDist = top - thisBottom; if (yDist < 0) { yDist = 0; } } // compute actual distance - return Position.Origin.getApproxDistance(new Position(xDist, yDist)); + return Point.getApproxDistance(0, 0, xDist, yDist); } /** @@ -804,8 +852,8 @@ public int getStasisTimer() { * @see #isPlagued */ public int getStimTimer() { - if (game.isLatComEnabled() && self().stimTimer.valid(game.getFrameCount())) { - return self().stimTimer.get(); + if (hasLatCom() && self.stimTimer.valid(game.getFrameCount())) { + return self.stimTimer.get(); } return unitData.getStimTimer(); } @@ -819,8 +867,8 @@ public int getStimTimer() { * incomplete unit will be when completed. */ public UnitType getBuildType() { - if (game.isLatComEnabled() && self().buildType.valid(game.getFrameCount())) { - return self().buildType.get(); + if (hasLatCom() && self.buildType.valid(game.getFrameCount())) { + return self.buildType.get(); } return UnitType.idToEnum[unitData.getBuildType()]; } @@ -845,8 +893,8 @@ public List getTrainingQueue() { * It exists as a more performant alternative to getTrainingQueue(). */ public UnitType getTrainingQueueAt(int i) { - if (game.isLatComEnabled() && self().trainingQueue[i].valid(game.getFrameCount())) { - return self().trainingQueue[i].get(); + if (hasLatCom() && self.trainingQueue[i].valid(game.getFrameCount())) { + return self.trainingQueue[i].get(); } return UnitType.idToEnum[unitData.getTrainingQueue(i)]; } @@ -859,8 +907,8 @@ public UnitType getTrainingQueueAt(int i) { */ public int getTrainingQueueCount() { int count = unitData.getTrainingQueueCount(); - if (game.isLatComEnabled() && self().trainingQueueCount.valid(game.getFrameCount())) { - return count + self().trainingQueueCount.get(); + if (hasLatCom() && self.trainingQueueCount.valid(game.getFrameCount())) { + return count + self.trainingQueueCount.get(); } return count; } @@ -876,8 +924,8 @@ public int getTrainingQueueCount() { * @see #getRemainingResearchTime */ public TechType getTech() { - if (game.isLatComEnabled() && self().tech.valid(game.getFrameCount())) { - return self().tech.get(); + if (hasLatCom() && self.tech.valid(game.getFrameCount())) { + return self.tech.get(); } return TechType.idToEnum[unitData.getTech()]; } @@ -893,8 +941,8 @@ public TechType getTech() { * @see #getRemainingUpgradeTime */ public UpgradeType getUpgrade() { - if (game.isLatComEnabled() && self().upgrade.valid(game.getFrameCount())) { - return self().upgrade.get(); + if (hasLatCom() && self.upgrade.valid(game.getFrameCount())) { + return self.upgrade.get(); } return UpgradeType.idToEnum[unitData.getUpgrade()]; } @@ -906,8 +954,8 @@ public UpgradeType getUpgrade() { * @return Number of frames remaining until the unit's completion. */ public int getRemainingBuildTime() { - if (game.isLatComEnabled() && self().remainingBuildTime.valid(game.getFrameCount())) { - return self().remainingBuildTime.get(); + if (hasLatCom() && self.remainingBuildTime.valid(game.getFrameCount())) { + return self.remainingBuildTime.get(); } return unitData.getRemainingBuildTime(); } @@ -927,8 +975,8 @@ public int getRemainingBuildTime() { * @see #getTrainingQueue */ public int getRemainingTrainTime() { - if (game.isLatComEnabled() && self().remainingTrainTime.valid(game.getFrameCount())) { - return self().remainingTrainTime.get(); + if (hasLatCom() && self.remainingTrainTime.valid(game.getFrameCount())) { + return self.remainingTrainTime.get(); } return unitData.getRemainingTrainTime(); } @@ -946,8 +994,8 @@ public int getRemainingTrainTime() { * @see #getTech */ public int getRemainingResearchTime() { - if (game.isLatComEnabled() && self().remainingResearchTime.valid(game.getFrameCount())) { - return self().remainingResearchTime.get(); + if (hasLatCom() && self.remainingResearchTime.valid(game.getFrameCount())) { + return self.remainingResearchTime.get(); } return unitData.getRemainingResearchTime(); } @@ -963,8 +1011,8 @@ public int getRemainingResearchTime() { * @see #getUpgrade */ public int getRemainingUpgradeTime() { - if (game.isLatComEnabled() && self().remainingUpgradeTime.valid(game.getFrameCount())) { - return self().remainingUpgradeTime.get(); + if (hasLatCom() && self.remainingUpgradeTime.valid(game.getFrameCount())) { + return self.remainingUpgradeTime.get(); } return unitData.getRemainingUpgradeTime(); } @@ -985,8 +1033,8 @@ public int getRemainingUpgradeTime() { * another unit. */ public Unit getBuildUnit() { - if (game.isLatComEnabled() && self().buildUnit.valid(game.getFrameCount())) { - return game.getUnit(self().buildUnit.get()); + if (hasLatCom() && self.buildUnit.valid(game.getFrameCount())) { + return game.getUnit(self.buildUnit.get()); } return game.getUnit(unitData.getBuildUnit()); } @@ -1000,8 +1048,8 @@ public Unit getBuildUnit() { * @see #getOrderTarget */ public Unit getTarget() { - if (game.isLatComEnabled() && self().target.valid(game.getFrameCount())) { - return game.getUnit(self().target.get()); + if (hasLatCom() && self.target.valid(game.getFrameCount())) { + return game.getUnit(self.target.get()); } return game.getUnit(unitData.getTarget()); } @@ -1013,8 +1061,8 @@ public Unit getTarget() { * @return Target position of a movement action. */ public Position getTargetPosition() { - if (game.isLatComEnabled() && self().targetPositionX.valid(game.getFrameCount())) { - return new Position(self().targetPositionX.get(), self().targetPositionY.get()); + if (hasLatCom() && self.targetPositionX.valid(game.getFrameCount())) { + return new Position(self.targetPositionX.get(), self.targetPositionY.get()); } return new Position(unitData.getTargetPositionX(), unitData.getTargetPositionY()); } @@ -1026,8 +1074,8 @@ public Position getTargetPosition() { * @return The primary {@link Order} that the unit is executing. */ public Order getOrder() { - if (game.isLatComEnabled() && self().order.valid(game.getFrameCount())) { - return self().order.get(); + if (hasLatCom() && self.order.valid(game.getFrameCount())) { + return self.order.get(); } return Order.idToEnum[unitData.getOrder()]; } @@ -1040,8 +1088,8 @@ public Order getOrder() { * @return The secondary {@link Order} that the unit is executing. */ public Order getSecondaryOrder() { - if (game.isLatComEnabled() && self().secondaryOrder.valid(game.getFrameCount())) { - return self().secondaryOrder.get(); + if (hasLatCom() && self.secondaryOrder.valid(game.getFrameCount())) { + return self.secondaryOrder.get(); } return Order.idToEnum[unitData.getSecondaryOrder()]; } @@ -1057,8 +1105,8 @@ public Order getSecondaryOrder() { * @see #getOrder */ public Unit getOrderTarget() { - if (game.isLatComEnabled() && self().orderTarget.valid(game.getFrameCount())) { - return game.getUnit(self().orderTarget.get()); + if (hasLatCom() && self.orderTarget.valid(game.getFrameCount())) { + return game.getUnit(self.orderTarget.get()); } return game.getUnit(unitData.getOrderTarget()); } @@ -1073,8 +1121,8 @@ public Unit getOrderTarget() { * @see #getOrder */ public Position getOrderTargetPosition() { - if (game.isLatComEnabled() && self().orderTargetPositionX.valid(game.getFrameCount())) { - return new Position(self().orderTargetPositionX.get(), self().orderTargetPositionY.get()); + if (hasLatCom() && self.orderTargetPositionX.valid(game.getFrameCount())) { + return new Position(self.orderTargetPositionX.get(), self.orderTargetPositionY.get()); } return new Position(unitData.getOrderTargetPositionX(), unitData.getOrderTargetPositionY()); } @@ -1091,8 +1139,8 @@ public Position getOrderTargetPosition() { * @see #getRallyUnit */ public Position getRallyPosition() { - if (game.isLatComEnabled() && self().rallyPositionX.valid(game.getFrameCount())) { - return new Position(self().rallyPositionX.get(), self().rallyPositionY.get()); + if (hasLatCom() && self.rallyPositionX.valid(game.getFrameCount())) { + return new Position(self.rallyPositionX.get(), self.rallyPositionY.get()); } return new Position(unitData.getRallyPositionX(), unitData.getRallyPositionY()); } @@ -1110,8 +1158,8 @@ public Position getRallyPosition() { * @see #getRallyPosition */ public Unit getRallyUnit() { - if (game.isLatComEnabled() && self().rallyUnit.valid(game.getFrameCount())) { - return game.getUnit(self().rallyUnit.get()); + if (hasLatCom() && self.rallyUnit.valid(game.getFrameCount())) { + return game.getUnit(self.rallyUnit.get()); } return game.getUnit(unitData.getRallyUnit()); } @@ -1527,8 +1575,8 @@ public boolean isCloaked() { * @return true if this unit is completed, and false if it is under construction or inaccessible. */ public boolean isCompleted() { - if (game.isLatComEnabled() && self().isCompleted.valid(game.getFrameCount())) { - return self().isCompleted.get(); + if (hasLatCom() && self.isCompleted.valid(game.getFrameCount())) { + return self.isCompleted.get(); } return unitData.isCompleted(); } @@ -1544,8 +1592,8 @@ public boolean isCompleted() { * @see #haltConstruction */ public boolean isConstructing() { - if (game.isLatComEnabled() && self().isConstructing.valid(game.getFrameCount())) { - return self().isConstructing.get(); + if (hasLatCom() && self.isConstructing.valid(game.getFrameCount())) { + return self.isConstructing.get(); } return unitData.isConstructing(); } @@ -1607,8 +1655,8 @@ public boolean isFollowing() { boolean isGathering() { - if (game.isLatComEnabled() && self().isGathering.valid(game.getFrameCount())) { - return self().isGathering.get(); + if (hasLatCom() && self.isGathering.valid(game.getFrameCount())) { + return self.isGathering.get(); } return unitData.isGathering(); } @@ -1722,8 +1770,8 @@ public boolean isHoldingPosition() { * @see Unit#stop */ public boolean isIdle() { - if (game.isLatComEnabled() && self().isIdle.valid(game.getFrameCount())) { - return self().isIdle.get(); + if (hasLatCom() && self.isIdle.valid(game.getFrameCount())) { + return self.isIdle.get(); } return unitData.isIdle(); } @@ -1848,8 +1896,8 @@ public boolean isMaelstrommed() { * @see #getRemainingBuildTime */ public boolean isMorphing() { - if (game.isLatComEnabled() && self().isMorphing.valid(game.getFrameCount())) { - return self().isMorphing.get(); + if (hasLatCom() && self.isMorphing.valid(game.getFrameCount())) { + return self.isMorphing.get(); } return unitData.isMorphing(); } @@ -1861,8 +1909,8 @@ public boolean isMorphing() { * @see #stop */ public boolean isMoving() { - if (game.isLatComEnabled() && self().isMoving.valid(game.getFrameCount())) { - return self().isMoving.get(); + if (hasLatCom() && self.isMoving.valid(game.getFrameCount())) { + return self.isMoving.get(); } return unitData.isMoving(); } @@ -2007,8 +2055,8 @@ public boolean isStuck() { * @see #getRemainingTrainTime */ public boolean isTraining() { - if (game.isLatComEnabled() && self().isTraining.valid(game.getFrameCount())) { - return self().isTraining.get(); + if (hasLatCom() && self.isTraining.valid(game.getFrameCount())) { + return self.isTraining.get(); } return unitData.isTraining(); } @@ -5845,7 +5893,7 @@ public boolean canCancelTrainSlot(int slot, boolean checkCanIssueCommandType, bo return false; } - return isTraining() && slot >= 0 && getTrainingQueue().size() > slot; + return isTraining() && slot >= 0 && getTrainingQueueCount() > slot; } public boolean canCancelMorph() { @@ -6406,10 +6454,28 @@ public int compareTo(final Unit other) { return id - other.id; } + static boolean overlapsRectangle(final Unit u, final int rectLeft, final int rectTop, final int rectRight, final int rectBottom) { + final Position pos = u.position; + if (pos != null) { + final UnitType type = u.getType(); + if (type != null) { + return rectLeft <= pos.x + type.dimensionRight() + && rectTop <= pos.y + type.dimensionDown() + && rectRight >= pos.x - type.dimensionLeft() + && rectBottom >= pos.y - type.dimensionUp(); + } + } + return rectLeft <= u.getRight() && rectTop <= u.getBottom() && rectRight >= u.getLeft() && rectBottom >= u.getTop(); + } + void updatePosition(final int frame) { if (frame > lastPositionUpdate) { lastPositionUpdate = frame; - position = new Position(unitData.getPositionX(), unitData.getPositionY()); + final int newX = unitData.getPositionX(); + final int newY = unitData.getPositionY(); + if (((position.x ^ newX) | (position.y ^ newY)) != 0) { + position = new Position(newX, newY); + } } } } diff --git a/src/main/java/bwapi/UnitType.java b/src/main/java/bwapi/UnitType.java index 940b2b00..67d66d7a 100644 --- a/src/main/java/bwapi/UnitType.java +++ b/src/main/java/bwapi/UnitType.java @@ -265,7 +265,7 @@ public enum UnitType { * @return The maximum width of all unit types, in pixels. */ public static int maxUnitWidth() { - return Arrays.stream(UnitType.values()).max(Comparator.comparingInt(UnitType::width)).get().width(); + return UnitTypeContainer.maxUnitWidth; } /** @@ -275,7 +275,7 @@ public static int maxUnitWidth() { * @return The maximum height of all unit types, in pixels. */ public static int maxUnitHeight() { - return Arrays.stream(UnitType.values()).max(Comparator.comparingInt(UnitType::height)).get().height(); + return UnitTypeContainer.maxUnitHeight; } /** @@ -298,17 +298,7 @@ public Race getRace() { * Returns pair({@link UnitType#None},0) If this unit type cannot be made by the player. */ public Pair whatBuilds() { - // Retrieve the type - final UnitType type = UnitTypeContainer.whatBuilds[id]; - int count = 1; - // Set count to 0 if there is no whatBuilds and 2 if it's an archon - if (type == UnitType.None) { - count = 0; - } else if (this == UnitType.Protoss_Archon || this == UnitType.Protoss_Dark_Archon) { - count = 2; - } - // Return the desired pair - return new Pair<>(type, count); + return UnitTypeContainer.whatBuildsPairs[id]; } /** @@ -365,7 +355,7 @@ public TechType cloakingTech() { * @return List of TechTypes containing ability information. */ public List abilities() { - return Collections.unmodifiableList(Arrays.asList(UnitTypeContainer.unitTechs[id])); + return UnitTypeContainer.unitTechsLists[id]; } /** @@ -374,7 +364,7 @@ public List abilities() { * @return List of UpgradeTypes containing upgrade types that will impact this unit type. */ public List upgrades() { - return Collections.unmodifiableList(Arrays.asList(UnitTypeContainer.upgrades[id])); + return UnitTypeContainer.upgradesLists[id]; } /** @@ -578,7 +568,7 @@ public int tileHeight() { * @return {@link TilePosition} containing the width (x) and height (y) of the unit type, in tiles. */ public TilePosition tileSize() { - return new TilePosition(tileWidth(), tileHeight()); + return UnitTypeContainer.tileSizes[id]; } /** @@ -1194,7 +1184,7 @@ public boolean canBuildAddon() { * @since 4.1.2 */ public List buildsWhat() { - return Collections.unmodifiableList(Arrays.asList(UnitTypeContainer.buildsWhat[id])); + return UnitTypeContainer.buildsWhatLists[id]; } /** @@ -1209,7 +1199,7 @@ public List buildsWhat() { * @since 4.1.2 */ public List researchesWhat() { - return Collections.unmodifiableList(Arrays.asList(UnitTypeContainer.researchesWhat[id])); + return UnitTypeContainer.researchesWhatLists[id]; } /** @@ -1223,7 +1213,7 @@ public List researchesWhat() { * @since 4.1.2 */ public List upgradesWhat() { - return Collections.unmodifiableList(Arrays.asList(UnitTypeContainer.upgradesWhat[id])); + return UnitTypeContainer.upgradesWhatLists[id]; } /** diff --git a/src/main/java/bwapi/UnitTypeContainer.java b/src/main/java/bwapi/UnitTypeContainer.java index 0ac0cd03..8d889491 100644 --- a/src/main/java/bwapi/UnitTypeContainer.java +++ b/src/main/java/bwapi/UnitTypeContainer.java @@ -1,6 +1,8 @@ package bwapi; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -1448,6 +1450,59 @@ class UnitTypeContainer { {},// Factories, {}// Unknown, }; + static final TilePosition[] tileSizes = new TilePosition[UnitType.values().length]; + @SuppressWarnings("unchecked") + static final Pair[] whatBuildsPairs = new Pair[UnitType.values().length]; + @SuppressWarnings("unchecked") + static final List[] unitTechsLists = new List[UnitType.values().length]; + @SuppressWarnings("unchecked") + static final List[] upgradesLists = new List[UnitType.values().length]; + @SuppressWarnings("unchecked") + static final List[] buildsWhatLists = new List[UnitType.values().length]; + @SuppressWarnings("unchecked") + static final List[] researchesWhatLists = new List[UnitType.values().length]; + @SuppressWarnings("unchecked") + static final List[] upgradesWhatLists = new List[UnitType.values().length]; + static final int maxUnitWidth; + static final int maxUnitHeight; + + static { + int maxW = 0; + int maxH = 0; + for (final UnitType ut : UnitType.values()) { + final int id = ut.id; + tileSizes[id] = new TilePosition( + unitDimensions[id][UnitDimensions.tileWidth], + unitDimensions[id][UnitDimensions.tileHeight]); + + final UnitType builderType = whatBuilds[id]; + int count = 1; + if (builderType == UnitType.None) { + count = 0; + } else if (ut == UnitType.Protoss_Archon || ut == UnitType.Protoss_Dark_Archon) { + count = 2; + } + whatBuildsPairs[id] = new Pair<>(builderType, count); + + unitTechsLists[id] = Collections.unmodifiableList(Arrays.asList(unitTechs[id])); + upgradesLists[id] = Collections.unmodifiableList(Arrays.asList(upgrades[id])); + buildsWhatLists[id] = Collections.unmodifiableList(Arrays.asList(buildsWhat[id])); + researchesWhatLists[id] = Collections.unmodifiableList(Arrays.asList(researchesWhat[id])); + upgradesWhatLists[id] = Collections.unmodifiableList(Arrays.asList(upgradesWhat[id])); + + final int w = unitDimensions[id][UnitDimensions.left] + 1 + unitDimensions[id][UnitDimensions.right]; + final int h = unitDimensions[id][UnitDimensions.up] + 1 + unitDimensions[id][UnitDimensions.down]; + if (w > maxW) { + maxW = w; + } + if (h > maxH) { + maxH = h; + } + } + maxUnitWidth = maxW; + maxUnitHeight = maxH; + } + static final List> reqUnitsMap = reqUnitsInit(); private static List> reqUnitsInit() { @@ -1532,6 +1587,10 @@ private static List> reqUnitsInit() { req.get(Protoss_Robotics_Support_Bay.id).put(Protoss_Robotics_Facility, 1); req.get(Protoss_Shield_Battery.id).put(Protoss_Gateway, 1); + for (int i = 0; i < req.size(); i++) { + req.set(i, Collections.unmodifiableMap(req.get(i))); + } + return req; } diff --git a/src/main/java/bwapi/UpgradeType.java b/src/main/java/bwapi/UpgradeType.java index bc8a789d..064e4adc 100644 --- a/src/main/java/bwapi/UpgradeType.java +++ b/src/main/java/bwapi/UpgradeType.java @@ -201,8 +201,13 @@ public enum UpgradeType { static final UpgradeType[] idToEnum = new UpgradeType[62 + 1]; + private List whatUses; + static { - Arrays.stream(UpgradeType.values()).forEach(v -> idToEnum[v.id] = v); + for (final UpgradeType v : UpgradeType.values()) { + idToEnum[v.id] = v; + v.whatUses = Collections.unmodifiableList(Arrays.asList(upgradeWhatUses[v.id])); + } } public final int id; @@ -309,7 +314,7 @@ public UnitType whatUpgrades() { * @return Set of unit types that passively use this upgrade type. */ public List whatUses() { - return Collections.unmodifiableList(Arrays.asList(upgradeWhatUses[id])); + return whatUses; } /** diff --git a/src/test/java/bwapi/CommandTempTest.java b/src/test/java/bwapi/CommandTempTest.java index 9af8b90b..74766973 100644 --- a/src/test/java/bwapi/CommandTempTest.java +++ b/src/test/java/bwapi/CommandTempTest.java @@ -2,9 +2,12 @@ import org.junit.Test; +import java.io.IOException; import java.util.Collections; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -61,4 +64,51 @@ public void cancelTrainSlotShouldShiftQueueWithoutIndexOutOfBounds() { assertEquals(Collections.singletonList(UnitType.Terran_Marine), unit.getTrainingQueue()); } + + @Test + public void unitSelfAndPlayerSelfShouldOnlyBeAllocatedOnCommandWriteInFrame0() throws IOException { + Game game = GameBuilder.createGame(); + game.onFrame(0); + + assertEquals(0, game.getFrameCount()); + assertTrue(game.isLatComEnabled()); + + // Reading unit and player getters across all initial units on Frame 0 must not allocate UnitSelf/PlayerSelf + for (Unit u : game.getAllUnits()) { + u.getType(); + u.getOrder(); + u.isIdle(); + u.getHitPoints(); + assertFalse(u.hasLatCom()); + } + for (Player p : game.getPlayers()) { + p.minerals(); + p.gas(); + p.supplyUsed(); + assertFalse(p.hasLatCom()); + } + + // Issue a real command on Frame 0 to a friendly worker and resource depot + Player self = game.self(); + int initialMinerals = self.minerals(); + Unit worker = self.getUnits().stream().filter(u -> u.getType().isWorker()).findFirst().orElseThrow(AssertionError::new); + Unit depot = self.getUnits().stream().filter(u -> u.getType().isResourceDepot()).findFirst().orElseThrow(AssertionError::new); + + Position moveTarget = new Position(200, 200); + assertTrue(worker.move(moveTarget)); + assertTrue(worker.hasLatCom()); + assertEquals(Order.Move, worker.getOrder()); + assertEquals(moveTarget, worker.getTargetPosition()); + + UnitType workerType = self.getRace().getWorker(); + assertTrue(depot.train(workerType)); + assertTrue(self.hasLatCom()); + assertEquals(initialMinerals - workerType.mineralPrice(), self.minerals()); + + // Neutral units (minerals/geysers) and other players still have no UnitSelf/PlayerSelf allocated + for (Unit mineral : game.getMinerals()) { + assertFalse(mineral.hasLatCom()); + } + assertFalse(game.neutral().hasLatCom()); + } } diff --git a/src/test/java/bwapi/GameTest.java b/src/test/java/bwapi/GameTest.java index f2fdb52a..9391b71b 100644 --- a/src/test/java/bwapi/GameTest.java +++ b/src/test/java/bwapi/GameTest.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doReturn; @@ -210,4 +211,35 @@ public void onFrameShouldRefreshPlayersAndExcludeDefeatedOrLeftPlayers() { assertTrue(game.enemies().isEmpty()); } + + @Test + public void unitListCachesShouldBeLazyAndCachedPerFrame() throws IOException { + Game game = GameBuilder.createGame(); + Player self = game.self(); + Player neutral = game.neutral(); + + Unit mineral = mock(Unit.class); + when(mineral.getType()).thenReturn(UnitType.Resource_Mineral_Field); + when(mineral.getPlayer()).thenReturn(neutral); + + Unit geyser = mock(Unit.class); + when(geyser.getType()).thenReturn(UnitType.Resource_Vespene_Geyser); + when(geyser.getPlayer()).thenReturn(neutral); + + Unit pylon = mock(Unit.class); + when(pylon.getType()).thenReturn(UnitType.Protoss_Pylon); + when(pylon.getPlayer()).thenReturn(self); + + game.setAllUnits(Arrays.asList(mineral, geyser, pylon)); + + assertEquals(Collections.singletonList(mineral), game.getMinerals()); + assertSame(game.getMinerals(), game.getMinerals()); + assertEquals(Collections.singletonList(geyser), game.getGeysers()); + assertSame(game.getGeysers(), game.getGeysers()); + assertEquals(Arrays.asList(mineral, geyser), game.getNeutralUnits()); + assertSame(game.getNeutralUnits(), game.getNeutralUnits()); + assertEquals(Collections.singletonList(pylon), self.getUnits()); + assertSame(self.getUnits(), self.getUnits()); + assertEquals(Arrays.asList(mineral, geyser), neutral.getUnits()); + } } \ No newline at end of file diff --git a/src/test/java/bwapi/PerformanceBenchmark.java b/src/test/java/bwapi/PerformanceBenchmark.java new file mode 100644 index 00000000..e50e296a --- /dev/null +++ b/src/test/java/bwapi/PerformanceBenchmark.java @@ -0,0 +1,384 @@ +package bwapi; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.SplittableRandom; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 3, time = 2) +@Measurement(iterations = 5, time = 2) +@Fork(1) +@State(Scope.Thread) +public class PerformanceBenchmark { + + private static final String MAP_NAME = "(4)Andromeda.scx"; + + Game game; + ClientData.GameData gameData; + + List allUnits; + List friendlyCombatUnits; + List enemyCombatUnits; + List mobileUnits; + ClientData.UnitData[] mobileUnitData; + int[] baseMobileX; + int[] baseMobileY; + + Position[] targetPositions; + Position[] rectTopLefts; + Position[] rectBottomRights; + TilePosition[] buildCandidateTiles; + Unit builderWorker; + + int currentFrame; + + @Setup(Level.Trial) + public void setup() throws IOException { + game = GameBuilder.createGame(MAP_NAME); + gameData = game.botClientData().gameData(); + + // 1. Show all initial map units (minerals, geysers, starting depot + 4 workers) + // On (4)Andromeda.scx this is ~190 static/initial units clustered at IDs 0..initialCount-1 + int initialCount = gameData.getInitialUnitCount(); + for (int id = 0; id < initialCount; id++) { + Unit u = game.getUnit(id); + if (u != null) { + game.unitShow(id); + } + } + + SplittableRandom rng = new SplittableRandom(42L); + int selfId = game.self().getID(); + int enemyId = game.enemy().getID(); + + TilePosition selfStart = game.self().getStartLocation(); + int startPixelX = selfStart.x * 32; + int startPixelY = selfStart.y * 32; + + int nextId = initialCount; + + // 2. Add 50 stationary buildings (25 friendly, 25 enemy) contiguous after initial map resources + UnitType[] buildingTypes = { + UnitType.Terran_Supply_Depot, + UnitType.Terran_Barracks, + UnitType.Terran_Factory, + UnitType.Terran_Missile_Turret, + UnitType.Terran_Bunker + }; + for (int i = 0; i < 50; i++) { + int id = nextId++; + UnitType type = buildingTypes[i % buildingTypes.length]; + int owner = (i < 25) ? selfId : enemyId; + int px = clampPixelX(startPixelX + rng.nextInt(-600, 600)); + int py = clampPixelY(startPixelY + rng.nextInt(-600, 600)); + populateUnitData(id, owner, type, px, py); + game.unitShow(id); + } + + // 3. Add 120 mobile combat/worker units (60 friendly, 60 enemy) at the tail of the unit table + UnitType[] mobileTypes = { + UnitType.Terran_Marine, + UnitType.Terran_Medic, + UnitType.Terran_Siege_Tank_Tank_Mode, + UnitType.Terran_Vulture, + UnitType.Terran_Goliath, + UnitType.Terran_Science_Vessel + }; + friendlyCombatUnits = new ArrayList<>(60); + enemyCombatUnits = new ArrayList<>(60); + mobileUnits = new ArrayList<>(120); + mobileUnitData = new ClientData.UnitData[120]; + baseMobileX = new int[120]; + baseMobileY = new int[120]; + + for (int i = 0; i < 120; i++) { + int id = nextId++; + UnitType type = mobileTypes[i % mobileTypes.length]; + boolean isFriendly = (i < 60); + int owner = isFriendly ? selfId : enemyId; + int px = clampPixelX(startPixelX + rng.nextInt(-1000, 1000)); + int py = clampPixelY(startPixelY + rng.nextInt(-1000, 1000)); + + populateUnitData(id, owner, type, px, py); + game.unitShow(id); + + Unit u = game.getUnit(id); + mobileUnits.add(u); + mobileUnitData[i] = gameData.getUnits(id); + baseMobileX[i] = px; + baseMobileY[i] = py; + + if (isFriendly) { + friendlyCombatUnits.add(u); + } else { + enemyCombatUnits.add(u); + } + } + + currentFrame = 1; + gameData.setFrameCount(currentFrame); + game.onFrame(currentFrame); + allUnits = game.getAllUnits(); + + // Promote long-lived Game and Unit instances to Old Gen, matching a running match + System.gc(); + + builderWorker = game.self().getUnits().stream() + .filter(u -> u.getType().isWorker()) + .findFirst() + .orElse(friendlyCombatUnits.get(0)); + + // 4. Target positions for getDistance(Position) + targetPositions = new Position[20]; + for (int i = 0; i < targetPositions.length; i++) { + targetPositions[i] = new Position( + clampPixelX(startPixelX + rng.nextInt(-1200, 1200)), + clampPixelY(startPixelY + rng.nextInt(-1200, 1200)) + ); + } + + // 5. Search rectangles for getUnitsInRectangle (32 rectangles of ~10x10 tiles around active areas) + rectTopLefts = new Position[32]; + rectBottomRights = new Position[32]; + for (int i = 0; i < 32; i++) { + int cx = clampPixelX(startPixelX + rng.nextInt(-900, 900)); + int cy = clampPixelY(startPixelY + rng.nextInt(-900, 900)); + rectTopLefts[i] = new Position(Math.max(0, cx - 160), Math.max(0, cy - 160)); + rectBottomRights[i] = new Position(Math.min(game.mapPixelWidth() - 1, cx + 160), Math.min(game.mapPixelHeight() - 1, cy + 160)); + } + + // 6. Collectible buildable tiles near start location so canBuildHere passes the static map buildability check + // and actually executes the unit-in-rectangle and resource checks. + List buildableCandidates = new ArrayList<>(32); + for (int dy = -20; dy <= 20 && buildableCandidates.size() < 32; dy++) { + for (int dx = -20; dx <= 20 && buildableCandidates.size() < 32; dx++) { + int tx = selfStart.x + dx; + int ty = selfStart.y + dy; + if (areTilesBuildable(tx, ty, 4, 3)) { + buildableCandidates.add(new TilePosition(tx, ty)); + } + } + } + buildCandidateTiles = buildableCandidates.toArray(new TilePosition[0]); + } + + private boolean areTilesBuildable(int tx, int ty, int w, int h) { + for (int x = tx; x < tx + w; x++) { + for (int y = ty; y < ty + h; y++) { + if (!game.isBuildable(x, y, true)) { + return false; + } + } + } + return true; + } + + private int clampPixelX(int x) { + return Math.max(64, Math.min(game.mapPixelWidth() - 64, x)); + } + + private int clampPixelY(int y) { + return Math.max(64, Math.min(game.mapPixelHeight() - 64, y)); + } + + private void populateUnitData(int id, int playerId, UnitType type, int x, int y) { + ClientData.UnitData ud = gameData.getUnits(id); + ud.setId(id); + ud.setPlayer(playerId); + ud.setType(type.id); + ud.setPositionX(x); + ud.setPositionY(y); + ud.setHitPoints(type.maxHitPoints()); + ud.setExists(true); + ud.setIsCompleted(true); + ud.setIsLifted(false); + ud.setTransport(-1); + ud.setCarrier(-1); + ud.setHatchery(-1); + ud.setAddon(-1); + } + + /** + * Benchmarks 3,600 Unit.getDistance(Unit) calls (60 friendly x 60 enemy units), + * typical of target selection / threat evaluation in a combat frame. + */ + @Benchmark + public int unitGetDistanceToUnit() { + int sum = 0; + List friendly = this.friendlyCombatUnits; + List enemy = this.enemyCombatUnits; + for (int i = 0, fSize = friendly.size(); i < fSize; i++) { + Unit f = friendly.get(i); + for (int j = 0, eSize = enemy.size(); j < eSize; j++) { + sum += f.getDistance(enemy.get(j)); + } + } + return sum; + } + + /** + * Benchmarks 2,400 Unit.getDistance(Position) calls (120 mobile units x 20 positions). + */ + @Benchmark + public int unitGetDistanceToPosition() { + int sum = 0; + List units = this.mobileUnits; + Position[] positions = this.targetPositions; + for (int i = 0, uSize = units.size(); i < uSize; i++) { + Unit u = units.get(i); + for (int j = 0; j < positions.length; j++) { + sum += u.getDistance(positions[j]); + } + } + return sum; + } + + /** + * Benchmarks Unit.updatePosition(frame) across all ~360 units in the game, + * where the first ~240 units (resources + buildings) are stationary and the + * last 120 units (army/workers) change position every frame. + */ + @Benchmark + public int unitUpdatePositionPerFrame() { + int frame = ++currentFrame; + int offset = frame & 7; + ClientData.UnitData[] uds = this.mobileUnitData; + for (int i = 0; i < uds.length; i++) { + uds[i].setPositionX(baseMobileX[i] + offset); + uds[i].setPositionY(baseMobileY[i] + offset); + } + + int sumX = 0; + for (Unit u : allUnits) { + u.updatePosition(frame); + sumX += u.getPosition().x; + } + return sumX; + } + + /** + * Benchmarks 32 Game.getUnitsInRectangle queries over the populated map. + */ + @Benchmark + public void getUnitsInRectangle(Blackhole bh) { + Position[] tls = this.rectTopLefts; + Position[] brs = this.rectBottomRights; + for (int i = 0; i < tls.length; i++) { + List units = game.getUnitsInRectangle(tls[i], brs[i], u -> !u.isFlying() && !u.isLoaded()); + bh.consume(units); + } + } + + /** + * Benchmarks Game.canBuildHere across 32 buildable candidate tiles for a Barracks, Supply Depot, and Command Center. + */ + @Benchmark + public int canBuildHere() { + int buildableCount = 0; + TilePosition[] tiles = this.buildCandidateTiles; + Unit builder = this.builderWorker; + for (int i = 0; i < tiles.length; i++) { + TilePosition tile = tiles[i]; + if (game.canBuildHere(tile, UnitType.Terran_Barracks, builder, false)) { + buildableCount++; + } + if (game.canBuildHere(tile, UnitType.Terran_Supply_Depot, builder, false)) { + buildableCount++; + } + if (game.canBuildHere(tile, UnitType.Terran_Command_Center, builder, false)) { + buildableCount++; + } + } + return buildableCount; + } + + /** + * Separate state for worst-case late-game visibility churn: + * - 700 active visible units distributed across IDs 0..3000 (simulating a 25-minute macro game + * where ~2,300 units have died and new units have IDs > 127 outside Integer.valueOf cache, + * and peak unit count previously reached ~1,200). + * - 40 units at the fog-of-war boundary toggling unitHide / unitShow on a frame, followed by onFrame(frame). + */ + @State(Scope.Thread) + public static class LateGameVisibilityState { + Game game; + ClientData.GameData gameData; + int[] fogBoundaryIds; + int frame; + + @Setup(Level.Trial) + public void setup() throws IOException { + game = GameBuilder.createGame(MAP_NAME); + gameData = game.botClientData().gameData(); + + int selfId = game.self().getID(); + int enemyId = game.enemy().getID(); + + // Simulate peak of 1,200 units earlier in the game so the collection has experienced realistic growth + for (int id = 0; id < 1200; id++) { + ClientData.UnitData ud = gameData.getUnits(id); + ud.setId(id); + ud.setPlayer(id % 2 == 0 ? selfId : enemyId); + ud.setType(UnitType.Terran_Marine.id); + ud.setPositionX(1000); + ud.setPositionY(1000); + ud.setHitPoints(40); + ud.setExists(true); + ud.setIsCompleted(true); + game.unitShow(id); + } + for (int id = 0; id < 1200; id++) { + game.unitHide(id); + } + + // Now populate 700 currently visible units scattered across IDs 0..2800 (step 4) + fogBoundaryIds = new int[40]; + int count = 0; + for (int id = 0; count < 700; id += 4) { + ClientData.UnitData ud = gameData.getUnits(id); + ud.setId(id); + ud.setPlayer(count < 350 ? selfId : enemyId); + ud.setType(UnitType.Terran_Marine.id); + ud.setPositionX(1024 + (count & 63) * 8); + ud.setPositionY(1024 + (count >> 6) * 8); + ud.setHitPoints(40); + ud.setExists(true); + ud.setIsCompleted(true); + game.unitShow(id); + if (count >= 660) { + fogBoundaryIds[count - 660] = id; + } + count++; + } + + frame = 1; + gameData.setFrameCount(frame); + game.onFrame(frame); + System.gc(); + } + } + + /** + * Benchmarks worst-case late-game visibility churn + onFrame rebuild of allUnits + * (40 fog-of-war unitHide/unitShow events with IDs > 2600 + onFrame over 700 visible units). + */ + @Benchmark + public int visibleUnitsWorstCaseOnFrame(LateGameVisibilityState s) { + Game g = s.game; + int[] boundary = s.fogBoundaryIds; + for (int i = 0; i < boundary.length; i++) { + int id = boundary[i]; + g.unitHide(id); + g.unitShow(id); + } + int f = ++s.frame; + g.onFrame(f); + return g.getAllUnits().size(); + } +}