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.
This commit is contained in:
407
src/main/java/com/tiedup/remake/cells/FloodFillAlgorithm.java
Normal file
407
src/main/java/com/tiedup/remake/cells/FloodFillAlgorithm.java
Normal file
@@ -0,0 +1,407 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user