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 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 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 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 walls = findWalls(level, bestInterior, corePos); // Detect features List beds = new ArrayList<>(); List petBeds = new ArrayList<>(); List anchors = new ArrayList<>(); List doors = new ArrayList<>(); List 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 bfs( Level level, BlockPos start, BlockPos corePos ) { Set visited = new HashSet<>(); Queue 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 bfsDiagnostic( Level level, BlockPos start, BlockPos corePos ) { Set visited = new HashSet<>(); Queue 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 findWalls( Level level, Set interior, BlockPos corePos ) { Set 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 interior, Set walls, List beds, List petBeds, List anchors, List doors, List 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(); } }