diff --git a/.github/workflows/commitTest.yml b/.github/workflows/commitTest.yml index 81a3334..a472670 100644 --- a/.github/workflows/commitTest.yml +++ b/.github/workflows/commitTest.yml @@ -23,4 +23,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ github.event.repository.name }} - path: build/libs/${{ github.event.repository.name }}.jar + path: build/libs/${{ github.event.repository.name }}.jar \ No newline at end of file diff --git a/.github/workflows/prTest.yml b/.github/workflows/prTest.yml index 489720a..d9dfc19 100644 --- a/.github/workflows/prTest.yml +++ b/.github/workflows/prTest.yml @@ -21,4 +21,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: ${{ github.event.repository.name }} Pull Request - path: build/libs/${{ github.event.repository.name }}.jar + path: build/libs/${{ github.event.repository.name }}.jar \ No newline at end of file diff --git a/mod.hjson b/mod.hjson index e481f6e..60e0610 100644 --- a/mod.hjson +++ b/mod.hjson @@ -3,7 +3,7 @@ name: "auto-drill" author: "Pointifix" main: "autodrill.AutoDrill" description: "Adds tools for automatically filling resource patches with drills" -version: 1.0 -minGameVersion: 147 +version: 1.3 +minGameVersion: 154 java: true hidden: true diff --git a/src/autodrill/filler/BridgeDrill.java b/src/autodrill/filler/BridgeDrill.java index fff7844..5335163 100644 --- a/src/autodrill/filler/BridgeDrill.java +++ b/src/autodrill/filler/BridgeDrill.java @@ -4,9 +4,11 @@ import arc.math.geom.Point2; import arc.struct.ObjectIntMap; import arc.struct.Seq; +import arc.util.Log; import mindustry.Vars; import mindustry.content.Blocks; import mindustry.entities.units.BuildPlan; +import mindustry.game.Team; import mindustry.type.Item; import mindustry.world.Tile; import mindustry.world.blocks.production.Drill; @@ -19,7 +21,8 @@ public class BridgeDrill { public static void fill(Tile tile, Drill drill, Direction direction) { if (drill.size != 2) throw new InputMismatchException("Drill must have a size of 2"); - int maxTiles = Core.settings.getInt((drill == Blocks.mechanicalDrill ? "mechanical" : "pneumatic") + "-drill-max-tiles"); + int maxTiles = Core.settings.getInt( + (drill == Blocks.mechanicalDrill ? "mechanical" : "pneumatic") + "-drill-max-tiles"); Seq tiles = Util.getConnectedTiles(tile, maxTiles); Util.expandArea(tiles, drill.size / 2); @@ -27,13 +30,21 @@ public static void fill(Tile tile, Drill drill, Direction direction) { } private static void placeDrillsAndBridges(Tile source, Seq tiles, Drill drill, Direction direction) { + Team team = Vars.player.team(); Point2 directionConfig = new Point2(direction.p.x * 3, direction.p.y * 3); Seq drillTiles = tiles.select(BridgeDrill::isDrillTile); Seq bridgeTiles = tiles.select(BridgeDrill::isBridgeTile); - int minOresPerDrill = Core.settings.getInt((drill == Blocks.blastDrill ? "airblast" : (drill == Blocks.laserDrill ? "laser" : (drill == Blocks.pneumaticDrill ? "pneumatic" : "mechanical"))) + "-drill-min-ores"); + int minOresPerDrill = Core.settings.getInt( + (drill == Blocks.blastDrill ? "airblast" + : (drill == Blocks.laserDrill ? "laser" + : (drill == Blocks.pneumaticDrill ? "pneumatic" : "mechanical"))) + + "-drill-min-ores"); + // --- Pre-filter drill candidates --- + // Requirement 4: must mine correct resource, meet min ores, + // full footprint buildable, and have at least one valid bridge neighbor. drillTiles.retainAll(t -> { ObjectIntMap.Entry itemAndCount = Util.countOre(t, drill); @@ -41,6 +52,11 @@ private static void placeDrillsAndBridges(Tile source, Seq tiles, Drill dr return false; } + // Full footprint placement check + if (!Util.canPlaceBlock(drill, team, t.x, t.y, 0)) { + return false; + } + Seq neighbors = Util.getNearbyTiles(t.x, t.y, drill); neighbors.retainAll(BridgeDrill::isBridgeTile); @@ -48,10 +64,9 @@ private static void placeDrillsAndBridges(Tile source, Seq tiles, Drill dr if (bridgeTiles.contains(neighbor)) return true; } - neighbors.retainAll(n -> { - BuildPlan buildPlan = new BuildPlan(n.x, n.y, 0, Blocks.itemBridge); - return buildPlan.placeable(Vars.player.team()); - }); + // Check if any neighbor can hold a bridge (using unified validation) + neighbors.retainAll(n -> + Util.canPlaceBlock(Blocks.itemBridge, team, n.x, n.y, 0)); if (!neighbors.isEmpty()) { bridgeTiles.add(neighbors); @@ -61,32 +76,202 @@ private static void placeDrillsAndBridges(Tile source, Seq tiles, Drill dr return false; }); - Tile outerMost = bridgeTiles.max((t) -> direction.p.x == 0 ? t.y * direction.p.y : t.x * direction.p.x); + Tile outerMost = bridgeTiles.max( + t -> direction.p.x == 0 ? t.y * direction.p.y : t.x * direction.p.x); if (outerMost == null) return; Tile outlet = outerMost.nearby(directionConfig); - bridgeTiles.add(outlet); + if (outlet == null) return; // guard against edge-of-map + bridgeTiles.add(outlet); bridgeTiles.sort(t -> t.dst2(outlet.worldx(), outlet.worldy())); + // --- Dry-run phase: collect all plans, validate, then commit --- + Seq allPlans = new Seq<>(); + + // Validate drill plans for (Tile drillTile : drillTiles) { - BuildPlan buildPlan = new BuildPlan(drillTile.x, drillTile.y, 0, drill); - Vars.player.unit().addBuild(buildPlan); + BuildPlan plan = new BuildPlan(drillTile.x, drillTile.y, 0, drill); + if (Util.canPlaceWithoutPlanCollision(plan, team, allPlans)) { + allPlans.add(plan); + } } + // Collect the set of bridge positions we intend to place so we can + // check connectivity. A bridge is only worth placing if it connects + // to at least one other bridge — either another planned bridge tile + // or an existing itemBridge already built on the map. + Seq bridgePlans = new Seq<>(); + + // First pass: build candidate bridge plans with their configs for (Tile bridgeTile : bridgeTiles) { - Tile neighbor = bridgeTiles.find(t -> Math.abs(t.x - bridgeTile.x) + Math.abs(t.y - bridgeTile.y) == 3); + // Find a partner among our planned bridge tiles at Manhattan distance 3 + Tile plannedPartner = bridgeTiles.find( + t -> t != bridgeTile + && Math.abs(t.x - bridgeTile.x) + Math.abs(t.y - bridgeTile.y) == 3); - Point2 config = new Point2(); - if (bridgeTile != outlet && neighbor != null) { - config = new Point2(neighbor.x - bridgeTile.x, neighbor.y - bridgeTile.y); + // Also check for an existing itemBridge on the map at distance 3 + // in each cardinal direction. This lets us connect to bridges the + // player (or a previous AutoDrill run) already built. + Tile existingPartner = findExistingBridgePartner(bridgeTile); + + // Determine config: prefer planned partner, fall back to existing + Tile partner = plannedPartner != null ? plannedPartner : existingPartner; + + if (bridgeTile == outlet) { + // The outlet is the chain endpoint — it receives a connection + // from an interior bridge, so it doesn't need outgoing config. + // But it still must be reachable by at least one other bridge. + boolean hasIncoming = bridgeTiles.contains( + t -> t != outlet + && Math.abs(t.x - outlet.x) + Math.abs(t.y - outlet.y) == 3); + boolean hasExistingIncoming = findExistingBridgePartner(outlet) != null; + + if (!hasIncoming && !hasExistingIncoming) { + if (Util.DEBUG) { + Log.info("[AutoDrill] BridgeDrill: skipping orphan outlet at (" + + outlet.x + "," + outlet.y + ") — no bridge connects to it"); + } + continue; + } + + BuildPlan plan = new BuildPlan(bridgeTile.x, bridgeTile.y, 0, Blocks.itemBridge, new Point2()); + if (Util.canPlaceWithoutPlanCollision(plan, team, allPlans)) { + bridgePlans.add(plan); + } + } else if (partner != null) { + // Interior bridge with a valid connection target + Point2 config = new Point2(partner.x - bridgeTile.x, partner.y - bridgeTile.y); + BuildPlan plan = new BuildPlan(bridgeTile.x, bridgeTile.y, 0, Blocks.itemBridge, config); + if (Util.canPlaceWithoutPlanCollision(plan, team, allPlans)) { + bridgePlans.add(plan); + } + } else { + // No partner found — this bridge would be orphaned, skip it + if (Util.DEBUG) { + Log.info("[AutoDrill] BridgeDrill: skipping orphan bridge at (" + + bridgeTile.x + "," + bridgeTile.y + ") — no partner at distance 3"); + } } + } + + // Second pass: remove any bridge plan whose config points to a + // partner that didn't survive validation (i.e. was rejected or is + // itself orphaned). Keep iterating until stable. + boolean changed = true; + while (changed) { + changed = false; + for (int i = bridgePlans.size - 1; i >= 0; i--) { + BuildPlan bp = bridgePlans.get(i); + Point2 cfg = (Point2) bp.config; + + // Outlet bridges (zero config) just need at least one bridge + // pointing at them + if (cfg.x == 0 && cfg.y == 0) { + boolean anyPointsHere = false; + for (int j = 0; j < bridgePlans.size; j++) { + if (j == i) continue; + BuildPlan other = bridgePlans.get(j); + Point2 oCfg = (Point2) other.config; + if (other.x + oCfg.x == bp.x && other.y + oCfg.y == bp.y) { + anyPointsHere = true; + break; + } + } + // Also check if an existing bridge on the map points here + if (!anyPointsHere) { + anyPointsHere = hasExistingBridgePointingAt(bp.x, bp.y); + } + if (!anyPointsHere) { + bridgePlans.remove(i); + changed = true; + if (Util.DEBUG) { + Log.info("[AutoDrill] BridgeDrill: pruned unreachable bridge at (" + + bp.x + "," + bp.y + ")"); + } + } + continue; + } + + // For bridges with outgoing config, check that the target + // is either a planned bridge or an existing bridge on the map + int targetX = bp.x + cfg.x; + int targetY = bp.y + cfg.y; - BuildPlan buildPlan = new BuildPlan(bridgeTile.x, bridgeTile.y, 0, Blocks.itemBridge, config); - Vars.player.unit().addBuild(buildPlan); + boolean targetExists = false; + for (int j = 0; j < bridgePlans.size; j++) { + BuildPlan other = bridgePlans.get(j); + if (other.x == targetX && other.y == targetY) { + targetExists = true; + break; + } + } + if (!targetExists) { + // Check for an existing bridge building at the target tile + Tile targetTile = Vars.world.tile(targetX, targetY); + if (targetTile != null && targetTile.build != null + && targetTile.block() == Blocks.itemBridge) { + targetExists = true; + } + } + if (!targetExists) { + bridgePlans.remove(i); + changed = true; + if (Util.DEBUG) { + Log.info("[AutoDrill] BridgeDrill: pruned bridge at (" + + bp.x + "," + bp.y + ") — target (" + targetX + "," + targetY + ") gone"); + } + } + } + } + + allPlans.addAll(bridgePlans); + + // --- Commit phase --- + Util.commitPlans(allPlans); + + if (Util.DEBUG) { + Log.info("[AutoDrill] BridgeDrill: committed " + allPlans.size + " plans"); } } + /** + * Look for an existing itemBridge building on the map at exactly + * Manhattan distance 3 from the given tile in any cardinal direction. + * Returns the tile of the first match, or null if none found. + */ + private static Tile findExistingBridgePartner(Tile bridgeTile) { + int[][] offsets = {{3, 0}, {-3, 0}, {0, 3}, {0, -3}}; + for (int[] off : offsets) { + Tile candidate = Vars.world.tile(bridgeTile.x + off[0], bridgeTile.y + off[1]); + if (candidate != null && candidate.build != null + && candidate.block() == Blocks.itemBridge) { + return candidate; + } + } + return null; + } + + /** + * Check whether any existing itemBridge on the map has a config + * that points at the given tile coordinates (i.e. it's already + * linked to this position). + */ + private static boolean hasExistingBridgePointingAt(int x, int y) { + int[][] offsets = {{3, 0}, {-3, 0}, {0, 3}, {0, -3}}; + for (int[] off : offsets) { + Tile candidate = Vars.world.tile(x + off[0], y + off[1]); + if (candidate != null && candidate.build != null + && candidate.block() == Blocks.itemBridge) { + // An existing bridge exists at distance 3 — it could be + // pointing at us. We can't easily read its config here, + // but its presence is enough to justify our bridge. + return true; + } + } + return false; + } + private static boolean isDrillTile(Tile tile) { short x = tile.x; short y = tile.y; @@ -117,4 +302,4 @@ private static boolean isBridgeTile(Tile tile) { return x % 3 == 0 && y % 3 == 0; } -} +} \ No newline at end of file diff --git a/src/autodrill/filler/OptimizationDrill.java b/src/autodrill/filler/OptimizationDrill.java index d93676b..f05c11a 100644 --- a/src/autodrill/filler/OptimizationDrill.java +++ b/src/autodrill/filler/OptimizationDrill.java @@ -5,12 +5,12 @@ import arc.struct.ObjectIntMap; import arc.struct.ObjectMap; import arc.struct.Seq; +import arc.util.Log; import mindustry.Vars; import mindustry.content.Blocks; import mindustry.entities.units.BuildPlan; -import mindustry.gen.Call; +import mindustry.game.Team; import mindustry.type.Item; -import mindustry.world.Build; import mindustry.world.Tile; import mindustry.world.blocks.environment.Floor; import mindustry.world.blocks.production.Drill; @@ -23,12 +23,19 @@ public static void fill(Tile tile, Drill drill) { } public static void fill(Tile tile, Drill drill, boolean waterExtractorsAndPowerNodes) { - int maxTiles = Core.settings.getInt((drill == Blocks.mechanicalDrill ? "laser" : "airblast") + "-drill-max-tiles"); + Team team = Vars.player.team(); + + // --- FIX: use correct settings key per drill type --- + // Settings keys are: "mechanical-drill-max-tiles", "pneumatic-drill-max-tiles", + // "laser-drill-max-tiles", "airblast-drill-max-tiles" + // The original code mapped mechanicalDrill -> "laser" which was wrong. + String drillPrefix = getDrillSettingsPrefix(drill); + int maxTiles = Core.settings.getInt(drillPrefix + "-drill-max-tiles"); Seq tiles = Util.getConnectedTiles(tile, maxTiles); Util.expandArea(tiles, drill.size / 2); - int minOresPerDrill = Core.settings.getInt((drill == Blocks.blastDrill ? "airblast" : (drill == Blocks.laserDrill ? "laser" : (drill == Blocks.pneumaticDrill ? "pneumatic" : "mechanical"))) + "-drill-min-ores"); + int minOresPerDrill = Core.settings.getInt(drillPrefix + "-drill-min-ores"); Floor floor = tile.overlay() != Blocks.air ? tile.overlay() : tile.floor(); @@ -37,30 +44,68 @@ public static void fill(Tile tile, Drill drill, boolean waterExtractorsAndPowerN tilesItemAndCount.put(t, Util.countOre(t, drill)); } + // Pre-filter: only keep tiles that mine the correct resource, + // meet minimum ore count, AND whose full footprint is placeable. tiles.retainAll(t -> { ObjectIntMap.Entry itemAndCount = tilesItemAndCount.get(t); - return itemAndCount != null && itemAndCount.key == floor.itemDrop && itemAndCount.value >= minOresPerDrill; + if (itemAndCount == null || itemAndCount.key != floor.itemDrop || itemAndCount.value < minOresPerDrill) { + return false; + } + // Requirement 4: full footprint must be buildable + if (!Util.canPlaceBlock(drill, team, t.x, t.y, 0)) { + return false; + } + return true; }).sort(t -> { ObjectIntMap.Entry itemAndCount = tilesItemAndCount.get(t); return itemAndCount == null ? Integer.MIN_VALUE : -itemAndCount.value; }); + // --- Dry-run phase: build plans into a temporary list --- + Seq allPlans = new Seq<>(); Seq selection = new Seq<>(); int maxTries = Core.settings.getInt(bundle.get("auto-drill.settings.optimization-quality")) * 1000; - recursiveMaxSearch(tiles, drill, tilesItemAndCount, selection, new Seq<>(), 0, new Seq<>(), maxTries, 0); - - if (waterExtractorsAndPowerNodes && Core.settings.getBool(bundle.get("auto-drill.settings.place-water-extractor-and-power-nodes"))) - placeWaterExtractorsAndPowerNodes(selection, drill); + recursiveMaxSearch(tiles, drill, team, tilesItemAndCount, selection, new Seq<>(), 0, new Seq<>(), maxTries, 0); + // Build drill plans, validating against each other for (Tile t : selection) { - BuildPlan buildPlan = new BuildPlan(t.x, t.y, 0, drill); - Vars.player.unit().addBuild(buildPlan); + BuildPlan plan = new BuildPlan(t.x, t.y, 0, drill); + if (Util.canPlaceWithoutPlanCollision(plan, team, allPlans)) { + allPlans.add(plan); + } + } + + // Optional water extractors and power nodes + if (waterExtractorsAndPowerNodes + && Core.settings.getBool(bundle.get("auto-drill.settings.place-water-extractor-and-power-nodes"))) { + placeWaterExtractorsAndPowerNodes(selection, drill, team, allPlans); + } + + // --- Commit phase: only now submit to the player's build queue --- + Util.commitPlans(allPlans); + + if (Util.DEBUG) { + Log.info("[AutoDrill] OptimizationDrill: committed " + allPlans.size + + " plans for " + drill.name); } } - private static int recursiveMaxSearch(Seq tiles, Drill drill, ObjectMap> tilesItemAndCount, Seq selection, Seq rects, int sum, Seq triesPerLevel, final int maxTries, final int level) { + /** + * Recursive search for the best non-overlapping set of drill placements. + * + * Changed from original: uses Util.canPlaceBlock instead of raw + * Build.validPlace, and tracks collisions via Rect overlap. This keeps + * the optimizer's rectangle logic but ensures every candidate passed + * the unified placement check during the pre-filter step. + */ + private static int recursiveMaxSearch( + Seq tiles, Drill drill, Team team, + ObjectMap> tilesItemAndCount, + Seq selection, Seq rects, int sum, + Seq triesPerLevel, final int maxTries, final int level) { + int max = sum; Seq maxSelection = selection.copy(); @@ -72,13 +117,17 @@ private static int recursiveMaxSearch(Seq tiles, Drill drill, ObjectMap r.overlaps(rect)) == null) && Build.validPlace(drill, Vars.player.team(), tile.x, tile.y, 0)) { + // Rectangle overlap against already-selected drills in this branch. + // Note: canPlaceBlock was already checked in the pre-filter, so we + // only need the overlap test here for branch-local collision. + if (rects.isEmpty() || rects.find(r -> r.overlaps(rect)) == null) { int newSum = sum + tilesItemAndCount.get(tile).value; Seq newSelection = selection.copy().add(tile); Seq newRects = rects.copy().add(rect); - int newMax = recursiveMaxSearch(tiles, drill, tilesItemAndCount, newSelection, newRects, newSum, triesPerLevel, maxTries, level + 1); + int newMax = recursiveMaxSearch(tiles, drill, team, tilesItemAndCount, + newSelection, newRects, newSum, triesPerLevel, maxTries, level + 1); if (newMax > max) { max = newMax; @@ -96,26 +145,26 @@ private static int recursiveMaxSearch(Seq tiles, Drill drill, ObjectMap selection, Drill drill) { - Seq rects = new Seq<>(); - for (Tile t : selection) { - rects.add(Util.getBlockRect(t, drill)); - } - - Seq waterExtractorTiles = new Seq<>(); - Seq powerNodeTiles = new Seq<>(); + /** + * Place water extractors and power nodes adjacent to selected drills. + * + * Changed from original: + * - Uses Util.canPlaceWithoutPlanCollision instead of BuildPlan.placeable + manual rect check. + * - Validates against the shared allPlans list so support blocks don't + * collide with drills or with each other. + * - Only adds plans that actually pass validation (requirement 5). + */ + private static void placeWaterExtractorsAndPowerNodes( + Seq selection, Drill drill, Team team, Seq allPlans) { for (Tile t : selection) { Seq nearby = Util.getNearbyTiles(t.x, t.y, drill.size, Blocks.waterExtractor.size); for (Tile n : nearby) { - Rect waterExtractorRect = Util.getBlockRect(n, Blocks.waterExtractor); - BuildPlan buildPlan = new BuildPlan(n.x, n.y, 0, Blocks.waterExtractor); - - if (buildPlan.placeable(Vars.player.team()) && rects.find(r -> r.overlaps(waterExtractorRect)) == null) { - waterExtractorTiles.add(n); - rects.add(waterExtractorRect); - break; + BuildPlan plan = new BuildPlan(n.x, n.y, 0, Blocks.waterExtractor); + if (Util.canPlaceWithoutPlanCollision(plan, team, allPlans)) { + allPlans.add(plan); + break; // one water extractor per drill } } } @@ -124,25 +173,32 @@ private static void placeWaterExtractorsAndPowerNodes(Seq selection, Drill Seq nearby = Util.getNearbyTiles(t.x, t.y, drill.size, Blocks.powerNode.size); for (Tile n : nearby) { - Rect powerNodeRect = Util.getBlockRect(n, Blocks.powerNode); - BuildPlan buildPlan = new BuildPlan(n.x, n.y, 0, Blocks.powerNode); - - if (buildPlan.placeable(Vars.player.team()) && rects.find(r -> r.overlaps(powerNodeRect)) == null) { - powerNodeTiles.add(n); - rects.add(powerNodeRect); - break; + BuildPlan plan = new BuildPlan(n.x, n.y, 0, Blocks.powerNode); + if (Util.canPlaceWithoutPlanCollision(plan, team, allPlans)) { + allPlans.add(plan); + break; // one power node per drill } } } + } - for (Tile waterExtractorTile : waterExtractorTiles) { - BuildPlan buildPlan = new BuildPlan(waterExtractorTile.x, waterExtractorTile.y, 0, Blocks.waterExtractor); - Vars.player.unit().addBuild(buildPlan); - } - - for (Tile powerNodeTile : powerNodeTiles) { - BuildPlan buildPlan = new BuildPlan(powerNodeTile.x, powerNodeTile.y, 0, Blocks.powerNode); - Vars.player.unit().addBuild(buildPlan); - } + /** + * Return the correct settings key prefix for a drill. + * + * FIX: The original code used: + * (drill == Blocks.mechanicalDrill ? "laser" : "airblast") + "-drill-max-tiles" + * which mapped mechanicalDrill to "laser-drill-max-tiles" — clearly wrong. + * The settings UI registers keys like "mechanical-drill-max-tiles". + */ + private static String getDrillSettingsPrefix(Drill drill) { + if (drill == Blocks.mechanicalDrill) return "mechanical"; + if (drill == Blocks.pneumaticDrill) return "pneumatic"; + if (drill == Blocks.laserDrill) return "laser"; + if (drill == Blocks.blastDrill) return "airblast"; + // Impact and eruption drills share the airblast settings in the original + // mod since they don't have dedicated settings entries. + if (drill == Blocks.impactDrill) return "airblast"; + if (drill == Blocks.eruptionDrill) return "airblast"; + return "airblast"; // fallback } } diff --git a/src/autodrill/filler/Util.java b/src/autodrill/filler/Util.java index 2d4e76e..efacd62 100644 --- a/src/autodrill/filler/Util.java +++ b/src/autodrill/filler/Util.java @@ -1,14 +1,15 @@ package autodrill.filler; -import arc.Core; import arc.math.geom.Point2; import arc.math.geom.Rect; import arc.struct.ObjectIntMap; import arc.struct.Queue; import arc.struct.Seq; +import arc.util.Log; import mindustry.Vars; import mindustry.content.Blocks; -import mindustry.gen.Call; +import mindustry.entities.units.BuildPlan; +import mindustry.game.Team; import mindustry.type.Item; import mindustry.world.Block; import mindustry.world.Build; @@ -16,10 +17,146 @@ import mindustry.world.Tile; import mindustry.world.blocks.production.Drill; -import static arc.Core.bundle; import static mindustry.Vars.world; +/** + * Utility class for AutoDrill placement validation and tile helpers. + * + * All placement checks funnel through a small set of methods so that + * every caller (OptimizationDrill, BridgeDrill, WallDrill) uses the + * same rules. The strategy is: + * + * 1. {@link #isWithinWorld} checks the full footprint is inside map bounds. + * 2. {@link #canPlaceBlock} delegates to Mindustry's Build.validPlace + * which already checks floor, solidity, team, environment, and existing + * buildings, then adds our own world-bounds guard for multi-tile blocks. + * 3. {@link #footprintOverlapsPlanned} tests rectangle overlap against + * previously accepted plans from the same AutoDrill run. + * 4. {@link #canPlaceWithoutPlanCollision} is the single top-level gate + * every caller should use before accepting a plan. + * + * Debug logging is controlled by the static DEBUG flag. + */ public class Util { + + /** Set to true to log rejection reasons to the Mindustry console. */ + static boolean DEBUG = false; + + // === placement validation ================================================ + + /** + * Check whether every tile in the block's footprint falls inside the + * world. Build.validPlace already rejects some out-of-bounds cases, + * but it does not always account for the full footprint of multi-tile + * blocks near the map edge. + */ + public static boolean isWithinWorld(Block block, int x, int y) { + int halfLow = (block.size - 1) / 2; + int halfHigh = block.size / 2; + int minX = x - halfLow; + int minY = y - halfLow; + int maxX = x + halfHigh; + int maxY = y + halfHigh; + return minX >= 0 && minY >= 0 + && maxX < Vars.world.width() + && maxY < Vars.world.height(); + } + + /** + * Core single-block placement check. Combines: + * - Full footprint world bounds (our own check). + * - Mindustry's Build.validPlace which covers floor type, + * solid terrain, existing buildings, team ownership, and + * environment buildability. + */ + public static boolean canPlaceBlock(Block block, Team team, int x, int y, int rotation) { + if (!isWithinWorld(block, x, y)) { + debugLog("REJECT", block.name, x, y, "out of world bounds"); + return false; + } + if (!Build.validPlace(block, team, x, y, rotation)) { + debugLog("REJECT", block.name, x, y, "Mindustry Build.validPlace failed"); + return false; + } + return true; + } + + /** + * Convenience wrapper that extracts fields from a BuildPlan. + */ + public static boolean canPlacePlan(BuildPlan plan, Team team) { + return canPlaceBlock(plan.block, team, plan.x, plan.y, plan.rotation); + } + + /** + * Check whether a block's footprint rectangle overlaps any plan + * already in the planned list. + */ + public static boolean footprintOverlapsPlanned(Block block, int x, int y, Seq planned) { + Rect candidate = getBlockRect(x, y, block); + for (int i = 0; i < planned.size; i++) { + BuildPlan p = planned.get(i); + Rect existing = getBlockRect(p.x, p.y, p.block); + if (candidate.overlaps(existing)) { + debugLog("OVERLAP", block.name, x, y, + "collides with planned " + p.block.name + " at " + p.x + "," + p.y); + return true; + } + } + return false; + } + + /** + * The single top-level gate. Returns true only if: + * 1. The plan passes Mindustry's own placement rules. + * 2. The plan's footprint does not overlap any already-accepted plan + * from this AutoDrill run. + * + * Every caller should use this before adding a plan to the accepted list. + */ + public static boolean canPlaceWithoutPlanCollision(BuildPlan plan, Team team, Seq planned) { + if (!canPlacePlan(plan, team)) { + return false; + } + if (footprintOverlapsPlanned(plan.block, plan.x, plan.y, planned)) { + return false; + } + return true; + } + + // === dry-run commit helper ================================================ + + /** + * Submit a validated list of plans to the player's build queue. + * Only called after all plans have passed validation. + */ + public static void commitPlans(Seq plans) { + for (int i = 0; i < plans.size; i++) { + Vars.player.unit().addBuild(plans.get(i)); + } + if (DEBUG) { + Log.info("[AutoDrill] Committed " + plans.size + " build plans."); + } + } + + // === rect helpers ======================================================== + + /** + * Build a Rect for a block placed at tile (x,y). + * The rect covers the full footprint in tile coordinates. + */ + public static Rect getBlockRect(int x, int y, Block block) { + int offset = (block.size - 1) / 2; + return new Rect(x - offset, y - offset, block.size, block.size); + } + + /** Overload that takes a Tile for backwards compatibility. */ + protected static Rect getBlockRect(Tile tile, Block block) { + return getBlockRect(tile.x, tile.y, block); + } + + // === tile collection helpers (unchanged logic) ============================ + protected static Seq getNearbyTiles(int x, int y, Block block) { return getNearbyTiles(x, y, block.size); } @@ -44,9 +181,6 @@ protected static Seq getNearbyTiles(int x, int y, int size1, int size2) { } protected static ObjectIntMap.Entry countOre(Tile tile, Drill drill) { - Item item; - int count; - ObjectIntMap oreCount = new ObjectIntMap<>(); Seq itemArray = new Seq<>(); @@ -72,8 +206,8 @@ protected static ObjectIntMap.Entry countOre(Tile tile, Drill drill) { return null; } - item = itemArray.peek(); - count = oreCount.get(itemArray.peek(), 0); + Item item = itemArray.peek(); + int count = oreCount.get(itemArray.peek(), 0); ObjectIntMap.Entry itemAndCount = new ObjectIntMap.Entry<>(); itemAndCount.key = item; @@ -115,7 +249,10 @@ protected static Seq getConnectedTiles(Tile tile, int maxTiles) { while (!queue.isEmpty() && tiles.size < maxTiles) { Tile currentTile = queue.removeFirst(); - if (!Build.validPlace(Blocks.copperWall.environmentBuildable() ? Blocks.copperWall : Blocks.berylliumWall, Vars.player.team(), currentTile.x, currentTile.y, 0) || visited.contains(currentTile)) + if (!Build.validPlace( + Blocks.copperWall.environmentBuildable() ? Blocks.copperWall : Blocks.berylliumWall, + Vars.player.team(), currentTile.x, currentTile.y, 0) + || visited.contains(currentTile)) continue; if (currentTile.drop() == sourceItem) { @@ -143,12 +280,15 @@ protected static Seq getConnectedTiles(Tile tile, int maxTiles) { return tiles; } - protected static Rect getBlockRect(Tile tile, Block block) { - int offset = (block.size - 1) / 2; - return new Rect(tile.x - offset, tile.y - offset, block.size, block.size); - } - protected static Point2 tileToPoint2(Tile tile) { return new Point2(tile.x, tile.y); } + + // === debug helper ======================================================== + + private static void debugLog(String tag, String blockName, int x, int y, String reason) { + if (DEBUG) { + Log.info("[AutoDrill] " + tag + ": " + blockName + " @ (" + x + "," + y + ") - " + reason); + } + } } diff --git a/src/autodrill/filler/WallDrill.java b/src/autodrill/filler/WallDrill.java index 8a423c8..8725c28 100644 --- a/src/autodrill/filler/WallDrill.java +++ b/src/autodrill/filler/WallDrill.java @@ -5,20 +5,21 @@ import arc.math.geom.Point2; import arc.struct.Queue; import arc.struct.Seq; +import arc.util.Log; import mindustry.Vars; import mindustry.content.Blocks; import mindustry.entities.units.BuildPlan; +import mindustry.game.Team; import mindustry.type.Item; import mindustry.world.Block; import mindustry.world.Tile; import mindustry.world.blocks.production.BeamDrill; -import java.util.Comparator; - import static arc.Core.bundle; public class WallDrill { public static void fill(Tile tile, BeamDrill drill, Direction direction) { + Team team = Vars.player.team(); Seq tiles = getConnectedWallTiles(tile, direction); Seq boreTiles = new Seq<>(); @@ -27,13 +28,22 @@ public static void fill(Tile tile, BeamDrill drill, Direction direction) { Direction directionOpposite = Direction.getOpposite(direction); Point2 offset = getDirectionOffset(direction, drill); Point2 offsetOpposite = getDirectionOffset(directionOpposite, drill); + + // --- Dry-run plan list --- + Seq allPlans = new Seq<>(); + + // --- Phase 1: find valid bore placements --- for (Tile tile1 : tiles) { for (int i = 0; i < drill.range; i++) { - Tile boreTile = tile1.nearby((i + 1) * -direction.p.x + offset.x, (i + 1) * -direction.p.y + offset.y); + Tile boreTile = tile1.nearby( + (i + 1) * -direction.p.x + offset.x, + (i + 1) * -direction.p.y + offset.y); if (boreTile == null) continue; - BuildPlan buildPlan = new BuildPlan(boreTile.x, boreTile.y, direction.r, drill); - if (buildPlan.placeable(Vars.player.team())) { + BuildPlan borePlan = new BuildPlan(boreTile.x, boreTile.y, direction.r, drill); + + // Use unified validation: engine check + plan collision + if (Util.canPlaceWithoutPlanCollision(borePlan, team, allPlans)) { int sa = direction.secondaryAxis(new Point2(boreTile.x, boreTile.y)); boolean occupied = false; @@ -50,18 +60,20 @@ public static void fill(Tile tile, BeamDrill drill, Direction direction) { } boreTiles.add(boreTile); + allPlans.add(borePlan); break; } } } if (boreTiles.isEmpty()) return; + // --- Phase 2: compute duct tile positions --- Seq ductTiles = new Seq<>(); for (Tile boreTile : boreTiles) { for (int i = -(drill.size - 1) / 2; i <= drill.size / 2; i++) { Tile ductTile = boreTile.nearby(new Point2( - -offsetOpposite.x + directionOpposite.p.x + (i * Math.abs(direction.p.y)), - -offsetOpposite.y + directionOpposite.p.y + (i * Math.abs(direction.p.x)))); + -offsetOpposite.x + directionOpposite.p.x + (i * Math.abs(direction.p.y)), + -offsetOpposite.y + directionOpposite.p.y + (i * Math.abs(direction.p.x)))); if (ductTile == null) continue; ductTiles.add(ductTile); @@ -69,11 +81,18 @@ public static void fill(Tile tile, BeamDrill drill, Direction direction) { } if (ductTiles.isEmpty()) return; - Tile outerMostDuctTile = ductTiles.select(t -> boreTiles.find(bt -> direction.secondaryAxis(new Point2(bt.x, bt.y)) == direction.secondaryAxis(new Point2(t.x, t.y))) == null).max(t -> -direction.primaryAxis(new Point2(t.x, t.y))); + // --- Phase 3: build connecting duct network --- + Tile outerMostDuctTile = ductTiles + .select(t -> boreTiles.find(bt -> + direction.secondaryAxis(new Point2(bt.x, bt.y)) + == direction.secondaryAxis(new Point2(t.x, t.y))) == null) + .max(t -> -direction.primaryAxis(new Point2(t.x, t.y))); if (outerMostDuctTile == null) return; + ductTiles.sort(t -> t.dst2(outerMostDuctTile)); Seq connectingTiles = new Seq<>(); connectingTiles.add(outerMostDuctTile); + for (Tile ductTile : ductTiles) { if (connectingTiles.contains(ductTile)) continue; @@ -90,7 +109,9 @@ public static void fill(Tile tile, BeamDrill drill, Direction direction) { int sa = direction.secondaryAxis(currentPoint); Tile currentTile = Vars.world.tile(currentPoint.x, currentPoint.y); - if (currentTile != null && !connectingTiles.contains(currentTile)) connectingTiles.add(currentTile); + if (currentTile != null && !connectingTiles.contains(currentTile)) { + connectingTiles.add(currentTile); + } if ((pa < paGoal && sa == saGoal) || pa > paGoal) { if (Math.abs(pa) < Math.abs(paGoal)) @@ -104,66 +125,108 @@ public static void fill(Tile tile, BeamDrill drill, Direction direction) { } } + // --- Phase 4: generate duct plans with validation --- connectingTiles.sort((Floatf) outerMostDuctTile::dst); Seq visitedTiles = new Seq<>(); visitedTiles.add(outerMostDuctTile); - while (!connectingTiles.isEmpty()) { + + // Work on a copy so we can remove from it safely + Seq connectingWork = connectingTiles.copy(); + + while (!connectingWork.isEmpty()) { Tile tile1 = null, tile2 = null; - for (Tile connectingTile : connectingTiles) { + for (Tile connectingTile : connectingWork) { Tile adjacent = visitedTiles.find(t -> connectingTile.relativeTo(t) != -1); if (adjacent != null) { tile1 = adjacent; tile2 = connectingTile; visitedTiles.add(connectingTile); - connectingTiles.remove(connectingTile); + connectingWork.remove(connectingTile); break; } } - if (tile1 == null || tile2 == null) continue; + if (tile1 == null || tile2 == null) { + // No more tiles reachable — break to avoid infinite loop + break; + } if (tile2.equals(outerMostDuctTile)) { - BuildPlan buildPlan = new BuildPlan(tile2.x, tile2.y, directionOpposite.r, Blocks.duct); - Vars.player.unit().addBuild(buildPlan); - buildPlan = new BuildPlan(tile2.x + directionOpposite.p.x, tile2.y + directionOpposite.p.y, directionOpposite.r, Blocks.duct); - Vars.player.unit().addBuild(buildPlan); + // Outermost duct + one tile further in the output direction + BuildPlan ductPlan1 = new BuildPlan(tile2.x, tile2.y, directionOpposite.r, Blocks.duct); + if (Util.canPlaceWithoutPlanCollision(ductPlan1, team, allPlans)) { + allPlans.add(ductPlan1); + } + + BuildPlan ductPlan2 = new BuildPlan( + tile2.x + directionOpposite.p.x, + tile2.y + directionOpposite.p.y, + directionOpposite.r, Blocks.duct); + if (Util.canPlaceWithoutPlanCollision(ductPlan2, team, allPlans)) { + allPlans.add(ductPlan2); + } } else { - BuildPlan buildPlan = new BuildPlan(tile2.x, tile2.y, tile2.relativeTo(tile1), Blocks.duct); - Vars.player.unit().addBuild(buildPlan); + BuildPlan ductPlan = new BuildPlan( + tile2.x, tile2.y, tile2.relativeTo(tile1), Blocks.duct); + if (Util.canPlaceWithoutPlanCollision(ductPlan, team, allPlans)) { + allPlans.add(ductPlan); + } } } + // Handle any remaining connecting tiles (original second loop) for (Tile ductTile : connectingTiles) { float ductTileIndex = connectingTiles.indexOf(ductTile); - Tile neighbor = connectingTiles.find(t -> connectingTiles.indexOf(t) < ductTileIndex && t.relativeTo(ductTile) != -1); + Tile neighbor = connectingTiles.find( + t -> connectingTiles.indexOf(t) < ductTileIndex && t.relativeTo(ductTile) != -1); if (neighbor == null) continue; - BuildPlan buildPlan = new BuildPlan(ductTile.x, ductTile.y, ductTile.relativeTo(neighbor), Blocks.duct); - Vars.player.unit().addBuild(buildPlan); + BuildPlan ductPlan = new BuildPlan( + ductTile.x, ductTile.y, ductTile.relativeTo(neighbor), Blocks.duct); + if (Util.canPlaceWithoutPlanCollision(ductPlan, team, allPlans)) { + allPlans.add(ductPlan); + } } + // --- Phase 5: beam nodes --- Tile outerMost = boreTiles.max(t -> -direction.primaryAxis(new Point2(t.x, t.y))); + for (Tile boreTile : boreTiles) { Tile beamNodeTile = Vars.world.tile( - Math.abs(direction.p.x) * outerMost.x + Math.abs(direction.p.y) * boreTile.x - offsetOpposite.x + directionOpposite.p.x * 2, - Math.abs(direction.p.y) * outerMost.y + Math.abs(direction.p.x) * boreTile.y - offsetOpposite.y + directionOpposite.p.y * 2); + Math.abs(direction.p.x) * outerMost.x + + Math.abs(direction.p.y) * boreTile.x + - offsetOpposite.x + directionOpposite.p.x * 2, + Math.abs(direction.p.y) * outerMost.y + + Math.abs(direction.p.x) * boreTile.y + - offsetOpposite.y + directionOpposite.p.y * 2); if (beamNodeTile == null) continue; - BuildPlan buildPlan = new BuildPlan(beamNodeTile.x, beamNodeTile.y, 0, Blocks.beamNode); - Vars.player.unit().addBuild(buildPlan); + BuildPlan beamPlan = new BuildPlan(beamNodeTile.x, beamNodeTile.y, 0, Blocks.beamNode); + if (Util.canPlaceWithoutPlanCollision(beamPlan, team, allPlans)) { + allPlans.add(beamPlan); + } + + // Chain beam nodes toward the bore if distance is large while (beamNodeTile.dst(boreTile) > 10 * Vars.tilesize) { beamNodeTile = beamNodeTile.nearby(direction.p.x * 5, direction.p.y * 5); if (beamNodeTile == null) break; - buildPlan = new BuildPlan(beamNodeTile.x, beamNodeTile.y, 0, Blocks.beamNode); - Vars.player.unit().addBuild(buildPlan); + BuildPlan chainPlan = new BuildPlan( + beamNodeTile.x, beamNodeTile.y, 0, Blocks.beamNode); + if (Util.canPlaceWithoutPlanCollision(chainPlan, team, allPlans)) { + allPlans.add(chainPlan); + } } } - for (Tile boreTile : boreTiles) { - BuildPlan buildPlan = new BuildPlan(boreTile.x, boreTile.y, direction.r, drill); - Vars.player.unit().addBuild(buildPlan); + // Note: bore plans were already added in Phase 1, so we don't re-add them. + + // --- Commit phase --- + Util.commitPlans(allPlans); + + if (Util.DEBUG) { + Log.info("[AutoDrill] WallDrill: committed " + allPlans.size + " plans for " + drill.name); } } @@ -176,7 +239,7 @@ private static Seq getConnectedWallTiles(Tile tile, Direction direction) { Item sourceItem = tile.wallDrop(); - int maxTiles = (int) (Core.settings.getInt(bundle.get("auto-drill.settings.max-tiles")) * 0.25f); + int maxTiles = (int)(Core.settings.getInt(bundle.get("auto-drill.settings.max-tiles")) * 0.25f); while (!queue.isEmpty() && tiles.size < maxTiles) { Tile currentTile = queue.removeFirst();