package com.hypixel.hytale.server.spawning.util; import com.hypixel.fastutil.longs.Long2ObjectConcurrentHashMap; import com.hypixel.hytale.component.Component; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.math.util.ChunkUtil; import com.hypixel.hytale.math.util.MathUtil; import com.hypixel.hytale.math.vector.Vector3d; import com.hypixel.hytale.math.vector.Vector3i; import com.hypixel.hytale.protocol.BlockMaterial; import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; import com.hypixel.hytale.server.core.asset.type.fluid.Fluid; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.accessor.ChunkAccessor; import com.hypixel.hytale.server.core.universe.world.accessor.LocalCachedChunkAccessor; import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.npc.NPCPlugin; import com.hypixel.hytale.server.npc.asset.builder.Builder; import com.hypixel.hytale.server.npc.role.Role; import com.hypixel.hytale.server.spawning.ISpawnableWithModel; import com.hypixel.hytale.server.spawning.SpawnTestResult; import com.hypixel.hytale.server.spawning.SpawningContext; import com.hypixel.hytale.server.spawning.SpawningPlugin; import com.hypixel.hytale.server.spawning.assets.spawns.config.RoleSpawnParameters; import com.hypixel.hytale.server.spawning.suppression.component.ChunkSuppressionEntry; import com.hypixel.hytale.server.spawning.suppression.component.SpawnSuppressionController; import com.hypixel.hytale.server.spawning.wrappers.BeaconSpawnWrapper; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntLists; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectArrays; import java.util.ArrayDeque; import java.util.Arrays; import java.util.BitSet; import java.util.Comparator; import java.util.Deque; import java.util.concurrent.ThreadLocalRandom; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class FloodFillPositionSelector implements Component { private static final int MAX_SPAWN_POSITIONS_HINT = 30; private static final double SPAWN_POSITION_DENSITY = 0.1; private static final int CONCURRENT_POSITION_OPTION_MULTIPLIER = 3; private static final double MAX_FAILED_SPAWN_POSITION_RATIO = 0.25; private static final double IRREGULAR_MIN_SPAWNS_MULTIPLIER = 0.3; private static final double IRREGULAR_MAX_SPAWNS_MULTIPLIER = 5.0; private static final int NOT_CHECKED = -1; private static final int BLOCKED = -2; private static final int TOO_HIGH = Integer.MAX_VALUE; private static final int TOO_LOW = Integer.MIN_VALUE; private static final int MAX_RESOLUTION_DIVISOR = 8; private static final ThreadLocal sortBufferProvider = ThreadLocal.withInitial( FloodFillPositionSelector.SortBufferProvider::new ); private static final Comparator WEIGHTED_POSITION_COMPARATOR = (entry1, entry2) -> Double.compare( ((FloodFillPositionSelector.WeightedPosition)entry2).getWeight(), ((FloodFillPositionSelector.WeightedPosition)entry1).getWeight() ); private final World world; @Nullable private ChunkAccessor chunkAccessor; private final int size; private int minY; private int maxY; @Nonnull private final BeaconSpawnWrapper spawnWrapper; @Nonnull private final int[] roleIndexes; @Nonnull private final int[] heightGrid; @Nonnull private final Int2ObjectMap resolutionMaps; private final BitSet fullResolutionMap; private int desiredPositionCount; private final IntArrayList highResolutionOptions = new IntArrayList(4); private final Deque floodFillQueue = new ArrayDeque<>(); private final SpawningContext spawningContext = new SpawningContext(); @Nullable private WorldChunk chunk; private final IntSet positionIndexes = new IntOpenHashSet(); private final Int2ObjectMap> positionsByRole = new Int2ObjectOpenHashMap(); private final Int2IntMap failedSpawnsByRole = new Int2IntOpenHashMap(); private boolean hasRun; private FloodFillPositionSelector.Debug debug; private boolean irregularCase; private IntSet failedPositionTestIndexes; private double calculatePositionsAfter; public static ComponentType getComponentType() { return SpawningPlugin.get().getFloodFillPositionSelectorComponentType(); } public FloodFillPositionSelector(World world, @Nonnull BeaconSpawnWrapper spawnWrapper) { this.world = world; int baseSize = MathUtil.ceil(spawnWrapper.getSpawnRadius()) * 2; this.size = (baseSize + 8 - 1) / 8 * 8; this.heightGrid = new int[this.size * this.size]; Arrays.fill(this.heightGrid, -1); this.resolutionMaps = new Int2ObjectOpenHashMap(); for (int i = 1; i <= 8; i *= 2) { this.resolutionMaps.put(i, new BitSet(this.heightGrid.length / i)); } this.fullResolutionMap = (BitSet)this.resolutionMaps.get(1); this.spawnWrapper = spawnWrapper; RoleSpawnParameters[] roleSpawnParameters = spawnWrapper.getSpawn().getNPCs(); this.roleIndexes = new int[roleSpawnParameters.length]; for (int i = 0; i < this.roleIndexes.length; i++) { int roleIndex = NPCPlugin.get().getIndex(roleSpawnParameters[i].getId()); this.roleIndexes[i] = roleIndex; this.positionsByRole.put(roleIndex, new ObjectArrayList()); } this.debug = spawnWrapper.getSpawn().getDebug(); if (this.debug != FloodFillPositionSelector.Debug.DISABLED) { this.failedPositionTestIndexes = new IntOpenHashSet(); } } public void setCalculatePositionsAfter(double calculatePositionsAfter) { this.calculatePositionsAfter = calculatePositionsAfter; } public boolean tickCalculatePositionsAfter(float dt) { return (this.calculatePositionsAfter -= dt) <= 0.0; } public boolean hasPositionsForRole(int roleIndex) { return !((ObjectArrayList)this.positionsByRole.get(roleIndex)).isEmpty(); } public boolean prepareSpawnContext( @Nonnull Vector3d playerPosition, int spawnsThisRound, int roleIndex, @Nonnull SpawningContext spawningContext, @Nonnull BeaconSpawnWrapper spawnWrapper ) { ObjectArrayList positions = (ObjectArrayList)this.positionsByRole .get(roleIndex); if (this.positionsByRole.isEmpty()) { return false; } else { double minDistanceFromPlayerSquared = spawnWrapper.getMinDistanceFromPlayerSquared(); double targetDistanceFromPlayerSquared = spawnWrapper.getTargetDistanceFromPlayerSquared(); for (int i = 0; i < positions.size(); i++) { FloodFillPositionSelector.WeightedPosition entry = (FloodFillPositionSelector.WeightedPosition)positions.get(i); double distance = playerPosition.distanceSquaredTo(entry.position); entry.weight = distance < minDistanceFromPlayerSquared ? 0.0 : Math.max(0.0, targetDistanceFromPlayerSquared - Math.abs(distance - targetDistanceFromPlayerSquared)); } int targetNumberOfOptions = spawnsThisRound * 3; int size = positions.size(); FloodFillPositionSelector.WeightedPosition[] sortBuffer = sortBufferProvider.get().getBuffer(size); System.arraycopy(positions.elements(), 0, sortBuffer, 0, size); ObjectArrays.mergeSort(positions.elements(), 0, size, WEIGHTED_POSITION_COMPARATOR, sortBuffer); double sum = 0.0; for (int i = 0; i < targetNumberOfOptions && i < positions.size(); i++) { FloodFillPositionSelector.WeightedPosition entry = (FloodFillPositionSelector.WeightedPosition)positions.get(i); if (entry.weight == 0.0) { break; } sum += entry.weight; } if (sum == 0.0) { return false; } else { sum = ThreadLocalRandom.current().nextDouble(sum); int selectedIndex = -1; FloodFillPositionSelector.WeightedPosition entry = null; for (int i = 0; i < targetNumberOfOptions && i < positions.size(); i++) { entry = (FloodFillPositionSelector.WeightedPosition)positions.get(i); selectedIndex = i; sum -= entry.weight; if (sum < 0.0) { break; } } if (entry == null) { return false; } else { Vector3i position = entry.position; SpawnSuppressionController suppressionController = this.world .getEntityStore() .getStore() .getResource(SpawnSuppressionController.getResourceType()); long indexChunk = ChunkUtil.indexChunk(ChunkUtil.chunkCoordinate(position.x), ChunkUtil.chunkCoordinate(position.z)); ChunkSuppressionEntry suppressionEntry = suppressionController.getChunkSuppressionMap().get(indexChunk); if ((suppressionEntry == null || !suppressionEntry.isSuppressingRoleAt(roleIndex, position.y)) && spawningContext.set(this.world, position.x, position.y, position.z) && spawningContext.canSpawn() == SpawnTestResult.TEST_OK) { return true; } else { positions.remove(selectedIndex); int totalFailed = this.failedSpawnsByRole.mergeInt(roleIndex, 1, Integer::sum); if (totalFailed > positions.size() * 0.25) { this.hasRun = false; } return false; } } } } } public boolean shouldRebuildCache() { return !this.hasRun; } public void forceRebuildCache() { this.hasRun = false; } public void init() { Arrays.fill(this.heightGrid, -1); for (int i = 1; i <= 8; i *= 2) { ((BitSet)this.resolutionMaps.get(i)).clear(); } for (int role : this.roleIndexes) { ((ObjectArrayList)this.positionsByRole.get(role)).clear(); this.failedSpawnsByRole.put(role, 0); } this.irregularCase = false; } public void buildPositionCache(@Nonnull Vector3d origin, @Nonnull FloodFillEntryPoolSimple pool) { int sizeHalf = this.size / 2; int worldX = MathUtil.floor(origin.getX()); int worldY = MathUtil.floor(origin.getY()); int worldZ = MathUtil.floor(origin.getZ()); this.chunkAccessor = LocalCachedChunkAccessor.atWorldCoords(this.world, worldX, worldZ, sizeHalf); this.chunk = this.chunkAccessor.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(worldX, worldZ)); if (this.chunk != null) { int[] yRange = this.spawnWrapper.getSpawn().getYRange(); this.minY = Math.max(0, worldY + yRange[0]); this.maxY = Math.min(319, worldY + yRange[1]); this.floodFill(worldX, worldY, worldZ, sizeHalf, sizeHalf, pool); this.desiredPositionCount = Math.min(MathUtil.ceil(this.fullResolutionMap.cardinality() * 0.1), 30); if (this.desiredPositionCount > 0) { this.findPositions(worldX, worldZ); } else { SpawningPlugin.get().getLogger().at(Level.WARNING).log("Spawn beacon at: " + origin + " unable to find any suitable positions to check"); } if (this.debug != FloodFillPositionSelector.Debug.DISABLED && (this.irregularCase || this.debug == FloodFillPositionSelector.Debug.ALL)) { for (int i = 2; i <= 8; i *= 2) { BitSet map = (BitSet)this.resolutionMaps.get(i); if (map.cardinality() > 0) { SpawningPlugin.get().getLogger().at(Level.WARNING).log(this.debugDumpLowResolutionMap(map, this.size / i)); } } SpawningPlugin.get().getLogger().at(Level.WARNING).log("Spawn beacon at: " + origin + (this.irregularCase ? " is an irregular case" : "")); } this.chunkAccessor = null; this.chunk = null; this.spawningContext.releaseFull(); this.hasRun = true; } } private void floodFill(int worldX, int worldY, int worldZ, int setX, int setZ, @Nonnull FloodFillEntryPoolSimple pool) { if (this.chunk != null) { this.floodFillQueue.clear(); int[] initialEntry = pool.allocate(); initialEntry[0] = worldX; initialEntry[1] = worldY; initialEntry[2] = worldZ; initialEntry[3] = setX; initialEntry[4] = setZ; this.floodFillQueue.add(initialEntry); int chunkX = this.chunk.getX(); int chunkZ = this.chunk.getZ(); label66: while (!this.floodFillQueue.isEmpty()) { int[] state = this.floodFillQueue.poll(); worldX = state[0]; worldY = state[1]; worldZ = state[2]; setX = state[3]; setZ = state[4]; if (setX >= 0 && setX < this.size && setZ >= 0 && setZ < this.size) { int index = getPositionIndex(setX, setZ, this.size); if (this.heightGrid[index] != -1) { pool.deallocate(state); } else { if (!ChunkUtil.isInsideChunk(chunkX, chunkZ, worldX, worldZ)) { WorldChunk newChunk = this.chunkAccessor.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(worldX, worldZ)); if (newChunk == null) { this.heightGrid[index] = -2; pool.deallocate(state); continue; } this.chunk = newChunk; chunkX = this.chunk.getX(); chunkZ = this.chunk.getZ(); } int block = this.chunk.getBlock(worldX, worldY, worldZ); if (block != 0 && BlockType.getAssetMap().getAsset(block).getMaterial() == BlockMaterial.Solid) { while (block != 0) { block = this.chunk.getBlock(worldX, ++worldY, worldZ); if (worldY > this.maxY) { this.heightGrid[index] = Integer.MAX_VALUE; pool.deallocate(state); continue label66; } } } else { while (block == 0) { block = this.chunk.getBlock(worldX, --worldY, worldZ); if (worldY < this.minY) { this.heightGrid[index] = Integer.MIN_VALUE; pool.deallocate(state); continue label66; } } worldY++; } this.heightGrid[index] = worldY; this.fullResolutionMap.set(index); state[0] = worldX + 1; state[1] = worldY; state[2] = worldZ; state[3] = setX + 1; state[4] = setZ; this.floodFillQueue.add(state); int[] entry2 = pool.allocate(); entry2[0] = worldX - 1; entry2[1] = worldY; entry2[2] = worldZ; entry2[3] = setX - 1; entry2[4] = setZ; this.floodFillQueue.add(entry2); int[] entry3 = pool.allocate(); entry3[0] = worldX; entry3[1] = worldY; entry3[2] = worldZ + 1; entry3[3] = setX; entry3[4] = setZ + 1; this.floodFillQueue.add(entry3); int[] entry4 = pool.allocate(); entry4[0] = worldX; entry4[1] = worldY; entry4[2] = worldZ - 1; entry4[3] = setX; entry4[4] = setZ - 1; this.floodFillQueue.add(entry4); } } else { pool.deallocate(state); } } } } private void findPositions(int originX, int originZ) { int resolution = 1; int segments; do { resolution *= 2; segments = this.buildLowerResolutionMap( (BitSet)this.resolutionMaps.get(resolution), this.size / resolution, (BitSet)this.resolutionMaps.get(resolution / 2), this.size / (resolution / 2) ); } while (resolution < 8 && segments / this.desiredPositionCount > 1); BitSet lowestResolutionMap = (BitSet)this.resolutionMaps.get(resolution); int openSpots = lowestResolutionMap.cardinality(); if (openSpots < this.desiredPositionCount) { resolution /= 2; lowestResolutionMap = (BitSet)this.resolutionMaps.get(resolution); } int sizeHalf = this.size / 2; int offsetOriginX = originX - sizeHalf; int offsetOriginZ = originZ - sizeHalf; SpawnSuppressionController suppressionController = this.world.getEntityStore().getStore().getResource(SpawnSuppressionController.getResourceType()); long chunkIndex = ChunkUtil.NOT_FOUND; ChunkSuppressionEntry suppressionEntry = null; Long2ObjectConcurrentHashMap chunkSuppressionMap = suppressionController.getChunkSuppressionMap(); for (int roleIndex : this.roleIndexes) { Builder spawnable = NPCPlugin.get().tryGetCachedValidRole(roleIndex); if (spawnable != null && spawnable.isSpawnable() && spawnable instanceof ISpawnableWithModel && this.spawningContext.setSpawnable((ISpawnableWithModel)spawnable, true)) { ObjectArrayList positionList = (ObjectArrayList)this.positionsByRole .get(roleIndex); for (int i = lowestResolutionMap.nextSetBit(0); i >= 0; i = lowestResolutionMap.nextSetBit(i + 1)) { int chosenIndex = i; for (int currentResolution = resolution; currentResolution > 1; currentResolution /= 2) { int nextResolution = currentResolution / 2; chosenIndex = this.pickOpenSegment( chosenIndex, this.size / currentResolution, (BitSet)this.resolutionMaps.get(nextResolution), this.size / nextResolution ); } int x = offsetOriginX + xFromIndex(chosenIndex, this.size); int y = this.heightGrid[chosenIndex]; int z = offsetOriginZ + zFromIndex(chosenIndex, this.size); long newChunkIndex = ChunkUtil.indexChunk(ChunkUtil.chunkCoordinate(x), ChunkUtil.chunkCoordinate(z)); if (chunkIndex != newChunkIndex) { suppressionEntry = chunkSuppressionMap.get(newChunkIndex); chunkIndex = newChunkIndex; } if (!this.canSpawn(x, y, z, roleIndex, suppressionEntry)) { if (this.debug != FloodFillPositionSelector.Debug.DISABLED) { this.failedPositionTestIndexes.add(chosenIndex); } int originalIndex = chosenIndex; chosenIndex = this.shiftIndexAwayFromWall(chosenIndex); if (chosenIndex == originalIndex) { continue; } x = offsetOriginX + xFromIndex(chosenIndex, this.size); y = this.heightGrid[chosenIndex]; z = offsetOriginZ + zFromIndex(chosenIndex, this.size); newChunkIndex = ChunkUtil.indexChunk(ChunkUtil.chunkCoordinate(x), ChunkUtil.chunkCoordinate(z)); if (chunkIndex != newChunkIndex) { suppressionEntry = chunkSuppressionMap.get(newChunkIndex); chunkIndex = newChunkIndex; } if (!this.canSpawn(x, y, z, roleIndex, suppressionEntry)) { if (this.debug != FloodFillPositionSelector.Debug.DISABLED) { this.failedPositionTestIndexes.add(chosenIndex); } continue; } } if (this.positionIndexes.add(chosenIndex)) { positionList.add(new FloodFillPositionSelector.WeightedPosition(x, y, z)); } if (i == Integer.MAX_VALUE) { break; } } int positionCount = this.positionIndexes.size(); if (this.debug != FloodFillPositionSelector.Debug.DISABLED && (positionCount < this.desiredPositionCount * 0.3 || positionCount > this.desiredPositionCount * 5.0)) { this.irregularCase = true; } if (this.irregularCase || this.debug == FloodFillPositionSelector.Debug.ALL) { SpawningPlugin.get().getLogger().at(Level.WARNING).log("Role: " + NPCPlugin.get().getName(roleIndex)); SpawningPlugin.get().getLogger().at(Level.WARNING).log(this.debugDumpBaseFloodFill()); this.failedPositionTestIndexes.clear(); } this.positionIndexes.clear(); } } } private int buildLowerResolutionMap(@Nonnull BitSet targetMap, int mapSize, @Nonnull BitSet parentMap, int parentMapSize) { for (int x = 0; x < mapSize; x++) { for (int z = 0; z < mapSize; z++) { int parentX = x * 2; int parentZ = z * 2; int index = getPositionIndex(parentX, parentZ, parentMapSize); if (parentMap.get(index) || parentMap.get(index + 1) || parentMap.get(index + parentMapSize) || parentMap.get(index + parentMapSize + 1)) { targetMap.set(getPositionIndex(x, z, mapSize)); } } } return targetMap.cardinality(); } private int pickOpenSegment(int lowResolutionIndex, int lowResolutionMapSize, @Nonnull BitSet higherResolutionMap, int highResolutionMapSize) { int x = xFromIndex(lowResolutionIndex, lowResolutionMapSize); int z = zFromIndex(lowResolutionIndex, lowResolutionMapSize); int parentX = x * 2; int parentZ = z * 2; int originIndex = getPositionIndex(parentX, parentZ, highResolutionMapSize); if (higherResolutionMap.get(originIndex)) { this.highResolutionOptions.add(originIndex); } int index = originIndex + 1; if (higherResolutionMap.get(index)) { this.highResolutionOptions.add(index); } index = originIndex + highResolutionMapSize; if (higherResolutionMap.get(index)) { this.highResolutionOptions.add(index); } index = originIndex + highResolutionMapSize + 1; if (higherResolutionMap.get(index)) { this.highResolutionOptions.add(index); } if (this.highResolutionOptions.size() > 1) { IntLists.shuffle(this.highResolutionOptions, ThreadLocalRandom.current()); } index = this.highResolutionOptions.getInt(0); this.highResolutionOptions.clear(); return index; } private int shiftIndexAwayFromWall(int index) { int newIndex = index; int checkIndex = index - 1; if (checkIndex < 0 || !this.fullResolutionMap.get(checkIndex)) { newIndex = index + 1; } checkIndex = index + 1; if (checkIndex >= this.fullResolutionMap.size() || !this.fullResolutionMap.get(checkIndex)) { newIndex--; } checkIndex = index - this.size; if (checkIndex < 0 || !this.fullResolutionMap.get(checkIndex)) { newIndex += this.size; } checkIndex = index + this.size; if (checkIndex >= this.fullResolutionMap.size() || !this.fullResolutionMap.get(checkIndex)) { newIndex -= this.size; } return this.fullResolutionMap.get(newIndex) ? newIndex : index; } private boolean canSpawn(int x, int y, int z, int roleIndex, @Nullable ChunkSuppressionEntry suppressionEntry) { if (!this.spawnWrapper.getSpawn().isOverrideSpawnSuppressors() && suppressionEntry != null && suppressionEntry.isSuppressingRoleAt(roleIndex, y)) { return false; } else if (!this.spawningContext.set(this.world, x, y, z) || this.spawningContext.canSpawn() != SpawnTestResult.TEST_OK || !this.spawnWrapper.withinLightRange(this.spawningContext)) { return false; } else if (this.spawningContext.ySpawn > this.maxY) { return false; } else { IntSet spawnBlockSet = this.spawnWrapper.getSpawnBlockSet(roleIndex); int spawnFluidTag = this.spawnWrapper.getSpawnFluidTag(roleIndex); if (spawnBlockSet == null && spawnFluidTag == Integer.MIN_VALUE) { return true; } else { return spawnBlockSet != null && spawnBlockSet.contains(this.spawningContext.groundBlockId) ? true : spawnFluidTag != Integer.MIN_VALUE && Fluid.getAssetMap().getIndexesForTag(spawnFluidTag).contains(this.spawningContext.groundFluidId); } } } @Nonnull private String debugDumpBaseFloodFill() { StringBuilder sb = new StringBuilder(); int sizeHalf = this.size / 2; int centre = getPositionIndex(sizeHalf, sizeHalf, this.size); for (int z = 0; z < this.size; z++) { for (int x = 0; x < this.size; x++) { int index = getPositionIndex(x, z, this.size); if (index == centre) { sb.append('B'); } else if (this.positionIndexes.contains(index)) { sb.append('S'); } else if (this.failedPositionTestIndexes.contains(index)) { sb.append('F'); } else { switch (this.heightGrid[index]) { case Integer.MIN_VALUE: sb.append('v'); break; case -2: case -1: sb.append('X'); break; case Integer.MAX_VALUE: sb.append('^'); break; default: sb.append('.'); } } sb.append(' '); } sb.append('\n'); } return sb.toString(); } @Nonnull private String debugDumpLowResolutionMap(@Nonnull BitSet map, int size) { StringBuilder sb = new StringBuilder(); for (int z = 0; z < size; z++) { for (int x = 0; x < size; x++) { sb.append((char)(map.get(getPositionIndex(x, z, size)) ? '.' : 'X')).append(' '); } sb.append('\n'); } return sb.toString(); } public static int getPositionIndex(int x, int z, int size) { return x * size + z; } public static int xFromIndex(int index, int size) { return index / size; } public static int zFromIndex(int index, int size) { return index % size; } @Nonnull @Override public Component clone() { FloodFillPositionSelector selector = new FloodFillPositionSelector(this.world, this.spawnWrapper); selector.init(); return selector; } public static enum Debug { DISABLED, IRREGULARITIES, ALL; } public static class SortBufferProvider { protected FloodFillPositionSelector.WeightedPosition[] buffer = new FloodFillPositionSelector.WeightedPosition[10]; public FloodFillPositionSelector.WeightedPosition[] getBuffer(int size) { if (size <= this.buffer.length) { return this.buffer; } else { this.buffer = (FloodFillPositionSelector.WeightedPosition[])ObjectArrays.grow(this.buffer, size); return this.buffer; } } } private static class WeightedPosition { @Nonnull private final Vector3i position; private double weight; private WeightedPosition(int x, int y, int z) { this.position = new Vector3i(x, y, z); } public double getWeight() { return this.weight; } } }