hytale-server/com/hypixel/hytale/server/spawning/util/FloodFillPositionSelector.java

693 lines
28 KiB
Java

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<EntityStore> {
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<FloodFillPositionSelector.SortBufferProvider> sortBufferProvider = ThreadLocal.withInitial(
FloodFillPositionSelector.SortBufferProvider::new
);
private static final Comparator<Object> WEIGHTED_POSITION_COMPARATOR = (entry1, entry2) -> Double.compare(
((FloodFillPositionSelector.WeightedPosition)entry2).getWeight(), ((FloodFillPositionSelector.WeightedPosition)entry1).getWeight()
);
private final World world;
@Nullable
private ChunkAccessor<WorldChunk> 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<BitSet> resolutionMaps;
private final BitSet fullResolutionMap;
private int desiredPositionCount;
private final IntArrayList highResolutionOptions = new IntArrayList(4);
private final Deque<int[]> floodFillQueue = new ArrayDeque<>();
private final SpawningContext spawningContext = new SpawningContext();
@Nullable
private WorldChunk chunk;
private final IntSet positionIndexes = new IntOpenHashSet();
private final Int2ObjectMap<ObjectArrayList<FloodFillPositionSelector.WeightedPosition>> 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<EntityStore, FloodFillPositionSelector> 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<FloodFillPositionSelector.WeightedPosition> positions = (ObjectArrayList<FloodFillPositionSelector.WeightedPosition>)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<ChunkSuppressionEntry> chunkSuppressionMap = suppressionController.getChunkSuppressionMap();
for (int roleIndex : this.roleIndexes) {
Builder<Role> spawnable = NPCPlugin.get().tryGetCachedValidRole(roleIndex);
if (spawnable != null
&& spawnable.isSpawnable()
&& spawnable instanceof ISpawnableWithModel
&& this.spawningContext.setSpawnable((ISpawnableWithModel)spawnable, true)) {
ObjectArrayList<FloodFillPositionSelector.WeightedPosition> positionList = (ObjectArrayList<FloodFillPositionSelector.WeightedPosition>)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<EntityStore> 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;
}
}
}