parent
8126729ea7
commit
300cb801da
15 changed files with 1076 additions and 798 deletions
@ -1,696 +1,62 @@ |
||||
package xyz.soukup.ecoCraftCore.mines; |
||||
|
||||
import org.bukkit.Bukkit; |
||||
import org.bukkit.Material; |
||||
import org.bukkit.Location; |
||||
import org.bukkit.World; |
||||
import org.bukkit.block.Block; |
||||
import org.bukkit.block.BlockFace; |
||||
|
||||
import java.util.*; |
||||
import xyz.soukup.ecoCraftCore.mines.generation.MineGenerationConfig; |
||||
import xyz.soukup.ecoCraftCore.mines.generation.MineGenerator; |
||||
import xyz.soukup.ecoCraftCore.mines.generation.MineRoomLibrary; |
||||
|
||||
import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; |
||||
|
||||
public class MineManager { |
||||
|
||||
private static final int TUNNEL_RADIUS = 5; // 11x11 = radius 5 from center
|
||||
private static final int MIN_BRANCH_SIZE = 30; |
||||
private static final int FIRST_BRANCH_MIN_SIZE = 20; |
||||
private static final int FIRST_BRANCH_SPLIT_DELAY = 12; |
||||
private static final int MAX_BRANCH_SIZE = 80; |
||||
private static final double BASE_SPLIT_INCREMENT = 0.004; |
||||
private static final double SPLIT_DECAY_PER_BRANCH = 0.0004; |
||||
private static final int BLOCKS_PER_TICK = 800; |
||||
|
||||
// Transition zone: how many blocks before/after a border we blend
|
||||
private static final int TRANSITION_RANGE = 8; |
||||
|
||||
private static final Random random = new Random(); |
||||
|
||||
private static final BlockFace[] DIRECTIONS = { |
||||
BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN, BlockFace.UP |
||||
}; |
||||
|
||||
// --- Biome definitions ---
|
||||
|
||||
public enum MineBiome { |
||||
NORMAL, DEEP, SUPER_DEEP |
||||
} |
||||
|
||||
// Fill progression per biome
|
||||
private static final Material[] NORMAL_FILLS = {Material.STONE, Material.ANDESITE, Material.COBBLESTONE, Material.TUFF}; |
||||
private static final int[] NORMAL_THRESHOLDS = {0, 30, 60, 90}; |
||||
|
||||
private static final Material[] DEEP_FILLS = {Material.DEEPSLATE, Material.COBBLED_DEEPSLATE, Material.CRACKED_DEEPSLATE_TILES}; |
||||
private static final int[] DEEP_THRESHOLDS = {0, 40, 80}; |
||||
|
||||
private static final Material[] SUPER_DEEP_FILLS = {Material.SMOOTH_BASALT, Material.BLACKSTONE, Material.CRACKED_POLISHED_BLACKSTONE_BRICKS, Material.GILDED_BLACKSTONE}; |
||||
private static final int[] SUPER_DEEP_THRESHOLDS = {0, 40, 80, 120}; |
||||
|
||||
// Special inline blocks (placed during generation, not ores)
|
||||
private static final double DIRT_CHANCE = 0.04; |
||||
private static final double GRAVEL_CHANCE = 0.05; |
||||
private static final double INFESTED_CHANCE = 0.008; |
||||
private static final double MUD_CHANCE_SUPER_DEEP = 0.03; |
||||
private static final double LAVA_CHANCE_SUPER_DEEP = 0.05; |
||||
|
||||
// Deep biome lava: starts low and increases through the fills
|
||||
private static final double LAVA_CHANCE_DEEP_MIN = 0.005; |
||||
private static final double LAVA_CHANCE_DEEP_MAX = 0.04; |
||||
|
||||
// --- Ore tables per fill type (rolled on break) ---
|
||||
|
||||
public record OreEntry(Material material, double weight) {} |
||||
|
||||
private static final Map<Material, OreEntry[]> ORE_TABLES = new LinkedHashMap<>(); |
||||
|
||||
static { |
||||
ORE_TABLES.put(Material.STONE, new OreEntry[]{ |
||||
new OreEntry(Material.COPPER_ORE, 1.0), |
||||
}); |
||||
ORE_TABLES.put(Material.ANDESITE, new OreEntry[]{ |
||||
new OreEntry(Material.COAL_ORE, 1.0), |
||||
}); |
||||
ORE_TABLES.put(Material.COBBLESTONE, new OreEntry[]{ |
||||
new OreEntry(Material.IRON_ORE, 1.0), |
||||
}); |
||||
ORE_TABLES.put(Material.TUFF, new OreEntry[]{ |
||||
new OreEntry(Material.GOLD_ORE, 1.0), |
||||
}); |
||||
ORE_TABLES.put(Material.DEEPSLATE, new OreEntry[]{ |
||||
new OreEntry(Material.DEEPSLATE_EMERALD_ORE, 0.4), |
||||
new OreEntry(Material.DEEPSLATE_LAPIS_ORE, 0.6), |
||||
}); |
||||
ORE_TABLES.put(Material.COBBLED_DEEPSLATE, new OreEntry[]{ |
||||
new OreEntry(Material.DEEPSLATE_REDSTONE_ORE, 0.6), |
||||
new OreEntry(Material.RAW_COPPER_BLOCK, 0.4), |
||||
}); |
||||
ORE_TABLES.put(Material.CRACKED_DEEPSLATE_TILES, new OreEntry[]{ |
||||
new OreEntry(Material.DEEPSLATE_DIAMOND_ORE, 0.6), |
||||
new OreEntry(Material.RAW_IRON_BLOCK, 0.4), |
||||
}); |
||||
ORE_TABLES.put(Material.SMOOTH_BASALT, new OreEntry[]{ |
||||
new OreEntry(Material.COAL_BLOCK, 1.0), |
||||
}); |
||||
ORE_TABLES.put(Material.BLACKSTONE, new OreEntry[]{ |
||||
new OreEntry(Material.RAW_GOLD_BLOCK, 1.0), |
||||
}); |
||||
ORE_TABLES.put(Material.CRACKED_POLISHED_BLACKSTONE_BRICKS, new OreEntry[]{ |
||||
new OreEntry(Material.ANCIENT_DEBRIS, 1.0), |
||||
}); |
||||
ORE_TABLES.put(Material.GILDED_BLACKSTONE, new OreEntry[]{ |
||||
new OreEntry(Material.GOLD_BLOCK, 1.0), |
||||
}); |
||||
} |
||||
|
||||
// Ore chance depending on how far this fill is from the detected zone
|
||||
// Index 0 = current fill, 1 = previous fill (one step lighter), 2 = three fills back, 3+ = any other
|
||||
private static final double ORE_CHANCE_CURRENT = 0.05; |
||||
private static final double ORE_CHANCE_PREVIOUS = 0.10; |
||||
private static final double ORE_CHANCE_THREE_BACK = 0.02; |
||||
private static final double ORE_CHANCE_OTHER = 0.01; |
||||
|
||||
// Ordered list of ALL fills from shallowest to deepest (used for distance calculations)
|
||||
private static final List<Material> ALL_FILLS_ORDERED = new ArrayList<>(); |
||||
|
||||
static { |
||||
Collections.addAll(ALL_FILLS_ORDERED, NORMAL_FILLS); |
||||
Collections.addAll(ALL_FILLS_ORDERED, DEEP_FILLS); |
||||
Collections.addAll(ALL_FILLS_ORDERED, SUPER_DEEP_FILLS); |
||||
} |
||||
|
||||
// --- All known fill materials (for checking in MineWorldManager) ---
|
||||
|
||||
private static final Set<Material> ALL_FILL_MATERIALS = new HashSet<>(ALL_FILLS_ORDERED); |
||||
|
||||
public static boolean isFillMaterial(Material material) { |
||||
return ALL_FILL_MATERIALS.contains(material); |
||||
} |
||||
|
||||
// --- Biome Y tracking ---
|
||||
|
||||
private static int normalStartY = 0; |
||||
private static int deepStartY = 0; |
||||
private static int superDeepStartY = 0; |
||||
|
||||
// --- Branch data class ---
|
||||
|
||||
private static class Branch { |
||||
final BlockFace direction; |
||||
final int startX, startY, startZ; |
||||
final int distanceFromStart; |
||||
final int maxSize; |
||||
final boolean isFirst; |
||||
final MineBiome biome; |
||||
double chanceToSplit; |
||||
|
||||
int endX, endY, endZ; |
||||
|
||||
Branch(BlockFace direction, int startX, int startY, int startZ, |
||||
int distanceFromStart, int maxSize, boolean isFirst, MineBiome biome) { |
||||
this.direction = direction; |
||||
this.startX = startX; |
||||
this.startY = startY; |
||||
this.startZ = startZ; |
||||
this.distanceFromStart = distanceFromStart; |
||||
this.maxSize = maxSize; |
||||
this.isFirst = isFirst; |
||||
this.biome = biome; |
||||
this.chanceToSplit = 0; |
||||
this.endX = startX; |
||||
this.endY = startY; |
||||
this.endZ = startZ; |
||||
} |
||||
} |
||||
|
||||
private static class BlockPlacement { |
||||
final int x, y, z; |
||||
final Material material; |
||||
|
||||
BlockPlacement(int x, int y, int z, Material material) { |
||||
this.x = x; |
||||
this.y = y; |
||||
this.z = z; |
||||
this.material = material; |
||||
} |
||||
} |
||||
|
||||
// ===================== MAIN ENTRY POINT =====================
|
||||
|
||||
public static void regenerate(World world) { |
||||
int startY = world.getMaxHeight() - 11; |
||||
int minY = world.getMinHeight() + 5; |
||||
int maxY = world.getMaxHeight(); |
||||
int worldMinY = world.getMinHeight(); |
||||
|
||||
normalStartY = startY; |
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { |
||||
Set<Long> fillPositions = new HashSet<>(); |
||||
List<BlockPlacement> fillPlacements = new ArrayList<>(); |
||||
|
||||
// ===== Biome 1: NORMAL =====
|
||||
plugin.getLogger().info("[MineGen] === Starting NORMAL biome generation ==="); |
||||
List<Branch> normalBranches = new ArrayList<>(); |
||||
int[] branchCount = {0}; |
||||
generateBiomeBranches(MineBiome.NORMAL, 0, startY, 0, startY, minY, |
||||
fillPlacements, fillPositions, normalBranches, branchCount); |
||||
plugin.getLogger().info("[MineGen] NORMAL done: " + fillPlacements.size() + " fill blocks, branches=" + branchCount[0]); |
||||
|
||||
// Find lowest Y from NORMAL
|
||||
Branch lowestNormal = findLowestBranch(normalBranches); |
||||
int deepX = lowestNormal != null ? lowestNormal.endX : 0; |
||||
int deepY = lowestNormal != null ? lowestNormal.endY : startY - 100; |
||||
int deepZ = lowestNormal != null ? lowestNormal.endZ : 0; |
||||
deepStartY = deepY; |
||||
|
||||
// ===== Biome 2: DEEP =====
|
||||
plugin.getLogger().info("[MineGen] === Starting DEEP biome generation at Y=" + deepY + " ==="); |
||||
List<Branch> deepBranches = new ArrayList<>(); |
||||
generateBiomeBranches(MineBiome.DEEP, deepX, deepY, deepZ, deepY, minY, |
||||
fillPlacements, fillPositions, deepBranches, branchCount); |
||||
plugin.getLogger().info("[MineGen] DEEP done: " + fillPlacements.size() + " total fill blocks, branches=" + branchCount[0]); |
||||
|
||||
// Find lowest Y from DEEP
|
||||
Branch lowestDeep = findLowestBranch(deepBranches); |
||||
int sdX = lowestDeep != null ? lowestDeep.endX : deepX; |
||||
int sdY = lowestDeep != null ? lowestDeep.endY : deepY - 100; |
||||
int sdZ = lowestDeep != null ? lowestDeep.endZ : deepZ; |
||||
superDeepStartY = sdY; |
||||
|
||||
// ===== Biome 3: SUPER_DEEP =====
|
||||
plugin.getLogger().info("[MineGen] === Starting SUPER_DEEP biome generation at Y=" + sdY + " ==="); |
||||
List<Branch> superDeepBranches = new ArrayList<>(); |
||||
generateBiomeBranches(MineBiome.SUPER_DEEP, sdX, sdY, sdZ, sdY, minY, |
||||
fillPlacements, fillPositions, superDeepBranches, branchCount); |
||||
plugin.getLogger().info("[MineGen] SUPER_DEEP done: " + fillPlacements.size() + " total fill blocks, branches=" + branchCount[0]); |
||||
|
||||
// ===== Walls =====
|
||||
List<BlockPlacement> wallPlacements = new ArrayList<>(); |
||||
generateWalls(fillPositions, wallPlacements, startY); |
||||
plugin.getLogger().info("[MineGen] Walls done: " + wallPlacements.size() + " bedrock blocks."); |
||||
|
||||
// ===== Schedule placement on main thread =====
|
||||
Bukkit.getScheduler().runTask(plugin, () -> |
||||
scheduleBlockPlacements(world, fillPlacements, wallPlacements, startY, worldMinY, maxY)); |
||||
}); |
||||
} |
||||
|
||||
// ===================== BIOME BRANCH GENERATION =====================
|
||||
|
||||
private static void generateBiomeBranches(MineBiome biome, int startX, int startY, int startZ, |
||||
int ceilingY, int minY, |
||||
List<BlockPlacement> placements, Set<Long> fillPositions, |
||||
List<Branch> completedBranches, int[] branchCount) { |
||||
Deque<Branch> queue = new ArrayDeque<>(); |
||||
|
||||
// Reset branch count for this biome so split chance starts fresh
|
||||
int[] biomeBranchCount = {0}; |
||||
|
||||
int firstSize = Math.max(FIRST_BRANCH_MIN_SIZE, MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE)); |
||||
Branch initial = new Branch(BlockFace.DOWN, startX, startY, startZ, 0, firstSize, true, biome); |
||||
queue.add(initial); |
||||
biomeBranchCount[0]++; |
||||
branchCount[0]++; |
||||
|
||||
plugin.getLogger().info("[MineGen] [" + biome + "] Starting at (" + startX + "," + startY + "," + startZ + "), minY=" + minY); |
||||
|
||||
while (!queue.isEmpty()) { |
||||
Branch branch = queue.pollFirst(); |
||||
double currentIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (biomeBranchCount[0] * SPLIT_DECAY_PER_BRANCH)); |
||||
plugin.getLogger().info("[MineGen] [" + biome + "] Processing branch dir=" + branch.direction |
||||
+ " start=(" + branch.startX + "," + branch.startY + "," + branch.startZ + ")" |
||||
+ " dist=" + branch.distanceFromStart + " maxSize=" + branch.maxSize |
||||
+ " first=" + branch.isFirst |
||||
+ " queued=" + queue.size() + " fillBlocks=" + fillPositions.size() |
||||
+ " splitIncrement=" + String.format("%.5f", currentIncrement)); |
||||
|
||||
generateSingleBranch(branch, queue, placements, fillPositions, ceilingY, minY, biomeBranchCount); |
||||
completedBranches.add(branch); |
||||
} |
||||
|
||||
plugin.getLogger().info("[MineGen] [" + biome + "] Complete. Branches=" + completedBranches.size()); |
||||
} |
||||
|
||||
private static void generateSingleBranch(Branch branch, Deque<Branch> queue, List<BlockPlacement> placements, |
||||
Set<Long> fillPositions, int ceilingY, int minY, int[] branchCount) { |
||||
int cx = branch.startX; |
||||
int cy = branch.startY; |
||||
int cz = branch.startZ; |
||||
|
||||
for (int step = 0; step < branch.maxSize; step++) { |
||||
int distance = branch.distanceFromStart + step; |
||||
|
||||
generateCrossSection(cx, cy, cz, branch.direction, distance, branch.biome, placements, fillPositions); |
||||
|
||||
branch.endX = cx; |
||||
branch.endY = cy; |
||||
branch.endZ = cz; |
||||
|
||||
// First branch: skip split logic for the first 12 blocks
|
||||
if (branch.isFirst && step < FIRST_BRANCH_SPLIT_DELAY) { |
||||
cx += branch.direction.getModX(); |
||||
cy += branch.direction.getModY(); |
||||
cz += branch.direction.getModZ(); |
||||
if (cy < minY || cy > ceilingY + 5) break; |
||||
continue; |
||||
} |
||||
|
||||
double splitIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH)); |
||||
|
||||
if (splitIncrement <= 0) { |
||||
cx += branch.direction.getModX(); |
||||
cy += branch.direction.getModY(); |
||||
cz += branch.direction.getModZ(); |
||||
if (cy < minY || cy > ceilingY + 5) break; |
||||
continue; |
||||
} |
||||
|
||||
branch.chanceToSplit += splitIncrement; |
||||
|
||||
if (random.nextDouble() < branch.chanceToSplit) { |
||||
branch.chanceToSplit = 0; |
||||
|
||||
int splitCount = 1 + random.nextInt(4); |
||||
List<BlockFace> available = getAvailableDirections(branch.direction); |
||||
Collections.shuffle(available, random); |
||||
|
||||
int created = 0; |
||||
for (BlockFace dir : available) { |
||||
if (created >= splitCount) break; |
||||
|
||||
int branchMaxSize = MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE); |
||||
|
||||
if (dir == BlockFace.UP && cy + branchMaxSize > ceilingY) continue; |
||||
if (dir == BlockFace.DOWN && cy - branchMaxSize < minY) continue; |
||||
|
||||
queue.add(new Branch(dir, cx, cy, cz, distance, branchMaxSize, false, branch.biome)); |
||||
branchCount[0]++; |
||||
created++; |
||||
} |
||||
plugin.getLogger().info("[MineGen] [" + branch.biome + "] Split at step=" + step |
||||
+ " pos=(" + cx + "," + cy + "," + cz + ") created=" + created |
||||
+ " total=" + branchCount[0]); |
||||
} |
||||
|
||||
cx += branch.direction.getModX(); |
||||
cy += branch.direction.getModY(); |
||||
cz += branch.direction.getModZ(); |
||||
|
||||
if (cy < minY || cy > ceilingY + 5) break; |
||||
} |
||||
} |
||||
|
||||
private static Branch findLowestBranch(List<Branch> branches) { |
||||
Branch lowest = null; |
||||
for (Branch b : branches) { |
||||
if (lowest == null || b.endY < lowest.endY) { |
||||
lowest = b; |
||||
} |
||||
} |
||||
return lowest; |
||||
} |
||||
|
||||
private static List<BlockFace> getAvailableDirections(BlockFace current) { |
||||
List<BlockFace> dirs = new ArrayList<>(); |
||||
BlockFace opposite = current.getOppositeFace(); |
||||
for (BlockFace dir : DIRECTIONS) { |
||||
if (dir != opposite && dir != current) { |
||||
dirs.add(dir); |
||||
} |
||||
} |
||||
return dirs; |
||||
} |
||||
|
||||
// ===================== CROSS SECTION GENERATION =====================
|
||||
|
||||
private static void generateCrossSection(int cx, int cy, int cz, BlockFace direction, int distance, |
||||
MineBiome biome, List<BlockPlacement> placements, Set<Long> fillPositions) { |
||||
int[][] offsets = getPerpOffsets(direction); |
||||
|
||||
for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { |
||||
for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) { |
||||
int bx = cx + offsets[0][0] * a + offsets[1][0] * b; |
||||
int by = cy + offsets[0][1] * a + offsets[1][1] * b; |
||||
int bz = cz + offsets[0][2] * a + offsets[1][2] * b; |
||||
|
||||
long key = posKey(bx, by, bz); |
||||
if (!fillPositions.add(key)) continue; |
||||
|
||||
Material mat = pickFillForDistance(distance, biome); |
||||
placements.add(new BlockPlacement(bx, by, bz, mat)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ===================== FILL SELECTION (no ores during generation) =====================
|
||||
|
||||
private static Material pickFillForDistance(int distance, MineBiome biome) { |
||||
Material[] fills = getFillsForBiome(biome); |
||||
int[] thresholds = getThresholdsForBiome(biome); |
||||
|
||||
// Determine primary fill
|
||||
Material primaryFill = fills[0]; |
||||
for (int i = thresholds.length - 1; i >= 0; i--) { |
||||
if (distance >= thresholds[i]) { |
||||
primaryFill = fills[i]; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
// Check transition zone
|
||||
Material transitionFill = getTransitionFill(distance, fills, thresholds); |
||||
if (transitionFill != null && transitionFill != primaryFill) { |
||||
int borderDist = getDistanceToBorder(distance, thresholds); |
||||
double blendRatio = 0.5 * (1.0 - (double) Math.abs(borderDist) / TRANSITION_RANGE); |
||||
if (random.nextDouble() < blendRatio) { |
||||
primaryFill = transitionFill; |
||||
} |
||||
} |
||||
|
||||
// Roll for special inline blocks (dirt, gravel, lava, mud, infested)
|
||||
Material special = rollSpecialBlock(biome, primaryFill, distance); |
||||
if (special != null) return special; |
||||
|
||||
return primaryFill; |
||||
} |
||||
public final class MineManager { |
||||
|
||||
private static Material rollSpecialBlock(MineBiome biome, Material fill, int distance) { |
||||
double roll = random.nextDouble(); |
||||
switch (biome) { |
||||
case NORMAL: |
||||
if (roll < DIRT_CHANCE) return Material.DIRT; |
||||
roll -= DIRT_CHANCE; |
||||
if (roll < GRAVEL_CHANCE) return Material.GRAVEL; |
||||
roll -= GRAVEL_CHANCE; |
||||
// Rare infested variants
|
||||
if (roll < INFESTED_CHANCE) { |
||||
return switch (fill) { |
||||
case STONE -> Material.INFESTED_STONE; |
||||
case COBBLESTONE -> Material.INFESTED_COBBLESTONE; |
||||
default -> null; // andesite, tuff don't have infested variants
|
||||
}; |
||||
} |
||||
break; |
||||
case DEEP: |
||||
// Lava in ALL deep fills, chance increases with distance through the biome
|
||||
// Deep thresholds max out at 80+ (cracked_deepslate_tiles)
|
||||
// Total deep distance range is roughly 0-120
|
||||
double deepProgress = Math.min(1.0, distance / 120.0); |
||||
double lavaChance = LAVA_CHANCE_DEEP_MIN + (LAVA_CHANCE_DEEP_MAX - LAVA_CHANCE_DEEP_MIN) * deepProgress; |
||||
if (roll < lavaChance) return Material.LAVA; |
||||
break; |
||||
case SUPER_DEEP: |
||||
if (roll < MUD_CHANCE_SUPER_DEEP) return Material.MUD; |
||||
roll -= MUD_CHANCE_SUPER_DEEP; |
||||
if (roll < LAVA_CHANCE_SUPER_DEEP) return Material.LAVA; |
||||
break; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private static Material getTransitionFill(int distance, Material[] fills, int[] thresholds) { |
||||
for (int i = 1; i < thresholds.length; i++) { |
||||
int border = thresholds[i]; |
||||
int diff = distance - border; |
||||
if (Math.abs(diff) <= TRANSITION_RANGE) { |
||||
return (diff < 0) ? fills[i] : fills[i - 1]; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private static int getDistanceToBorder(int distance, int[] thresholds) { |
||||
int closest = Integer.MAX_VALUE; |
||||
for (int i = 1; i < thresholds.length; i++) { |
||||
int diff = distance - thresholds[i]; |
||||
if (Math.abs(diff) < Math.abs(closest)) { |
||||
closest = diff; |
||||
} |
||||
} |
||||
return closest; |
||||
} |
||||
|
||||
private static Material[] getFillsForBiome(MineBiome biome) { |
||||
return switch (biome) { |
||||
case NORMAL -> NORMAL_FILLS; |
||||
case DEEP -> DEEP_FILLS; |
||||
case SUPER_DEEP -> SUPER_DEEP_FILLS; |
||||
}; |
||||
} |
||||
|
||||
private static int[] getThresholdsForBiome(MineBiome biome) { |
||||
return switch (biome) { |
||||
case NORMAL -> NORMAL_THRESHOLDS; |
||||
case DEEP -> DEEP_THRESHOLDS; |
||||
case SUPER_DEEP -> SUPER_DEEP_THRESHOLDS; |
||||
}; |
||||
private MineManager() { |
||||
} |
||||
|
||||
// ===================== ORE-ON-BREAK SYSTEM (called from MineWorldManager) =====================
|
||||
public static boolean regenerate(World world) { |
||||
MineGenerationConfig generationConfig = MineGenerationConfig.fromConfiguration(plugin.getConfig(), world); |
||||
MineRoomLibrary roomLibrary = MineRoomLibrary.load( |
||||
plugin.getDataFolder(), |
||||
generationConfig.roomsDirectory(), |
||||
plugin.getLogger() |
||||
); |
||||
|
||||
/** |
||||
* Detects the deepest fill material among the 6 surrounding blocks to determine |
||||
* which fill zone the player is in. Then rolls for an ore based on the broken block's |
||||
* position in the fill sequence relative to the detected zone. |
||||
* |
||||
* Ore chances: |
||||
* 5% for ores from the current (detected) fill |
||||
* 10% for ores from the previous fill (one step lighter) |
||||
* 2% for ores from three fills back |
||||
* 1% for any other fill's ores |
||||
* |
||||
* Returns null if no ore was rolled. |
||||
*/ |
||||
public static Material rollOreOnBreak(Block brokenBlock, Material brokenType) { |
||||
// Detect the deepest fill in the 6 surrounding blocks
|
||||
Material detectedFill = detectDeepestSurroundingFill(brokenBlock); |
||||
if (detectedFill == null) { |
||||
// Fallback: use the broken block itself if it's a fill
|
||||
detectedFill = isFillMaterial(brokenType) ? brokenType : Material.STONE; |
||||
if (roomLibrary.isEmpty()) { |
||||
plugin.getLogger().severe("[MineGen] No valid room prefabs were loaded from: " |
||||
+ roomLibrary.roomsDirectory().getAbsolutePath()); |
||||
return false; |
||||
} |
||||
|
||||
int detectedIndex = ALL_FILLS_ORDERED.indexOf(detectedFill); |
||||
int brokenIndex = ALL_FILLS_ORDERED.indexOf(brokenType); |
||||
MineGenerator.Result generationResult = new MineGenerator( |
||||
world, |
||||
generationConfig, |
||||
roomLibrary, |
||||
plugin.getLogger() |
||||
).generate(); |
||||
|
||||
// If the broken block isn't a fill material, no ore roll
|
||||
if (brokenIndex < 0) return null; |
||||
|
||||
// How many steps back from the detected fill is this broken block?
|
||||
// detectedIndex is the deepest (highest index), brokenIndex is where we are in the sequence
|
||||
int stepsBack = detectedIndex - brokenIndex; |
||||
|
||||
// Determine ore chance based on distance from detected fill
|
||||
double chance; |
||||
if (stepsBack == 0) { |
||||
chance = ORE_CHANCE_CURRENT; // 5% - breaking the detected fill itself
|
||||
} else if (stepsBack == 1) { |
||||
chance = ORE_CHANCE_PREVIOUS; // 10% - one step lighter
|
||||
} else if (stepsBack == 2) { |
||||
chance = ORE_CHANCE_THREE_BACK; // 2% - two steps lighter
|
||||
} else { |
||||
chance = ORE_CHANCE_OTHER; // 1% - anything else
|
||||
if (!generationResult.success()) { |
||||
return false; |
||||
} |
||||
|
||||
// Roll for ore
|
||||
if (random.nextDouble() >= chance) return null; |
||||
|
||||
// Pick a random ore from the broken block's ore table
|
||||
OreEntry[] oreTable = ORE_TABLES.get(brokenType); |
||||
if (oreTable == null || oreTable.length == 0) return null; |
||||
Location spawn = getConfiguredSpawnLocation(world); |
||||
world.setSpawnLocation(spawn); |
||||
|
||||
return pickWeightedOre(oreTable); |
||||
} |
||||
|
||||
/** |
||||
* Looks at the 6 blocks surrounding the broken block and finds the deepest fill material. |
||||
*/ |
||||
private static Material detectDeepestSurroundingFill(Block block) { |
||||
Material deepest = null; |
||||
int deepestIndex = -1; |
||||
|
||||
for (BlockFace face : DIRECTIONS) { |
||||
Block neighbor = block.getRelative(face); |
||||
Material type = neighbor.getType(); |
||||
|
||||
int index = ALL_FILLS_ORDERED.indexOf(type); |
||||
if (index > deepestIndex) { |
||||
deepestIndex = index; |
||||
deepest = type; |
||||
} |
||||
plugin.getLogger().info("[MineGen] Mine generated with " + generationResult.roomsPlaced() + " rooms."); |
||||
return true; |
||||
} |
||||
|
||||
return deepest; |
||||
} |
||||
|
||||
/** |
||||
* Picks a random ore from the table using weights. |
||||
*/ |
||||
private static Material pickWeightedOre(OreEntry[] table) { |
||||
double totalWeight = 0; |
||||
for (OreEntry e : table) totalWeight += e.weight; |
||||
|
||||
double roll = random.nextDouble() * totalWeight; |
||||
double cumulative = 0; |
||||
for (OreEntry e : table) { |
||||
cumulative += e.weight; |
||||
if (roll < cumulative) return e.material; |
||||
} |
||||
return table[table.length - 1].material; |
||||
} |
||||
|
||||
// ===================== PERPENDICULAR AXES =====================
|
||||
|
||||
private static int[][] getPerpOffsets(BlockFace dir) { |
||||
return switch (dir) { |
||||
case UP, DOWN -> new int[][]{{1, 0, 0}, {0, 0, 1}}; |
||||
case NORTH, SOUTH -> new int[][]{{1, 0, 0}, {0, 1, 0}}; |
||||
case EAST, WEST -> new int[][]{{0, 0, 1}, {0, 1, 0}}; |
||||
default -> new int[][]{{1, 0, 0}, {0, 0, 1}}; |
||||
}; |
||||
} |
||||
|
||||
// ===================== WALL GENERATION =====================
|
||||
|
||||
private static void generateWalls(Set<Long> fillPositions, List<BlockPlacement> wallPlacements, int startY) { |
||||
Set<Long> wallPositions = new HashSet<>(); |
||||
plugin.getLogger().info("[MineGen] Generating bedrock walls for " + fillPositions.size() + " fill positions..."); |
||||
|
||||
for (long key : fillPositions) { |
||||
int x = decodeX(key); |
||||
int y = decodeY(key); |
||||
int z = decodeZ(key); |
||||
|
||||
for (BlockFace face : DIRECTIONS) { |
||||
int nx = x + face.getModX(); |
||||
int ny = y + face.getModY(); |
||||
int nz = z + face.getModZ(); |
||||
|
||||
long neighborKey = posKey(nx, ny, nz); |
||||
|
||||
if (!fillPositions.contains(neighborKey)) { |
||||
if (ny > startY) continue; |
||||
|
||||
if (wallPositions.add(neighborKey)) { |
||||
wallPlacements.add(new BlockPlacement(nx, ny, nz, Material.BEDROCK)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ===================== BLOCK PLACEMENT SCHEDULING =====================
|
||||
|
||||
private static void scheduleBlockPlacements(World world, List<BlockPlacement> fillPlacements, |
||||
List<BlockPlacement> wallPlacements, |
||||
int startY, int minY, int maxY) { |
||||
// Three-phase placement: 1) Dirt placeholder 2) Bedrock walls 3) Actual fill
|
||||
List<BlockPlacement> allPlacements = new ArrayList<>(); |
||||
|
||||
// Phase A: Dirt placeholders at all fill positions
|
||||
List<BlockPlacement> dirtPlaceholders = new ArrayList<>(fillPlacements.size()); |
||||
for (BlockPlacement bp : fillPlacements) { |
||||
dirtPlaceholders.add(new BlockPlacement(bp.x, bp.y, bp.z, Material.DIRT)); |
||||
} |
||||
allPlacements.addAll(dirtPlaceholders); |
||||
|
||||
// Phase B: Bedrock walls
|
||||
allPlacements.addAll(wallPlacements); |
||||
|
||||
// Phase C: Actual fill (overwrites dirt)
|
||||
allPlacements.addAll(fillPlacements); |
||||
|
||||
// Air blocks above the entrance opening
|
||||
for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { |
||||
for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) { |
||||
allPlacements.add(new BlockPlacement(a, startY + 1, b, Material.AIR)); |
||||
} |
||||
} |
||||
|
||||
int totalBlocks = allPlacements.size(); |
||||
int tickDelay = 0; |
||||
|
||||
plugin.getLogger().info("[MineGen] Scheduling " + totalBlocks + " block placements (" |
||||
+ dirtPlaceholders.size() + " placeholder + " + wallPlacements.size() + " walls + " |
||||
+ fillPlacements.size() + " fill) in batches of " + BLOCKS_PER_TICK); |
||||
|
||||
for (int i = 0; i < totalBlocks; i += BLOCKS_PER_TICK) { |
||||
int from = i; |
||||
int to = Math.min(i + BLOCKS_PER_TICK, totalBlocks); |
||||
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> { |
||||
for (int j = from; j < to; j++) { |
||||
BlockPlacement bp = allPlacements.get(j); |
||||
if (bp.y < minY || bp.y >= maxY) continue; |
||||
world.getBlockAt(bp.x, bp.y, bp.z).setType(bp.material, false); |
||||
} |
||||
}, tickDelay); |
||||
|
||||
tickDelay++; |
||||
} |
||||
|
||||
int finalDelay = tickDelay; |
||||
Bukkit.getScheduler().runTaskLater(plugin, |
||||
() -> plugin.getLogger().info("Mine regeneration complete. " + totalBlocks + " blocks placed."), |
||||
finalDelay + 2); |
||||
} |
||||
|
||||
// ===================== POSITION ENCODING/DECODING =====================
|
||||
|
||||
private static long posKey(int x, int y, int z) { |
||||
return ((long) (x + 30000000) & 0x3FFFFFF) |
||||
| (((long) (y + 2048) & 0xFFF) << 26) |
||||
| (((long) (z + 30000000) & 0x3FFFFFF) << 38); |
||||
} |
||||
|
||||
private static int decodeX(long key) { |
||||
return (int) (key & 0x3FFFFFF) - 30000000; |
||||
} |
||||
public static Location getConfiguredSpawnLocation(World world) { |
||||
Location fallback = new Location(world, 5.5, world.getMaxHeight() - 9, 5.5, 0f, 0f); |
||||
|
||||
private static int decodeY(long key) { |
||||
return (int) ((key >> 26) & 0xFFF) - 2048; |
||||
if (!plugin.getConfig().isConfigurationSection("mine.spawn")) { |
||||
return fallback; |
||||
} |
||||
|
||||
private static int decodeZ(long key) { |
||||
return (int) ((key >> 38) & 0x3FFFFFF) - 30000000; |
||||
double x = plugin.getConfig().getDouble("mine.spawn.x", fallback.getX()); |
||||
double y = plugin.getConfig().getDouble("mine.spawn.y", fallback.getY()); |
||||
double z = plugin.getConfig().getDouble("mine.spawn.z", fallback.getZ()); |
||||
float yaw = (float) plugin.getConfig().getDouble("mine.spawn.yaw", 0.0); |
||||
float pitch = (float) plugin.getConfig().getDouble("mine.spawn.pitch", 0.0); |
||||
return new Location(world, x, y, z, yaw, pitch); |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,50 @@ |
||||
package xyz.soukup.ecoCraftCore.mines.generation; |
||||
|
||||
import java.util.Locale; |
||||
|
||||
public record MineBranchState( |
||||
int branchId, |
||||
boolean mainPath, |
||||
int depth, |
||||
String activeNaturalBiome, |
||||
int biomeOriginDepth, |
||||
int naturalBiomeLength |
||||
) { |
||||
|
||||
public boolean hasActiveNaturalBiome() { |
||||
return activeNaturalBiome != null && !activeNaturalBiome.isBlank(); |
||||
} |
||||
|
||||
public MineBranchState withDepth(int newDepth) { |
||||
return new MineBranchState( |
||||
branchId, |
||||
mainPath, |
||||
newDepth, |
||||
activeNaturalBiome, |
||||
biomeOriginDepth, |
||||
naturalBiomeLength |
||||
); |
||||
} |
||||
|
||||
public MineBranchState clearNaturalBiome() { |
||||
return new MineBranchState( |
||||
branchId, |
||||
mainPath, |
||||
depth, |
||||
null, |
||||
-1, |
||||
0 |
||||
); |
||||
} |
||||
|
||||
public MineBranchState activateNaturalBiome(String biome, int originDepth, int length) { |
||||
return new MineBranchState( |
||||
branchId, |
||||
mainPath, |
||||
depth, |
||||
biome == null ? null : biome.toUpperCase(Locale.ROOT), |
||||
originDepth, |
||||
Math.max(1, length) |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,68 @@ |
||||
package xyz.soukup.ecoCraftCore.mines.generation; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
public enum MineDirection { |
||||
FRONT(1 << 0, 0, 0, 1), |
||||
BACK(1 << 1, 0, 0, -1), |
||||
LEFT(1 << 2, -1, 0, 0), |
||||
RIGHT(1 << 3, 1, 0, 0), |
||||
TOP(1 << 4, 0, 1, 0), |
||||
BOTTOM(1 << 5, 0, -1, 0); |
||||
|
||||
private static final List<MineDirection> HORIZONTAL_DIRECTIONS = List.of(FRONT, BACK, LEFT, RIGHT); |
||||
|
||||
private final int bitMask; |
||||
private final int deltaX; |
||||
private final int deltaY; |
||||
private final int deltaZ; |
||||
|
||||
MineDirection(int bitMask, int deltaX, int deltaY, int deltaZ) { |
||||
this.bitMask = bitMask; |
||||
this.deltaX = deltaX; |
||||
this.deltaY = deltaY; |
||||
this.deltaZ = deltaZ; |
||||
} |
||||
|
||||
public int bitMask() { |
||||
return bitMask; |
||||
} |
||||
|
||||
public int deltaX() { |
||||
return deltaX; |
||||
} |
||||
|
||||
public int deltaY() { |
||||
return deltaY; |
||||
} |
||||
|
||||
public int deltaZ() { |
||||
return deltaZ; |
||||
} |
||||
|
||||
public MineDirection opposite() { |
||||
return switch (this) { |
||||
case FRONT -> BACK; |
||||
case BACK -> FRONT; |
||||
case LEFT -> RIGHT; |
||||
case RIGHT -> LEFT; |
||||
case TOP -> BOTTOM; |
||||
case BOTTOM -> TOP; |
||||
}; |
||||
} |
||||
|
||||
public static List<MineDirection> horizontalDirections() { |
||||
return HORIZONTAL_DIRECTIONS; |
||||
} |
||||
|
||||
public static List<MineDirection> directionsFromMask(int exitsMask) { |
||||
List<MineDirection> directions = new ArrayList<>(); |
||||
for (MineDirection direction : values()) { |
||||
if ((exitsMask & direction.bitMask) != 0) { |
||||
directions.add(direction); |
||||
} |
||||
} |
||||
return directions; |
||||
} |
||||
} |
||||
@ -0,0 +1,191 @@ |
||||
package xyz.soukup.ecoCraftCore.mines.generation; |
||||
|
||||
import com.sk89q.worldedit.math.BlockVector3; |
||||
import org.bukkit.World; |
||||
import org.bukkit.configuration.file.FileConfiguration; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Comparator; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
import java.util.Map; |
||||
import java.util.regex.Pattern; |
||||
|
||||
public record MineGenerationConfig( |
||||
String roomsDirectory, |
||||
String starterRoomId, |
||||
String defaultBiome, |
||||
int roomSize, |
||||
int maxDepth, |
||||
int maxRooms, |
||||
boolean ignoreAir, |
||||
BlockVector3 origin, |
||||
List<DepthBiomeRule> depthBiomes, |
||||
List<NaturalBiomeRule> naturalBiomes |
||||
) { |
||||
|
||||
private static final String BASE_PATH = "mine.generator"; |
||||
private static final Pattern ROOM_IDENTIFIER_PATTERN = Pattern.compile("^[A-Z]{2}\\d+$"); |
||||
|
||||
public static MineGenerationConfig fromConfiguration(FileConfiguration configuration, World world) { |
||||
String roomsDirectory = configuration.getString(BASE_PATH + ".rooms-directory", "rooms"); |
||||
String starterRoomId = normalizeRoomIdentifier(configuration.getString(BASE_PATH + ".starter-room", "ST15")); |
||||
String defaultBiome = normalizeBiome(configuration.getString(BASE_PATH + ".default-biome", "ST"), "ST"); |
||||
|
||||
int roomSize = Math.max(1, configuration.getInt(BASE_PATH + ".room-size", 16)); |
||||
int maxDepth = Math.max(1, configuration.getInt(BASE_PATH + ".max-depth", 64)); |
||||
int maxRooms = Math.max(1, configuration.getInt(BASE_PATH + ".max-rooms", 320)); |
||||
boolean ignoreAir = configuration.getBoolean(BASE_PATH + ".ignore-air", false); |
||||
|
||||
int defaultY = world.getMaxHeight() - 10; |
||||
BlockVector3 origin = BlockVector3.at( |
||||
configuration.getInt(BASE_PATH + ".origin.x", 0), |
||||
configuration.getInt(BASE_PATH + ".origin.y", defaultY), |
||||
configuration.getInt(BASE_PATH + ".origin.z", 0) |
||||
); |
||||
|
||||
List<DepthBiomeRule> depthBiomes = parseDepthBiomes( |
||||
configuration.getMapList(BASE_PATH + ".depth-biomes"), |
||||
defaultBiome |
||||
); |
||||
List<NaturalBiomeRule> naturalBiomes = parseNaturalBiomes( |
||||
configuration.getMapList(BASE_PATH + ".natural-biomes") |
||||
); |
||||
|
||||
return new MineGenerationConfig( |
||||
roomsDirectory, |
||||
starterRoomId, |
||||
defaultBiome, |
||||
roomSize, |
||||
maxDepth, |
||||
maxRooms, |
||||
ignoreAir, |
||||
origin, |
||||
depthBiomes, |
||||
naturalBiomes |
||||
); |
||||
} |
||||
|
||||
public String resolveDepthBiome(int depth) { |
||||
String biome = defaultBiome; |
||||
for (DepthBiomeRule rule : depthBiomes) { |
||||
if (depth >= rule.minDepth()) { |
||||
biome = rule.biome(); |
||||
} else { |
||||
break; |
||||
} |
||||
} |
||||
return biome; |
||||
} |
||||
|
||||
private static List<DepthBiomeRule> parseDepthBiomes(List<Map<?, ?>> entries, String defaultBiome) { |
||||
List<DepthBiomeRule> rules = new ArrayList<>(); |
||||
for (Map<?, ?> entry : entries) { |
||||
String biome = normalizeBiome(entry.get("biome"), null); |
||||
if (biome == null) { |
||||
continue; |
||||
} |
||||
int minDepth = Math.max(0, readInt(entry, "min-depth", 0)); |
||||
rules.add(new DepthBiomeRule(biome, minDepth)); |
||||
} |
||||
|
||||
if (rules.isEmpty()) { |
||||
rules.add(new DepthBiomeRule(defaultBiome, 0)); |
||||
} |
||||
|
||||
rules.sort(Comparator.comparingInt(DepthBiomeRule::minDepth)); |
||||
return List.copyOf(rules); |
||||
} |
||||
|
||||
private static List<NaturalBiomeRule> parseNaturalBiomes(List<Map<?, ?>> entries) { |
||||
List<NaturalBiomeRule> rules = new ArrayList<>(); |
||||
for (Map<?, ?> entry : entries) { |
||||
String biome = normalizeBiome(entry.get("biome"), null); |
||||
if (biome == null) { |
||||
continue; |
||||
} |
||||
|
||||
double chance = clamp(readDouble(entry, "chance", 0.0), 0.0, 1.0); |
||||
if (chance <= 0.0) { |
||||
continue; |
||||
} |
||||
|
||||
int length = Math.max(1, readInt(entry, "length", 1)); |
||||
rules.add(new NaturalBiomeRule(biome, chance, length)); |
||||
} |
||||
|
||||
return List.copyOf(rules); |
||||
} |
||||
|
||||
private static int readInt(Map<?, ?> entry, String key, int fallback) { |
||||
Object value = entry.get(key); |
||||
if (value instanceof Number number) { |
||||
return number.intValue(); |
||||
} |
||||
if (value instanceof String stringValue) { |
||||
try { |
||||
return Integer.parseInt(stringValue); |
||||
} catch (NumberFormatException ignored) { |
||||
return fallback; |
||||
} |
||||
} |
||||
return fallback; |
||||
} |
||||
|
||||
private static double readDouble(Map<?, ?> entry, String key, double fallback) { |
||||
Object value = entry.get(key); |
||||
if (value instanceof Number number) { |
||||
return number.doubleValue(); |
||||
} |
||||
if (value instanceof String stringValue) { |
||||
try { |
||||
return Double.parseDouble(stringValue); |
||||
} catch (NumberFormatException ignored) { |
||||
return fallback; |
||||
} |
||||
} |
||||
return fallback; |
||||
} |
||||
|
||||
private static double clamp(double value, double min, double max) { |
||||
if (value < min) { |
||||
return min; |
||||
} |
||||
if (value > max) { |
||||
return max; |
||||
} |
||||
return value; |
||||
} |
||||
|
||||
private static String normalizeRoomIdentifier(String rawValue) { |
||||
if (rawValue == null || rawValue.isBlank()) { |
||||
return "ST15"; |
||||
} |
||||
|
||||
String normalized = rawValue.trim().toUpperCase(Locale.ROOT); |
||||
int extensionIndex = normalized.indexOf('.'); |
||||
if (extensionIndex > 0) { |
||||
normalized = normalized.substring(0, extensionIndex); |
||||
} |
||||
|
||||
if (!ROOM_IDENTIFIER_PATTERN.matcher(normalized).matches()) { |
||||
return "ST15"; |
||||
} |
||||
|
||||
return normalized; |
||||
} |
||||
|
||||
private static String normalizeBiome(Object rawValue, String fallback) { |
||||
if (!(rawValue instanceof String biome) || biome.isBlank()) { |
||||
return fallback; |
||||
} |
||||
|
||||
return biome.trim().toUpperCase(Locale.ROOT); |
||||
} |
||||
|
||||
public record DepthBiomeRule(String biome, int minDepth) { |
||||
} |
||||
|
||||
public record NaturalBiomeRule(String biome, double chance, int length) { |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
package xyz.soukup.ecoCraftCore.mines.generation; |
||||
|
||||
public record MineGenerationPoint( |
||||
MineGridPosition gridPosition, |
||||
MineDirection incomingDirection, |
||||
MineDirection travelDirection, |
||||
MineBranchState branchState |
||||
) { |
||||
} |
||||
@ -0,0 +1,376 @@ |
||||
package xyz.soukup.ecoCraftCore.mines.generation; |
||||
|
||||
import com.sk89q.worldedit.EditSession; |
||||
import com.sk89q.worldedit.WorldEdit; |
||||
import com.sk89q.worldedit.WorldEditException; |
||||
import com.sk89q.worldedit.bukkit.BukkitAdapter; |
||||
import com.sk89q.worldedit.function.operation.Operation; |
||||
import com.sk89q.worldedit.function.operation.Operations; |
||||
import com.sk89q.worldedit.math.BlockVector3; |
||||
import com.sk89q.worldedit.session.ClipboardHolder; |
||||
import org.bukkit.World; |
||||
|
||||
import java.util.ArrayDeque; |
||||
import java.util.ArrayList; |
||||
import java.util.Deque; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Random; |
||||
import java.util.concurrent.ThreadLocalRandom; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
public final class MineGenerator { |
||||
|
||||
private static final MineGridPosition STARTER_GRID_POSITION = new MineGridPosition(0, 0, 0); |
||||
|
||||
private final World world; |
||||
private final MineGenerationConfig config; |
||||
private final MineRoomLibrary roomLibrary; |
||||
private final Logger logger; |
||||
private final Random random; |
||||
|
||||
private int nextBranchId = 1; |
||||
|
||||
public MineGenerator(World world, MineGenerationConfig config, MineRoomLibrary roomLibrary, Logger logger) { |
||||
this.world = world; |
||||
this.config = config; |
||||
this.roomLibrary = roomLibrary; |
||||
this.logger = logger; |
||||
this.random = ThreadLocalRandom.current(); |
||||
} |
||||
|
||||
public Result generate() { |
||||
List<MineRoomPrefab> starterCandidates = roomLibrary.roomsForIdentifier(config.starterRoomId()); |
||||
if (starterCandidates.isEmpty()) { |
||||
logger.severe("[MineGen] Starter room '" + config.starterRoomId() + "' was not found in room prefabs."); |
||||
return Result.failure(); |
||||
} |
||||
|
||||
MineRoomPrefab starterRoom = chooseWeighted(starterCandidates); |
||||
if (starterRoom == null) { |
||||
logger.severe("[MineGen] Starter room selection failed."); |
||||
return Result.failure(); |
||||
} |
||||
|
||||
if (!hasAllHorizontalExits(starterRoom)) { |
||||
logger.severe("[MineGen] Starter room '" + starterRoom.sourceFile().getName() |
||||
+ "' must contain FRONT, BACK, LEFT and RIGHT exits."); |
||||
return Result.failure(); |
||||
} |
||||
|
||||
Map<MineGridPosition, MineRoomPrefab> occupiedPositions = new HashMap<>(); |
||||
Deque<MineGenerationPoint> generationQueue = new ArrayDeque<>(); |
||||
int roomsPlaced = 0; |
||||
|
||||
com.sk89q.worldedit.world.World worldEditWorld = BukkitAdapter.adapt(world); |
||||
try (EditSession editSession = WorldEdit.getInstance().newEditSession(worldEditWorld)) { |
||||
pasteRoom(editSession, starterRoom, STARTER_GRID_POSITION); |
||||
occupiedPositions.put(STARTER_GRID_POSITION, starterRoom); |
||||
roomsPlaced++; |
||||
|
||||
for (MineDirection direction : MineDirection.horizontalDirections()) { |
||||
MineBranchState branchState = new MineBranchState(nextBranchId++, true, 1, null, -1, 0); |
||||
enqueueNextPoint(generationQueue, STARTER_GRID_POSITION, direction, branchState); |
||||
} |
||||
|
||||
while (!generationQueue.isEmpty() && roomsPlaced < config.maxRooms()) { |
||||
MineGenerationPoint point = generationQueue.poll(); |
||||
MineBranchState branchState = expireNaturalBiomeIfNeeded(point.branchState()); |
||||
|
||||
if (branchState.depth() > config.maxDepth()) { |
||||
continue; |
||||
} |
||||
if (occupiedPositions.containsKey(point.gridPosition())) { |
||||
continue; |
||||
} |
||||
|
||||
MineBranchState placementState = maybeActivateNaturalBiome(branchState); |
||||
String resolvedBiome = resolveBiome(placementState); |
||||
|
||||
MineRoomPrefab selectedRoom = selectRoom( |
||||
point, |
||||
placementState, |
||||
resolvedBiome, |
||||
occupiedPositions |
||||
); |
||||
if (selectedRoom == null) { |
||||
continue; |
||||
} |
||||
|
||||
pasteRoom(editSession, selectedRoom, point.gridPosition()); |
||||
occupiedPositions.put(point.gridPosition(), selectedRoom); |
||||
roomsPlaced++; |
||||
|
||||
enqueueFollowingPoints( |
||||
generationQueue, |
||||
point, |
||||
placementState, |
||||
selectedRoom |
||||
); |
||||
} |
||||
} catch (WorldEditException exception) { |
||||
logger.log(Level.SEVERE, "[MineGen] Failed to generate mine layout.", exception); |
||||
return Result.failure(); |
||||
} |
||||
|
||||
return new Result(true, roomsPlaced); |
||||
} |
||||
|
||||
private String resolveBiome(MineBranchState branchState) { |
||||
if (branchState.hasActiveNaturalBiome()) { |
||||
return branchState.activeNaturalBiome(); |
||||
} |
||||
return config.resolveDepthBiome(branchState.depth()); |
||||
} |
||||
|
||||
private MineRoomPrefab selectRoom( |
||||
MineGenerationPoint point, |
||||
MineBranchState branchState, |
||||
String biome, |
||||
Map<MineGridPosition, MineRoomPrefab> occupiedPositions |
||||
) { |
||||
List<MineRoomPrefab> candidates = collectCandidateRooms( |
||||
point, |
||||
branchState, |
||||
biome, |
||||
occupiedPositions, |
||||
true |
||||
); |
||||
|
||||
if (candidates.isEmpty()) { |
||||
candidates = collectCandidateRooms( |
||||
point, |
||||
branchState, |
||||
biome, |
||||
occupiedPositions, |
||||
false |
||||
); |
||||
} |
||||
|
||||
if (candidates.isEmpty()) { |
||||
return null; |
||||
} |
||||
|
||||
return chooseWeighted(candidates); |
||||
} |
||||
|
||||
private List<MineRoomPrefab> collectCandidateRooms( |
||||
MineGenerationPoint point, |
||||
MineBranchState branchState, |
||||
String biome, |
||||
Map<MineGridPosition, MineRoomPrefab> occupiedPositions, |
||||
boolean avoidOccupiedExitTargets |
||||
) { |
||||
int blockedExitMask = buildBlockedExitMask(point.gridPosition(), point.incomingDirection(), occupiedPositions); |
||||
|
||||
List<MineRoomPrefab> candidates = new ArrayList<>(); |
||||
for (MineRoomPrefab room : roomLibrary.allRooms()) { |
||||
if (!room.biome().equalsIgnoreCase(biome)) { |
||||
continue; |
||||
} |
||||
if (!room.hasExit(point.incomingDirection())) { |
||||
continue; |
||||
} |
||||
if (!matchesBranchConstraints(room, branchState.mainPath())) { |
||||
continue; |
||||
} |
||||
if (avoidOccupiedExitTargets && (room.exitsMask() & blockedExitMask) != 0) { |
||||
continue; |
||||
} |
||||
candidates.add(room); |
||||
} |
||||
|
||||
return candidates; |
||||
} |
||||
|
||||
private boolean matchesBranchConstraints(MineRoomPrefab room, boolean mainPath) { |
||||
int exitCount = room.exitCount(); |
||||
if (mainPath) { |
||||
return exitCount >= 2; |
||||
} |
||||
return exitCount == 2; |
||||
} |
||||
|
||||
private int buildBlockedExitMask( |
||||
MineGridPosition targetPosition, |
||||
MineDirection incomingDirection, |
||||
Map<MineGridPosition, MineRoomPrefab> occupiedPositions |
||||
) { |
||||
int blockedMask = 0; |
||||
for (MineDirection direction : MineDirection.values()) { |
||||
if (direction == incomingDirection) { |
||||
continue; |
||||
} |
||||
if (occupiedPositions.containsKey(targetPosition.offset(direction))) { |
||||
blockedMask |= direction.bitMask(); |
||||
} |
||||
} |
||||
return blockedMask; |
||||
} |
||||
|
||||
private void enqueueFollowingPoints( |
||||
Deque<MineGenerationPoint> generationQueue, |
||||
MineGenerationPoint point, |
||||
MineBranchState placementState, |
||||
MineRoomPrefab room |
||||
) { |
||||
List<MineDirection> outgoingDirections = room.exits().stream() |
||||
.filter(direction -> direction != point.incomingDirection()) |
||||
.toList(); |
||||
|
||||
if (outgoingDirections.isEmpty()) { |
||||
return; |
||||
} |
||||
|
||||
MineDirection continuationDirection = chooseContinuationDirection(outgoingDirections, point.travelDirection()); |
||||
MineBranchState continuationState = placementState.withDepth(placementState.depth() + 1); |
||||
enqueueNextPoint(generationQueue, point.gridPosition(), continuationDirection, continuationState); |
||||
|
||||
if (!placementState.mainPath()) { |
||||
return; |
||||
} |
||||
|
||||
for (MineDirection direction : outgoingDirections) { |
||||
if (direction == continuationDirection) { |
||||
continue; |
||||
} |
||||
|
||||
MineBranchState childBranchState = new MineBranchState( |
||||
nextBranchId++, |
||||
false, |
||||
placementState.depth() + 1, |
||||
placementState.activeNaturalBiome(), |
||||
placementState.biomeOriginDepth(), |
||||
placementState.naturalBiomeLength() |
||||
); |
||||
enqueueNextPoint(generationQueue, point.gridPosition(), direction, childBranchState); |
||||
} |
||||
} |
||||
|
||||
private MineDirection chooseContinuationDirection(List<MineDirection> outgoingDirections, MineDirection preferredDirection) { |
||||
if (outgoingDirections.contains(preferredDirection)) { |
||||
return preferredDirection; |
||||
} |
||||
return outgoingDirections.get(random.nextInt(outgoingDirections.size())); |
||||
} |
||||
|
||||
private MineBranchState expireNaturalBiomeIfNeeded(MineBranchState branchState) { |
||||
if (!branchState.hasActiveNaturalBiome()) { |
||||
return branchState; |
||||
} |
||||
|
||||
int roomsSpentInBiome = branchState.depth() - branchState.biomeOriginDepth(); |
||||
if (roomsSpentInBiome >= branchState.naturalBiomeLength()) { |
||||
return branchState.clearNaturalBiome(); |
||||
} |
||||
return branchState; |
||||
} |
||||
|
||||
private MineBranchState maybeActivateNaturalBiome(MineBranchState branchState) { |
||||
if (branchState.hasActiveNaturalBiome()) { |
||||
return branchState; |
||||
} |
||||
if (config.naturalBiomes().isEmpty()) { |
||||
return branchState; |
||||
} |
||||
|
||||
double totalChance = 0.0; |
||||
for (MineGenerationConfig.NaturalBiomeRule rule : config.naturalBiomes()) { |
||||
totalChance += rule.chance(); |
||||
} |
||||
|
||||
if (totalChance <= 0.0) { |
||||
return branchState; |
||||
} |
||||
|
||||
if (random.nextDouble() > Math.min(1.0, totalChance)) { |
||||
return branchState; |
||||
} |
||||
|
||||
double selectionRoll = random.nextDouble(totalChance); |
||||
double cumulativeChance = 0.0; |
||||
for (MineGenerationConfig.NaturalBiomeRule rule : config.naturalBiomes()) { |
||||
cumulativeChance += rule.chance(); |
||||
if (selectionRoll <= cumulativeChance) { |
||||
return branchState.activateNaturalBiome(rule.biome(), branchState.depth(), rule.length()); |
||||
} |
||||
} |
||||
|
||||
return branchState; |
||||
} |
||||
|
||||
private boolean hasAllHorizontalExits(MineRoomPrefab room) { |
||||
for (MineDirection direction : MineDirection.horizontalDirections()) { |
||||
if (!room.hasExit(direction)) { |
||||
return false; |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
private static void enqueueNextPoint( |
||||
Deque<MineGenerationPoint> generationQueue, |
||||
MineGridPosition currentPosition, |
||||
MineDirection outgoingDirection, |
||||
MineBranchState branchState |
||||
) { |
||||
generationQueue.add(new MineGenerationPoint( |
||||
currentPosition.offset(outgoingDirection), |
||||
outgoingDirection.opposite(), |
||||
outgoingDirection, |
||||
branchState |
||||
)); |
||||
} |
||||
|
||||
private MineRoomPrefab chooseWeighted(List<MineRoomPrefab> rooms) { |
||||
if (rooms.isEmpty()) { |
||||
return null; |
||||
} |
||||
if (rooms.size() == 1) { |
||||
return rooms.getFirst(); |
||||
} |
||||
|
||||
int totalWeight = 0; |
||||
for (MineRoomPrefab room : rooms) { |
||||
totalWeight += Math.max(1, room.weight()); |
||||
} |
||||
|
||||
int roll = random.nextInt(totalWeight); |
||||
int runningWeight = 0; |
||||
for (MineRoomPrefab room : rooms) { |
||||
runningWeight += Math.max(1, room.weight()); |
||||
if (roll < runningWeight) { |
||||
return room; |
||||
} |
||||
} |
||||
|
||||
return rooms.getLast(); |
||||
} |
||||
|
||||
private void pasteRoom(EditSession editSession, MineRoomPrefab room, MineGridPosition gridPosition) throws WorldEditException { |
||||
BlockVector3 target = toWorldPosition(gridPosition); |
||||
Operation operation = new ClipboardHolder(room.clipboard()) |
||||
.createPaste(editSession) |
||||
.to(target) |
||||
.ignoreAirBlocks(config.ignoreAir()) |
||||
.build(); |
||||
Operations.complete(operation); |
||||
} |
||||
|
||||
private BlockVector3 toWorldPosition(MineGridPosition gridPosition) { |
||||
return config.origin().add( |
||||
gridPosition.x() * config.roomSize(), |
||||
gridPosition.y() * config.roomSize(), |
||||
gridPosition.z() * config.roomSize() |
||||
); |
||||
} |
||||
|
||||
public record Result(boolean success, int roomsPlaced) { |
||||
|
||||
public static Result failure() { |
||||
return new Result(false, 0); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
package xyz.soukup.ecoCraftCore.mines.generation; |
||||
|
||||
public record MineGridPosition(int x, int y, int z) { |
||||
|
||||
public MineGridPosition offset(MineDirection direction) { |
||||
return new MineGridPosition( |
||||
x + direction.deltaX(), |
||||
y + direction.deltaY(), |
||||
z + direction.deltaZ() |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,181 @@ |
||||
package xyz.soukup.ecoCraftCore.mines.generation; |
||||
|
||||
import com.sk89q.worldedit.extent.clipboard.Clipboard; |
||||
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; |
||||
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; |
||||
import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; |
||||
|
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Locale; |
||||
import java.util.Map; |
||||
import java.util.logging.Logger; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
import java.util.stream.Collectors; |
||||
|
||||
public final class MineRoomLibrary { |
||||
|
||||
private static final Pattern ROOM_FILE_PATTERN = Pattern.compile("^([A-Za-z]{2})(\\d+)(?:_(\\d+))?$"); |
||||
|
||||
private final File roomsDirectory; |
||||
private final List<MineRoomPrefab> prefabs; |
||||
private final Map<String, List<MineRoomPrefab>> prefabsByIdentifier; |
||||
|
||||
private MineRoomLibrary( |
||||
File roomsDirectory, |
||||
List<MineRoomPrefab> prefabs, |
||||
Map<String, List<MineRoomPrefab>> prefabsByIdentifier |
||||
) { |
||||
this.roomsDirectory = roomsDirectory; |
||||
this.prefabs = prefabs; |
||||
this.prefabsByIdentifier = prefabsByIdentifier; |
||||
} |
||||
|
||||
public static MineRoomLibrary load(File dataFolder, String roomsDirectoryName, Logger logger) { |
||||
String safeDirectoryName = roomsDirectoryName == null || roomsDirectoryName.isBlank() |
||||
? "rooms" |
||||
: roomsDirectoryName; |
||||
|
||||
File roomsDirectory = new File(dataFolder, safeDirectoryName); |
||||
if (!roomsDirectory.exists() && !roomsDirectory.mkdirs()) { |
||||
logger.warning("[MineGen] Could not create room prefab directory: " + roomsDirectory.getAbsolutePath()); |
||||
} |
||||
|
||||
File[] roomFiles = roomsDirectory.listFiles(MineRoomLibrary::isSupportedRoomFile); |
||||
if (roomFiles == null || roomFiles.length == 0) { |
||||
return new MineRoomLibrary(roomsDirectory, List.of(), Map.of()); |
||||
} |
||||
|
||||
List<MineRoomPrefab> prefabs = new ArrayList<>(); |
||||
Map<String, List<MineRoomPrefab>> prefabsByIdentifier = new HashMap<>(); |
||||
|
||||
for (File roomFile : roomFiles) { |
||||
RoomFileMetadata metadata = parseMetadata(roomFile.getName(), logger); |
||||
if (metadata == null) { |
||||
continue; |
||||
} |
||||
|
||||
ClipboardFormat format = ClipboardFormats.findByFile(roomFile); |
||||
if (format == null) { |
||||
logger.warning("[MineGen] Unsupported room format: " + roomFile.getName()); |
||||
continue; |
||||
} |
||||
|
||||
try ( |
||||
FileInputStream inputStream = new FileInputStream(roomFile); |
||||
ClipboardReader reader = format.getReader(inputStream) |
||||
) { |
||||
Clipboard clipboard = reader.read(); |
||||
MineRoomPrefab prefab = new MineRoomPrefab( |
||||
metadata.identifier(), |
||||
metadata.biome(), |
||||
metadata.exitsMask(), |
||||
metadata.weight(), |
||||
roomFile, |
||||
clipboard |
||||
); |
||||
prefabs.add(prefab); |
||||
prefabsByIdentifier.computeIfAbsent(metadata.identifier(), ignored -> new ArrayList<>()).add(prefab); |
||||
} catch (IOException exception) { |
||||
logger.warning("[MineGen] Failed to load room prefab " + roomFile.getName() |
||||
+ ": " + exception.getMessage()); |
||||
} |
||||
} |
||||
|
||||
return new MineRoomLibrary( |
||||
roomsDirectory, |
||||
List.copyOf(prefabs), |
||||
prefabsByIdentifier.entrySet().stream() |
||||
.collect(Collectors.toUnmodifiableMap( |
||||
Map.Entry::getKey, |
||||
entry -> List.copyOf(entry.getValue()) |
||||
)) |
||||
); |
||||
} |
||||
|
||||
public File roomsDirectory() { |
||||
return roomsDirectory; |
||||
} |
||||
|
||||
public boolean isEmpty() { |
||||
return prefabs.isEmpty(); |
||||
} |
||||
|
||||
public List<MineRoomPrefab> allRooms() { |
||||
return prefabs; |
||||
} |
||||
|
||||
public List<MineRoomPrefab> roomsForIdentifier(String identifier) { |
||||
if (identifier == null) { |
||||
return List.of(); |
||||
} |
||||
|
||||
return prefabsByIdentifier.getOrDefault(identifier.toUpperCase(Locale.ROOT), List.of()); |
||||
} |
||||
|
||||
private static boolean isSupportedRoomFile(File file) { |
||||
if (!file.isFile()) { |
||||
return false; |
||||
} |
||||
|
||||
String lowerName = file.getName().toLowerCase(Locale.ROOT); |
||||
return lowerName.endsWith(".schematic") || lowerName.endsWith(".schem"); |
||||
} |
||||
|
||||
private static RoomFileMetadata parseMetadata(String fileName, Logger logger) { |
||||
int extensionIndex = fileName.lastIndexOf('.'); |
||||
if (extensionIndex <= 0) { |
||||
logger.warning("[MineGen] Ignoring room prefab '" + fileName + "': missing file extension."); |
||||
return null; |
||||
} |
||||
|
||||
String baseName = fileName.substring(0, extensionIndex); |
||||
Matcher matcher = ROOM_FILE_PATTERN.matcher(baseName); |
||||
if (!matcher.matches()) { |
||||
logger.warning("[MineGen] Ignoring room prefab '" + fileName |
||||
+ "': expected format <room_biome><room_exits>_<room_weight>."); |
||||
return null; |
||||
} |
||||
|
||||
String biome = matcher.group(1).toUpperCase(Locale.ROOT); |
||||
int exitsMask; |
||||
try { |
||||
exitsMask = Integer.parseInt(matcher.group(2)); |
||||
} catch (NumberFormatException exception) { |
||||
logger.warning("[MineGen] Ignoring room prefab '" + fileName + "': invalid exits value."); |
||||
return null; |
||||
} |
||||
|
||||
if (exitsMask <= 0 || exitsMask > 63) { |
||||
logger.warning("[MineGen] Ignoring room prefab '" + fileName |
||||
+ "': exits bitmask must be in range 1..63."); |
||||
return null; |
||||
} |
||||
|
||||
int weight = 1; |
||||
String weightGroup = matcher.group(3); |
||||
if (weightGroup != null) { |
||||
try { |
||||
weight = Integer.parseInt(weightGroup); |
||||
if (weight <= 0) { |
||||
logger.warning("[MineGen] Room prefab '" + fileName + "' has non-positive weight, using 1."); |
||||
weight = 1; |
||||
} |
||||
} catch (NumberFormatException exception) { |
||||
logger.warning("[MineGen] Room prefab '" + fileName + "' has invalid weight, using 1."); |
||||
weight = 1; |
||||
} |
||||
} |
||||
|
||||
String identifier = biome + exitsMask; |
||||
return new RoomFileMetadata(identifier, biome, exitsMask, weight); |
||||
} |
||||
|
||||
private record RoomFileMetadata(String identifier, String biome, int exitsMask, int weight) { |
||||
} |
||||
} |
||||
@ -0,0 +1,28 @@ |
||||
package xyz.soukup.ecoCraftCore.mines.generation; |
||||
|
||||
import com.sk89q.worldedit.extent.clipboard.Clipboard; |
||||
|
||||
import java.io.File; |
||||
import java.util.List; |
||||
|
||||
public record MineRoomPrefab( |
||||
String identifier, |
||||
String biome, |
||||
int exitsMask, |
||||
int weight, |
||||
File sourceFile, |
||||
Clipboard clipboard |
||||
) { |
||||
|
||||
public boolean hasExit(MineDirection direction) { |
||||
return (exitsMask & direction.bitMask()) != 0; |
||||
} |
||||
|
||||
public int exitCount() { |
||||
return Integer.bitCount(exitsMask); |
||||
} |
||||
|
||||
public List<MineDirection> exits() { |
||||
return MineDirection.directionsFromMask(exitsMask); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue