Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
408 lines
13 KiB
Java
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();
|
|
}
|
|
}
|