Files
TiedUp-/src/main/java/com/tiedup/remake/cells/FloodFillAlgorithm.java
NotEvil f6466360b6 Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
2026-04-12 00:51:22 +02:00

408 lines
13 KiB
Java

package com.tiedup.remake.cells;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
/**
* BFS flood-fill algorithm for detecting enclosed rooms around a Cell Core.
*
* Scans outward from air neighbors of the Core block, treating solid blocks
* (including the Core itself) as walls. Picks the smallest successful fill
* as the cell interior (most likely the room, not the hallway).
*/
public final class FloodFillAlgorithm {
static final int MAX_VOLUME = 1200;
static final int MIN_VOLUME = 2;
static final int MAX_X = 12;
static final int MAX_Y = 8;
static final int MAX_Z = 12;
private FloodFillAlgorithm() {}
/**
* Try flood-fill from each air neighbor of the Core position.
* Pick the smallest successful fill (= most likely the cell, not the hallway).
* If none succeed, return a failure result.
*/
public static FloodFillResult tryFill(Level level, BlockPos corePos) {
Set<BlockPos> bestInterior = null;
Direction bestDirection = null;
for (Direction dir : Direction.values()) {
BlockPos neighbor = corePos.relative(dir);
BlockState neighborState = level.getBlockState(neighbor);
if (!isPassable(neighborState)) {
continue;
}
Set<BlockPos> interior = bfs(level, neighbor, corePos);
if (interior == null) {
// Overflow or out of bounds — this direction opens to the outside
continue;
}
if (interior.size() < MIN_VOLUME) {
continue;
}
if (bestInterior == null || interior.size() < bestInterior.size()) {
bestInterior = interior;
bestDirection = dir;
}
}
if (bestInterior == null) {
// No direction produced a valid fill — check why
// Try again to determine the most helpful error message
boolean anyAir = false;
boolean tooLarge = false;
boolean tooSmall = false;
boolean outOfBounds = false;
for (Direction dir : Direction.values()) {
BlockPos neighbor = corePos.relative(dir);
BlockState neighborState = level.getBlockState(neighbor);
if (!isPassable(neighborState)) continue;
anyAir = true;
Set<BlockPos> result = bfsDiagnostic(level, neighbor, corePos);
if (result == null) {
// Overflowed — could be not enclosed or too large
tooLarge = true;
} else if (result.size() < MIN_VOLUME) {
tooSmall = true;
} else {
outOfBounds = true;
}
}
if (!anyAir) {
return FloodFillResult.failure(
"msg.tiedup.cell_core.not_enclosed"
);
} else if (tooLarge) {
// Could be open to outside or genuinely too large
return FloodFillResult.failure(
"msg.tiedup.cell_core.not_enclosed"
);
} else if (outOfBounds) {
return FloodFillResult.failure(
"msg.tiedup.cell_core.out_of_bounds"
);
} else if (tooSmall) {
return FloodFillResult.failure(
"msg.tiedup.cell_core.too_small"
);
} else {
return FloodFillResult.failure(
"msg.tiedup.cell_core.not_enclosed"
);
}
}
// Build walls set
Set<BlockPos> walls = findWalls(level, bestInterior, corePos);
// Detect features
List<BlockPos> beds = new ArrayList<>();
List<BlockPos> petBeds = new ArrayList<>();
List<BlockPos> anchors = new ArrayList<>();
List<BlockPos> doors = new ArrayList<>();
List<BlockPos> linkedRedstone = new ArrayList<>();
detectFeatures(
level,
bestInterior,
walls,
beds,
petBeds,
anchors,
doors,
linkedRedstone
);
return FloodFillResult.success(
bestInterior,
walls,
bestDirection,
beds,
petBeds,
anchors,
doors,
linkedRedstone
);
}
/**
* BFS from start position, treating corePos and solid blocks as walls.
*
* @return The set of interior (passable) positions, or null if the fill
* overflowed MAX_VOLUME or exceeded MAX bounds.
*/
private static Set<BlockPos> bfs(
Level level,
BlockPos start,
BlockPos corePos
) {
Set<BlockPos> visited = new HashSet<>();
Queue<BlockPos> queue = new ArrayDeque<>();
visited.add(start);
queue.add(start);
int minX = start.getX(),
maxX = start.getX();
int minY = start.getY(),
maxY = start.getY();
int minZ = start.getZ(),
maxZ = start.getZ();
while (!queue.isEmpty()) {
BlockPos current = queue.poll();
for (Direction dir : Direction.values()) {
BlockPos next = current.relative(dir);
if (next.equals(corePos)) {
// Core is always treated as wall
continue;
}
if (visited.contains(next)) {
continue;
}
// Treat unloaded chunks as walls to avoid synchronous chunk loading
if (!level.isLoaded(next)) {
continue;
}
BlockState state = level.getBlockState(next);
if (!isPassable(state)) {
// Solid block = wall, don't expand
continue;
}
visited.add(next);
// Check volume
if (visited.size() > MAX_VOLUME) {
return null; // Too large or not enclosed
}
// Update bounds
minX = Math.min(minX, next.getX());
maxX = Math.max(maxX, next.getX());
minY = Math.min(minY, next.getY());
maxY = Math.max(maxY, next.getY());
minZ = Math.min(minZ, next.getZ());
maxZ = Math.max(maxZ, next.getZ());
// Check dimensional bounds
if (
(maxX - minX + 1) > MAX_X ||
(maxY - minY + 1) > MAX_Y ||
(maxZ - minZ + 1) > MAX_Z
) {
return null; // Exceeds max dimensions
}
queue.add(next);
}
}
return visited;
}
/**
* Diagnostic BFS: same as bfs() but returns the set even on bounds overflow
* (returns null only on volume overflow). Used to determine error messages.
*/
private static Set<BlockPos> bfsDiagnostic(
Level level,
BlockPos start,
BlockPos corePos
) {
Set<BlockPos> visited = new HashSet<>();
Queue<BlockPos> queue = new ArrayDeque<>();
visited.add(start);
queue.add(start);
while (!queue.isEmpty()) {
BlockPos current = queue.poll();
for (Direction dir : Direction.values()) {
BlockPos next = current.relative(dir);
if (next.equals(corePos) || visited.contains(next)) {
continue;
}
// Treat unloaded chunks as walls to avoid synchronous chunk loading
if (!level.isLoaded(next)) {
continue;
}
BlockState state = level.getBlockState(next);
if (!isPassable(state)) {
continue;
}
visited.add(next);
if (visited.size() > MAX_VOLUME) {
return null;
}
queue.add(next);
}
}
return visited;
}
/**
* Find all solid blocks adjacent to the interior set (the walls of the cell).
* The Core block itself is always included as a wall.
*/
private static Set<BlockPos> findWalls(
Level level,
Set<BlockPos> interior,
BlockPos corePos
) {
Set<BlockPos> walls = new HashSet<>();
walls.add(corePos);
for (BlockPos pos : interior) {
for (Direction dir : Direction.values()) {
BlockPos neighbor = pos.relative(dir);
if (!interior.contains(neighbor) && !neighbor.equals(corePos)) {
// This is a solid boundary block
walls.add(neighbor);
}
}
}
return walls;
}
/**
* Scan interior and wall blocks to detect notable features.
*/
private static void detectFeatures(
Level level,
Set<BlockPos> interior,
Set<BlockPos> walls,
List<BlockPos> beds,
List<BlockPos> petBeds,
List<BlockPos> anchors,
List<BlockPos> doors,
List<BlockPos> linkedRedstone
) {
// Scan interior for beds and pet beds
for (BlockPos pos : interior) {
BlockState state = level.getBlockState(pos);
Block block = state.getBlock();
if (block instanceof BedBlock) {
// Only count the HEAD part to avoid double-counting (beds are 2 blocks)
if (
state.getValue(BedBlock.PART) ==
net.minecraft.world.level.block.state.properties.BedPart.HEAD
) {
beds.add(pos.immutable());
}
}
// Check for mod's pet bed block
if (block instanceof com.tiedup.remake.v2.blocks.PetBedBlock) {
petBeds.add(pos.immutable());
}
}
// Scan walls for doors, redstone components, and anchors
for (BlockPos pos : walls) {
BlockState state = level.getBlockState(pos);
Block block = state.getBlock();
// Doors, trapdoors, fence gates
if (block instanceof DoorBlock) {
// Only count the lower half to avoid double-counting
if (
state.getValue(DoorBlock.HALF) ==
net.minecraft.world.level.block.state.properties.DoubleBlockHalf.LOWER
) {
doors.add(pos.immutable());
}
} else if (
block instanceof TrapDoorBlock ||
block instanceof FenceGateBlock
) {
doors.add(pos.immutable());
}
// Chain blocks as anchors
if (block instanceof ChainBlock) {
anchors.add(pos.immutable());
}
// Buttons and levers as linked redstone
if (block instanceof ButtonBlock || block instanceof LeverBlock) {
linkedRedstone.add(pos.immutable());
}
}
// Also check for buttons/levers on the interior side adjacent to walls
for (BlockPos pos : interior) {
BlockState state = level.getBlockState(pos);
Block block = state.getBlock();
if (block instanceof ButtonBlock || block instanceof LeverBlock) {
linkedRedstone.add(pos.immutable());
}
}
}
/**
* Determine if a block state is passable for flood-fill purposes.
*
* Air and non-solid blocks (torches, carpets, flowers, signs, etc.) are passable.
* Closed doors block the fill (treated as walls). Open doors let fill through.
* Glass, bars, fences are solid → treated as wall.
*/
private static boolean isPassable(BlockState state) {
if (state.isAir()) {
return true;
}
Block block = state.getBlock();
// Doors are always treated as walls for flood-fill (detected as features separately).
// This prevents the fill from leaking through open doors.
if (
block instanceof DoorBlock ||
block instanceof TrapDoorBlock ||
block instanceof FenceGateBlock
) {
return false;
}
// Beds are interior furniture, not walls.
// BedBlock.isSolid() returns true in 1.20.1 which would misclassify them as walls,
// preventing detectFeatures() from finding them (it only scans interior for beds).
if (block instanceof BedBlock) {
return true;
}
// Non-solid decorative blocks are passable
// This covers torches, carpets, flowers, signs, pressure plates, etc.
return !state.isSolid();
}
}