From 63b9f4886fb5b504d42b1e482e912ba78f435f98 Mon Sep 17 00:00:00 2001 From: Hut512 Date: Fri, 30 Oct 2026 01:23:24 +0100 Subject: [PATCH] Fast async floodfill for SteamEditWand --- .../src/de/steamwar/bausystem/BauSystem.java | 6 +- .../core/worldedit/ConnectedSearch.java | 475 ++++++++++++++++++ .../de/steamwar/core/worldedit/SteamEdit.java | 7 + .../core/worldedit/SteamEditWand.java | 75 +-- 4 files changed, 524 insertions(+), 39 deletions(-) create mode 100644 SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/ConnectedSearch.java diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java index c524c4d0..2e4d2b6f 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java @@ -30,6 +30,7 @@ import de.steamwar.bausystem.features.tracer.TraceManager; import de.steamwar.bausystem.features.tracer.TraceRecorder; import de.steamwar.bausystem.features.world.BauScoreboard; import de.steamwar.bausystem.linkage.BauGuiItem; +import de.steamwar.bausystem.region.Point; import de.steamwar.bausystem.region.Region; import de.steamwar.bausystem.region.RegionSystem; import de.steamwar.bausystem.utils.ScoreboardElement; @@ -47,7 +48,6 @@ import de.steamwar.providers.BauServerInfo; import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.GameRule; -import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -136,7 +136,9 @@ public class BauSystem extends JavaPlugin implements Listener { } else { return SteamEdit.AdjacentFilter.ALWAYS; } - return (x, y, z) -> area.inRegion(new Location(null, x, y, z), true); + Point min = area.getMinPoint(true); + Point max = area.getMaxPoint(true); + return new SteamEdit.BoxFilter(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); }; Bukkit.getWorlds().get(0).setGameRule(GameRule.SEND_COMMAND_FEEDBACK, false); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/ConnectedSearch.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/ConnectedSearch.java new file mode 100644 index 00000000..8dd76d30 --- /dev/null +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/ConnectedSearch.java @@ -0,0 +1,475 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.core.worldedit; + +import com.fastasyncworldedit.core.queue.IChunkGet; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.World; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockType; +import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.block.BlockTypesCache; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.server.level.ServerLevel; + +/** + * Bounds of the blocks face-connected to a start block, meant to run off the main thread. + *

+ * Works on 16³ chunk sections as bitsets of 64 longs, indexed like FAWE's section arrays + * (bit y << 8 | z << 4 | x, so a long holds four 16 block x-rows of one y layer). + * Each section is classified once from a FAWE copy of its blocks, then filled with bit operations: + * whole x-rows at once, z and y one layer per pass, repeated until nothing changes, and + * handed over to the neighbouring sections through their faces. + * Unloaded chunks count as not connected, so a search never loads chunks. + * A search must only be used by one thread at a time. + */ +final class ConnectedSearch { + + // Horizontal search box around the start block, larger than every schematic type + static final int RADIUS = 255; + + private static final int WORDS = 4096 / 64; + // Lowest and highest x of each 16 bit x-row in a word + private static final long ROW_FIRST = 0x0001_0001_0001_0001L; + private static final long ROW_LAST = 0x8000_8000_8000_8000L; + private static final long ROW = 0xFFFFL; + + /** + * Where a search reads its blocks from. + */ + @FunctionalInterface + interface SectionSource { + /** + * @return the blocks of a section as FAWE block state ordinals, null if it is not loaded or empty + */ + char[] blocks(int sectionX, int sectionY, int sectionZ); + + /** + * Reads loaded chunks through FAWE, which copies the sections thread safely. + * Caches the chunks, so it belongs to one search. + */ + static SectionSource loaded(World world, ServerLevel level) { + Long2ObjectOpenHashMap chunks = new Long2ObjectOpenHashMap<>(); + return (sectionX, sectionY, sectionZ) -> { + long key = (long) sectionX << 32 | (sectionZ & 0xFFFFFFFFL); + IChunkGet chunk; + if (chunks.containsKey(key)) { + chunk = chunks.get(key); + } else { + // Paper's lookup for any thread, FAWE itself would load missing chunks + chunk = level.getChunkSource().getChunkAtIfLoadedImmediately(sectionX, sectionZ) == null ? null : world.get(sectionX, sectionZ); + chunks.put(key, chunk); + } + return chunk == null || !chunk.hasSection(sectionY) ? null : chunk.load(sectionY); + }; + } + } + + private final SectionSource source; + private final SteamEdit.AdjacentFilter filter; + + // Search box in block coordinates, inclusive + private final int minX; + private final int minY; + private final int minZ; + private final int maxX; + private final int maxY; + private final int maxZ; + + private final int minSectionX; + private final int minSectionY; + private final int minSectionZ; + private final int sectionsX; + private final int sectionsY; + private final int sectionsZ; + + // Per section: null until classified, EMPTY if nothing in it can be connected + private final Section[] sections; + private int[] queue = new int[256]; + private int queueSize = 0; + + private boolean complete = true; + + ConnectedSearch(World world, ServerLevel level, int x, int y, int z, SteamEdit.AdjacentFilter filter) { + this(SectionSource.loaded(world, level), world.getMinY(), world.getMaxY(), x, y, z, filter); + } + + /** + * @param worldMinY lowest block y of the world + * @param worldMaxY highest block y of the world + */ + ConnectedSearch(SectionSource source, int worldMinY, int worldMaxY, int x, int y, int z, SteamEdit.AdjacentFilter filter) { + this.source = source; + this.filter = filter; + + int boxMinX = x - RADIUS, boxMinY = worldMinY, boxMinZ = z - RADIUS; + int boxMaxX = x + RADIUS, boxMaxY = worldMaxY, boxMaxZ = z + RADIUS; + if (filter instanceof SteamEdit.BoxFilter box) { + boxMinX = Math.max(boxMinX, box.minX()); + boxMinY = Math.max(boxMinY, box.minY()); + boxMinZ = Math.max(boxMinZ, box.minZ()); + boxMaxX = Math.min(boxMaxX, box.maxX()); + boxMaxY = Math.min(boxMaxY, box.maxY()); + boxMaxZ = Math.min(boxMaxZ, box.maxZ()); + } + minX = boxMinX; + minY = boxMinY; + minZ = boxMinZ; + maxX = boxMaxX; + maxY = boxMaxY; + maxZ = boxMaxZ; + + minSectionX = minX >> 4; + minSectionY = minY >> 4; + minSectionZ = minZ >> 4; + sectionsX = (maxX >> 4) - minSectionX + 1; + sectionsY = (maxY >> 4) - minSectionY + 1; + sectionsZ = (maxZ >> 4) - minSectionZ + 1; + sections = new Section[sectionsX * sectionsY * sectionsZ]; + + // The start block is part of the result regardless of its type, like WorldEdit's RecursiveVisitor + int start = sectionIndex(x >> 4, y >> 4, z >> 4); + Section section = section(start); + if (section == Section.EMPTY) { + section = new Section(x >> 4, y >> 4, z >> 4); + sections[start] = section; + } + int bit = bit(x & 15, y & 15, z & 15); + section.reached[bit >>> 6] |= 1L << bit; + enqueue(start, section); + } + + /** + * @param deadline {@link System#nanoTime()} after which the search stops with the bounds found so far + */ + ConnectedSearch run(long deadline) { + while (queueSize > 0) { + if (System.nanoTime() > deadline) { + complete = false; + break; + } + + int index = queue[--queueSize]; + Section section = sections[index]; + section.queued = false; + section.fill(); + spread(section); + } + return this; + } + + BlockVector3[] bounds() { + int[] min = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + int[] max = {Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE}; + for (Section section : sections) { + if (section != null && section != Section.EMPTY) section.bounds(min, max); + } + return new BlockVector3[]{BlockVector3.at(min[0], min[1], min[2]), BlockVector3.at(max[0], max[1], max[2])}; + } + + boolean isComplete() { + return complete; + } + + private static int bit(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + private int sectionIndex(int sectionX, int sectionY, int sectionZ) { + return ((sectionY - minSectionY) * sectionsZ + (sectionZ - minSectionZ)) * sectionsX + (sectionX - minSectionX); + } + + private void enqueue(int index, Section section) { + if (section.queued) return; + section.queued = true; + if (queueSize == queue.length) { + int[] grown = new int[queue.length * 2]; + System.arraycopy(queue, 0, grown, 0, queueSize); + queue = grown; + } + queue[queueSize++] = index; + } + + /** + * Hands the reached blocks on the faces of a section over to its neighbours. + */ + private void spread(Section from) { + long[] reached = from.reached; + long[] face = new long[WORDS]; + + if (from.x > minSectionX) { + for (int w = 0; w < WORDS; w++) face[w] = (reached[w] & ROW_FIRST) << 15; + handOver(from.x - 1, from.y, from.z, face); + } + if (from.x < minSectionX + sectionsX - 1) { + for (int w = 0; w < WORDS; w++) face[w] = (reached[w] & ROW_LAST) >>> 15; + handOver(from.x + 1, from.y, from.z, face); + } + + if (from.z > minSectionZ) { + java.util.Arrays.fill(face, 0); + for (int y = 0; y < 16; y++) face[y << 2 | 3] = (reached[y << 2] & ROW) << 48; + handOver(from.x, from.y, from.z - 1, face); + } + if (from.z < minSectionZ + sectionsZ - 1) { + java.util.Arrays.fill(face, 0); + for (int y = 0; y < 16; y++) face[y << 2] = reached[y << 2 | 3] >>> 48; + handOver(from.x, from.y, from.z + 1, face); + } + + if (from.y > minSectionY) { + java.util.Arrays.fill(face, 0); + System.arraycopy(reached, 0, face, WORDS - 4, 4); + handOver(from.x, from.y - 1, from.z, face); + } + if (from.y < minSectionY + sectionsY - 1) { + java.util.Arrays.fill(face, 0); + System.arraycopy(reached, WORDS - 4, face, 0, 4); + handOver(from.x, from.y + 1, from.z, face); + } + } + + private void handOver(int sectionX, int sectionY, int sectionZ, long[] face) { + boolean any = false; + for (long word : face) { + if (word != 0) { + any = true; + break; + } + } + if (!any) return; + + int index = sectionIndex(sectionX, sectionY, sectionZ); + Section section = section(index); + if (section == Section.EMPTY) return; + + boolean changed = false; + for (int w = 0; w < WORDS; w++) { + long added = face[w] & section.solid[w] & ~section.reached[w]; + if (added != 0) { + section.reached[w] |= added; + changed = true; + } + } + if (changed) enqueue(index, section); + } + + /** + * @return the classified section, {@link Section#EMPTY} if it is unloaded, outside the search box or has nothing to connect + */ + private Section section(int index) { + Section section = sections[index]; + if (section != null) return section; + + int sectionX = index % sectionsX + minSectionX; + int sectionZ = index / sectionsX % sectionsZ + minSectionZ; + int sectionY = index / sectionsX / sectionsZ + minSectionY; + section = classify(sectionX, sectionY, sectionZ); + sections[index] = section; + return section; + } + + private Section classify(int sectionX, int sectionY, int sectionZ) { + char[] blocks = source.blocks(sectionX, sectionY, sectionZ); + if (blocks == null) return Section.EMPTY; + + Section section = new Section(sectionX, sectionY, sectionZ); + if (!classify(blocks, section.solid)) return Section.EMPTY; + restrict(section); + return section; + } + + /** + * Sets the connectable blocks of a section. + * + * @return whether the section has any connectable block + */ + private static boolean classify(char[] blocks, long[] solid) { + boolean[] connectable = Connectable.ORDINALS; + long any = 0; + for (int w = 0; w < WORDS; w++) { + int base = w << 6; + long word = 0; + for (int i = 0; i < 64; i++) { + if (connectable[blocks[base + i]]) word |= 1L << i; + } + solid[w] = word; + any |= word; + } + return any != 0; + } + + /** + * Removes the blocks outside the search box and the filter from a section. + */ + private void restrict(Section section) { + int baseX = section.x << 4, baseY = section.y << 4, baseZ = section.z << 4; + boolean inside = baseX >= minX && baseX + 15 <= maxX + && baseY >= minY && baseY + 15 <= maxY + && baseZ >= minZ && baseZ + 15 <= maxZ; + + if (!inside) { + long row = 0; + for (int x = 0; x < 16; x++) { + if (baseX + x >= minX && baseX + x <= maxX) row |= 1L << x; + } + for (int y = 0; y < 16; y++) { + boolean yInside = baseY + y >= minY && baseY + y <= maxY; + for (int zq = 0; zq < 4; zq++) { + long mask = 0; + for (int lane = 0; lane < 4; lane++) { + int z = baseZ + (zq << 2 | lane); + if (yInside && z >= minZ && z <= maxZ) mask |= row << (lane << 4); + } + section.solid[y << 2 | zq] &= mask; + } + } + } + + // Boxes are applied above, other filters have to be asked per block + if (filter != SteamEdit.AdjacentFilter.ALWAYS && !(filter instanceof SteamEdit.BoxFilter)) { + for (int bit = 0; bit < 4096; bit++) { + long flag = 1L << bit; + if ((section.solid[bit >>> 6] & flag) != 0 && !filter.contains(baseX + (bit & 15), baseY + (bit >>> 8), baseZ + (bit >>> 4 & 15))) { + section.solid[bit >>> 6] &= ~flag; + } + } + } + } + + // Initialized on the first search, once WorldEdit has loaded its block registry + private static final class Connectable { + // Same as Bukkit's !isEmpty() && !isLiquid(), indexed by FAWE's block state ordinal + static final boolean[] ORDINALS = new boolean[Character.MAX_VALUE + 1]; + + static { + BlockState[] states = BlockTypesCache.states; + // Ordinal 0 is FAWE's reserved "no block" + for (int ordinal = 1; ordinal < states.length; ordinal++) { + BlockType type = states[ordinal].getBlockType(); + ORDINALS[ordinal] = !type.getMaterial().isAir() && type != BlockTypes.WATER && type != BlockTypes.LAVA; + } + } + } + + private static final class Section { + static final Section EMPTY = new Section(0, 0, 0); + + final int x; + final int y; + final int z; + final long[] solid = new long[WORDS]; + final long[] reached = new long[WORDS]; + boolean queued = false; + + Section(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Extends the reached blocks over all connected solid blocks of this section. + * Ascending passes carry +y/+z within the pass, descending passes -y/-z. + */ + void fill() { + boolean changed = true; + boolean ascending = true; + while (changed) { + changed = false; + for (int i = 0; i < WORDS; i++) { + int w = ascending ? i : WORDS - 1 - i; + long s = solid[w]; + long r = reached[w]; + + long in = r + | (r << 1 & ~ROW_FIRST) | (r >>> 1 & ~ROW_LAST) + | r << 16 | r >>> 16; + if ((w & 3) != 0) in |= reached[w - 1] >>> 48; + if ((w & 3) != 3) in |= reached[w + 1] << 48; + if (w >= 4) in |= reached[w - 4]; + if (w < WORDS - 4) in |= reached[w + 4]; + + long next = r | fillRows(in & s, s); + if (next != r) { + reached[w] = next; + changed = true; + } + } + ascending = !ascending; + } + } + + /** + * Extends the seeds along the x-rows over contiguous solid blocks, in both directions. + */ + private static long fillRows(long seeds, long solid) { + long up = seeds; + long open = solid & ~ROW_FIRST; + up |= open & (up << 1); + open &= open << 1; + up |= open & (up << 2); + open &= open << 2; + up |= open & (up << 4); + open &= open << 4; + up |= open & (up << 8); + + long down = seeds; + open = solid & ~ROW_LAST; + down |= open & (down >>> 1); + open &= open >>> 1; + down |= open & (down >>> 2); + open &= open >>> 2; + down |= open & (down >>> 4); + open &= open >>> 4; + down |= open & (down >>> 8); + + return up | down; + } + + void bounds(int[] min, int[] max) { + long rows = 0; + int zMask = 0; + int lowWord = -1, highWord = -1; + for (int w = 0; w < WORDS; w++) { + long r = reached[w]; + if (r == 0) continue; + if (lowWord < 0) lowWord = w; + highWord = w; + for (int lane = 0; lane < 4; lane++) { + long row = r >>> (lane << 4) & ROW; + if (row != 0) { + rows |= row; + zMask |= 1 << ((w & 3) << 2 | lane); + } + } + } + if (lowWord < 0) return; + + int baseX = x << 4, baseY = y << 4, baseZ = z << 4; + min[0] = Math.min(min[0], baseX + Long.numberOfTrailingZeros(rows)); + max[0] = Math.max(max[0], baseX + 63 - Long.numberOfLeadingZeros(rows)); + min[1] = Math.min(min[1], baseY + (lowWord >>> 2)); + max[1] = Math.max(max[1], baseY + (highWord >>> 2)); + min[2] = Math.min(min[2], baseZ + Integer.numberOfTrailingZeros(zMask)); + max[2] = Math.max(max[2], baseZ + 31 - Integer.numberOfLeadingZeros(zMask)); + } + } +} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEdit.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEdit.java index 03c7922e..616f7638 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEdit.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEdit.java @@ -52,6 +52,13 @@ public class SteamEdit { boolean contains(int x, int y, int z); } + public record BoxFilter(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) implements AdjacentFilter { + @Override + public boolean contains(int x, int y, int z) { + return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ; + } + } + private static final class Command extends SWCommand { public Command() { diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java index a6d56638..d9793d73 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java @@ -19,13 +19,11 @@ package de.steamwar.core.worldedit; +import com.fastasyncworldedit.core.util.TaskManager; import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.extension.platform.Actor; -import com.sk89q.worldedit.function.mask.Mask; -import com.sk89q.worldedit.function.operation.Operations; -import com.sk89q.worldedit.function.visitor.RecursiveVisitor; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.RegionSelector; import com.sk89q.worldedit.regions.selector.CuboidRegionSelector; @@ -33,10 +31,12 @@ import com.sk89q.worldedit.regions.selector.limit.PermissiveSelectorLimits; import de.steamwar.core.Core; import de.steamwar.inventory.SWItem; import io.papermc.paper.event.player.PlayerPickBlockEvent; +import net.minecraft.server.level.ServerLevel; import org.bukkit.*; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeModifier; import org.bukkit.block.Block; +import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -45,8 +45,13 @@ import org.bukkit.event.player.PlayerSwapHandItemsEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; public class SteamEditWand implements Listener { @@ -71,7 +76,7 @@ public class SteamEditWand implements Listener { ITEM.setItemMeta(itemMeta); } - private static final int MAX_CONNECTED_DEPTH = 256; + private static final long MAX_CONNECTED_NANOS = TimeUnit.MILLISECONDS.toNanos(500); private boolean checkWand(Player player) { ItemStack item = player.getInventory().getItemInMainHand(); @@ -81,6 +86,7 @@ public class SteamEditWand implements Listener { } private final WorldEditPlugin we; + private final Set selectingConnected = new HashSet<>(); SteamEditWand() { we = WorldEditWrapper.getWorldEditPlugin(); @@ -167,9 +173,36 @@ public class SteamEditWand implements Listener { Player player = event.getPlayer(); if (!checkWand(player)) return; event.setCancelled(true); + if (!selectingConnected.add(player.getUniqueId())) return; + + Block origin = event.getBlock(); + World world = origin.getWorld(); + SteamEdit.AdjacentFilter filter = SteamEdit.ADJACENT_FILTERS.apply(origin.getLocation()); + com.sk89q.worldedit.world.World weWorld = BukkitAdapter.adapt(world); + ServerLevel level = ((CraftWorld) world).getHandle(); + int x = origin.getX(), y = origin.getY(), z = origin.getZ(); + + TaskManager.taskManager().async(() -> { + BlockVector3[] bounds = null; + try { + bounds = new ConnectedSearch(weWorld, level, x, y, z, filter) + .run(System.nanoTime() + MAX_CONNECTED_NANOS) + .bounds(); + } catch (Exception e) { + Core.getInstance().getLogger().log(Level.SEVERE, "Could not select connected blocks", e); + } + + BlockVector3[] result = bounds; + TaskManager.taskManager().task(() -> { + selectingConnected.remove(player.getUniqueId()); + if (result != null && player.isOnline()) selectConnected(player, result); + }); + }); + } + + private void selectConnected(Player player, BlockVector3[] bounds) { LocalSession session = we.getSession(player); RegionSelector regionSelector = regionSelector(player, session); - BlockVector3[] bounds = connectedBounds(event.getBlock()); boolean primaryChanged = Corner.PRIMARY.changed(regionSelector, bounds[0]); boolean secondaryChanged = Corner.SECONDARY.changed(regionSelector, bounds[1]); @@ -181,38 +214,6 @@ public class SteamEditWand implements Listener { WorldEditRenderer.renderPlayer(player); } - private static BlockVector3[] connectedBounds(Block origin) { - World world = origin.getWorld(); - SteamEdit.AdjacentFilter filter = SteamEdit.ADJACENT_FILTERS.apply(origin.getLocation()); - BlockVector3 start = BukkitAdapter.asBlockVector(origin.getLocation()); - BlockVector3[] bounds = {start, start}; - - Mask connected = new Mask() { - @Override - public boolean test(BlockVector3 position) { - int x = position.x(); - int y = position.y(); - int z = position.z(); - if (!filter.contains(x, y, z)) return false; - Block block = world.getBlockAt(x, y, z); - return !block.isEmpty() && !block.isLiquid(); - } - - @Override - public Mask copy() { - return this; - } - }; - RecursiveVisitor visitor = new RecursiveVisitor(connected, position -> { - bounds[0] = bounds[0].getMinimum(position); - bounds[1] = bounds[1].getMaximum(position); - return true; - }, MAX_CONNECTED_DEPTH, world.getMinHeight(), world.getMaxHeight() - 1); - visitor.visit(start); - Operations.complete(visitor); - return bounds; - } - private enum Corner { PRIMARY { @Override