diff --git a/pom.xml b/pom.xml
index 38fc361..c32d4dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,16 @@
${java.version}
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+
+ slime
+
+
+
io.ebean
@@ -88,6 +98,10 @@
+
+ enginehub
+ https://maven.enginehub.org/repo/
+
codemc-releases
https://repo.codemc.io/repository/maven-releases/
@@ -140,6 +154,12 @@
1.21.10-R0.1-SNAPSHOT
provided
+
+ com.sk89q.worldedit
+ worldedit-bukkit
+ 7.3.12
+ provided
+
com.github.stefvanschie.inventoryframework
IF
diff --git a/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java b/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java
index aa96cc4..2a1749e 100644
--- a/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java
+++ b/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java
@@ -88,9 +88,12 @@ public final class EcoCraftCore extends JavaPlugin {
prepareSlimeWorldsSaver();
}catch (IOException e) {
- MineWorldManager.init();
+ e.printStackTrace();
+ getLogger().severe("Failed to save island templates.");
}
+ MineWorldManager.init();
+
this.getServer().getScheduler().runTaskTimer(this, /* Lambda: */ task -> {
VirtualChest.saveCache();
} , 6000, 6000);
diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java
index 25cd2f4..f55c8ba 100644
--- a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java
+++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java
@@ -6,6 +6,7 @@ import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import org.bukkit.Location;
import org.bukkit.World;
+import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import xyz.soukup.ecoCraftCore.messages.Messages;
@@ -26,29 +27,34 @@ public class MineCommand {
}
private static int regenerateMines(CommandContext context) {
+ CommandSender sender = context.getSource().getSender();
World world = MineWorldManager.getWorld();
if (world == null) {
- Messages.send(context.getSource().getSender(), "mine.error.no-world");
+ Messages.send(sender, "mine.error.no-world");
return 0;
}
- Messages.send(context.getSource().getSender(), "mine.regenerating");
+ Messages.send(sender, "mine.regenerating");
- // Teleport all players out of the mine world first
- for (Player p : world.getPlayers()) {
- p.teleport(p.getServer().getWorlds().getFirst().getSpawnLocation());
- Messages.send(p, "mine.teleported-out");
+ for (Player player : world.getPlayers()) {
+ player.teleport(player.getServer().getWorlds().getFirst().getSpawnLocation());
+ Messages.send(player, "mine.teleported-out");
}
- // Delete old world, create fresh one, then generate
- MineWorldManager.recreateWorld(newWorld -> MineManager.regenerate(newWorld));
+ MineWorldManager.recreateWorld(newWorld -> {
+ boolean success = MineManager.regenerate(newWorld);
+ if (success) {
+ Messages.send(sender, "mine.regenerate-complete");
+ } else {
+ Messages.send(sender, "mine.error.regenerate-failed");
+ }
+ });
return 1;
}
private static int teleportToMines(CommandContext context) {
Player player = (Player) context.getSource().getSender();
-
World world = MineWorldManager.getWorld();
if (world == null) {
@@ -57,9 +63,13 @@ public class MineCommand {
}
Location spawn = MineWorldManager.getSpawnLocation();
+ if (spawn == null) {
+ Messages.send(player, "mine.error.no-world");
+ return 0;
+ }
+
player.teleport(spawn);
Messages.send(player, "mine.teleporting");
return 1;
}
}
-
diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java
index 614575d..168cc97 100644
--- a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java
+++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java
@@ -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 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 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 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 fillPositions = new HashSet<>();
- List fillPlacements = new ArrayList<>();
-
- // ===== Biome 1: NORMAL =====
- plugin.getLogger().info("[MineGen] === Starting NORMAL biome generation ===");
- List 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 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 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 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 placements, Set fillPositions,
- List completedBranches, int[] branchCount) {
- Deque 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 queue, List placements,
- Set 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 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 branches) {
- Branch lowest = null;
- for (Branch b : branches) {
- if (lowest == null || b.endY < lowest.endY) {
- lowest = b;
- }
- }
- return lowest;
- }
-
- private static List getAvailableDirections(BlockFace current) {
- List 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 placements, Set 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;
- }
-
- 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;
- };
- }
-
- // ===================== ORE-ON-BREAK SYSTEM (called from MineWorldManager) =====================
-
- /**
- * 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;
- }
-
- int detectedIndex = ALL_FILLS_ORDERED.indexOf(detectedFill);
- int brokenIndex = ALL_FILLS_ORDERED.indexOf(brokenType);
+public final class MineManager {
- // 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
- }
-
- // 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;
-
- return pickWeightedOre(oreTable);
+ private MineManager() {
}
- /**
- * 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();
+ public static boolean regenerate(World world) {
+ MineGenerationConfig generationConfig = MineGenerationConfig.fromConfiguration(plugin.getConfig(), world);
+ MineRoomLibrary roomLibrary = MineRoomLibrary.load(
+ plugin.getDataFolder(),
+ generationConfig.roomsDirectory(),
+ plugin.getLogger()
+ );
- int index = ALL_FILLS_ORDERED.indexOf(type);
- if (index > deepestIndex) {
- deepestIndex = index;
- deepest = type;
- }
+ if (roomLibrary.isEmpty()) {
+ plugin.getLogger().severe("[MineGen] No valid room prefabs were loaded from: "
+ + roomLibrary.roomsDirectory().getAbsolutePath());
+ return false;
}
- return deepest;
- }
+ MineGenerator.Result generationResult = new MineGenerator(
+ world,
+ generationConfig,
+ roomLibrary,
+ plugin.getLogger()
+ ).generate();
- /**
- * 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;
+ if (!generationResult.success()) {
+ return false;
}
- return table[table.length - 1].material;
- }
- // ===================== PERPENDICULAR AXES =====================
+ Location spawn = getConfiguredSpawnLocation(world);
+ world.setSpawnLocation(spawn);
- 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}};
- };
+ plugin.getLogger().info("[MineGen] Mine generated with " + generationResult.roomsPlaced() + " rooms.");
+ return true;
}
- // ===================== WALL GENERATION =====================
-
- private static void generateWalls(Set fillPositions, List wallPlacements, int startY) {
- Set 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 fillPlacements,
- List wallPlacements,
- int startY, int minY, int maxY) {
- // Three-phase placement: 1) Dirt placeholder 2) Bedrock walls 3) Actual fill
- List allPlacements = new ArrayList<>();
+ public static Location getConfiguredSpawnLocation(World world) {
+ Location fallback = new Location(world, 5.5, world.getMaxHeight() - 9, 5.5, 0f, 0f);
- // Phase A: Dirt placeholders at all fill positions
- List dirtPlaceholders = new ArrayList<>(fillPlacements.size());
- for (BlockPlacement bp : fillPlacements) {
- dirtPlaceholders.add(new BlockPlacement(bp.x, bp.y, bp.z, Material.DIRT));
+ if (!plugin.getConfig().isConfigurationSection("mine.spawn")) {
+ return fallback;
}
- 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;
- }
-
- private static int decodeY(long key) {
- return (int) ((key >> 26) & 0xFFF) - 2048;
- }
- 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);
}
}
diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java
index 1a2d8d8..f2d5a94 100644
--- a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java
+++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java
@@ -4,7 +4,11 @@ import com.infernalsuite.asp.api.AdvancedSlimePaperAPI;
import com.infernalsuite.asp.api.world.SlimeWorld;
import com.infernalsuite.asp.api.world.properties.SlimeProperties;
import com.infernalsuite.asp.api.world.properties.SlimePropertyMap;
-import org.bukkit.*;
+import org.bukkit.Bukkit;
+import org.bukkit.GameRule;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -15,8 +19,6 @@ import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerItemDamageEvent;
import org.bukkit.event.player.PlayerJoinEvent;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.Damageable;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import xyz.soukup.ecoCraftCore.database.objects.Island;
@@ -39,25 +41,23 @@ public class MineWorldManager implements Listener {
try {
SlimeWorld slimeWorld = null;
- // Try to load existing world
if (loader.worldExists(MINE_WORLD_NAME)) {
try {
slimeWorld = asp.readWorld(loader, MINE_WORLD_NAME, false, new SlimePropertyMap());
- } catch (Exception e) {
+ } catch (Exception exception) {
plugin.getLogger().warning("Mine world data is corrupted, recreating...");
loader.deleteWorld(MINE_WORLD_NAME);
}
}
- // Create fresh world if loading failed or didn't exist
if (slimeWorld == null) {
- SlimePropertyMap props = new SlimePropertyMap();
- props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL");
+ SlimePropertyMap properties = new SlimePropertyMap();
+ properties.setValue(SlimeProperties.ENVIRONMENT, "NORMAL");
Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null);
island.save();
- slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, props, loader);
+ slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, properties, loader);
}
SlimeWorld finalWorld = slimeWorld;
@@ -70,11 +70,14 @@ public class MineWorldManager implements Listener {
mineWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
mineWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
mineWorld.setTime(6000);
+ if (plugin.getConfig().getBoolean("mine.paste-on-startup", false)) {
+ MineManager.regenerate(mineWorld);
+ }
plugin.getLogger().info("Mine world loaded successfully.");
}
});
- } catch (Exception e) {
- e.printStackTrace();
+ } catch (Exception exception) {
+ exception.printStackTrace();
plugin.getLogger().severe("Failed to load mine world.");
}
});
@@ -85,16 +88,12 @@ public class MineWorldManager implements Listener {
}
public static Location getSpawnLocation() {
- if (mineWorld == null) return null;
- int startY = mineWorld.getMaxHeight() - 10;
- return new Location(mineWorld, 5, startY + 1, 5);
+ if (mineWorld == null) {
+ return null;
+ }
+ return MineManager.getConfiguredSpawnLocation(mineWorld);
}
- /**
- * Deletes the current mine world and creates a fresh empty one.
- * Calls the callback with the new World once it's ready.
- * Must be called from the main thread with all players already teleported out.
- */
public static void recreateWorld(Consumer callback) {
AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance();
DatabaseIslandLoader loader = new DatabaseIslandLoader();
@@ -113,13 +112,13 @@ public class MineWorldManager implements Listener {
plugin.getLogger().info("[MineWorld] Old mine world deleted.");
}
- SlimePropertyMap props = new SlimePropertyMap();
- props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL");
+ SlimePropertyMap properties = new SlimePropertyMap();
+ properties.setValue(SlimeProperties.ENVIRONMENT, "NORMAL");
Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null);
island.save();
- SlimeWorld slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, props, loader);
+ SlimeWorld slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, properties, loader);
Bukkit.getScheduler().runTask(plugin, () -> {
asp.loadWorld(slimeWorld, true);
@@ -136,8 +135,8 @@ public class MineWorldManager implements Listener {
plugin.getLogger().severe("[MineWorld] Failed to load fresh mine world.");
}
});
- } catch (Exception e) {
- e.printStackTrace();
+ } catch (Exception exception) {
+ exception.printStackTrace();
plugin.getLogger().severe("[MineWorld] Failed to recreate mine world.");
}
});
@@ -151,23 +150,19 @@ public class MineWorldManager implements Listener {
return world.equals(mineWorld);
}
- // Apply invisible mining fatigue when entering mine world
@EventHandler
public void onWorldChange(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
- // Entering mine world
if (isInMineWorld(player)) {
applyMiningFatigue(player);
}
- // Leaving mine world
if (isInMineWorld(event.getFrom())) {
player.removePotionEffect(PotionEffectType.MINING_FATIGUE);
}
}
- // Re-apply fatigue on join if player is in mine world
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
@@ -176,7 +171,6 @@ public class MineWorldManager implements Listener {
}
}
- // Re-apply fatigue on respawn in mine world
@EventHandler
public void onDeath(PlayerDeathEvent event) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
@@ -191,114 +185,48 @@ public class MineWorldManager implements Listener {
player.addPotionEffect(new PotionEffect(
PotionEffectType.MINING_FATIGUE,
Integer.MAX_VALUE,
- 0, // level I (amplifier 0)
- true, // ambient
- false, // no particles
- false // no icon
+ 0,
+ true,
+ false,
+ false
));
}
- // Double tool durability damage in mine world
@EventHandler
public void onItemDamage(PlayerItemDamageEvent event) {
- if (!isInMineWorld(event.getPlayer())) return;
- if (event.getItem().getType().getMaxDurability() <= 0) return;
+ if (!isInMineWorld(event.getPlayer())) {
+ return;
+ }
+ if (event.getItem().getType().getMaxDurability() <= 0) {
+ return;
+ }
event.setDamage(event.getDamage() * 2);
}
- // Prevent entity explosions from destroying bedrock walls
@EventHandler
public void onEntityExplode(EntityExplodeEvent event) {
- if (!isInMineWorld(event.getEntity().getWorld())) return;
+ if (!isInMineWorld(event.getEntity().getWorld())) {
+ return;
+ }
event.blockList().removeIf(block -> block.getType() == Material.BEDROCK);
}
- // Prevent block explosions from destroying bedrock walls
@EventHandler
public void onBlockExplode(BlockExplodeEvent event) {
- if (!isInMineWorld(event.getBlock().getWorld())) return;
+ if (!isInMineWorld(event.getBlock().getWorld())) {
+ return;
+ }
event.blockList().removeIf(block -> block.getType() == Material.BEDROCK);
}
- // Block break logic: bedrock unbreakable, ore-on-break for ALL fills, mining sequence
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
- if (!isInMineWorld(event.getPlayer())) return;
-
- Material type = event.getBlock().getType();
-
- // Bedrock is completely unbreakable (mine walls)
- if (type == Material.BEDROCK) {
- event.setCancelled(true);
+ if (!isInMineWorld(event.getPlayer())) {
return;
}
- // For any fill material (at any stage of the mining sequence), roll for ore first.
- // The ore replaces the block as another mining step — player then breaks the ore normally.
- if (MineManager.isFillMaterial(type)) {
- Material ore = MineManager.rollOreOnBreak(event.getBlock(), type);
- if (ore != null) {
- event.setCancelled(true);
- event.getBlock().setType(ore);
- event.getBlock().getState().update(true);
- applyToolDamage(event);
- return;
- }
- }
-
- // No ore rolled — apply normal mining sequence (deeper fills degrade into lighter fills)
- Material nextTier = getNextTier(type);
- if (nextTier != null) {
+ if (event.getBlock().getType() == Material.BEDROCK) {
event.setCancelled(true);
- event.getBlock().setType(nextTier);
- event.getBlock().getState().update(true);
- applyToolDamage(event);
- return;
}
-
- // Terminal fill (stone) or ores — break normally
- }
-
- /**
- * Applies doubled durability damage to the player's tool when we cancel a break event.
- * Only applies if the held item actually has durability.
- */
- private void applyToolDamage(BlockBreakEvent event) {
- ItemStack tool = event.getPlayer().getInventory().getItemInMainHand();
- if (tool.getType().isAir() || tool.getType().getMaxDurability() <= 0) return;
- if (!(tool.getItemMeta() instanceof Damageable damageable)) return;
-
- damageable.setDamage(damageable.getDamage() + 2); // doubled durability
- tool.setItemMeta(damageable);
-
- if (damageable.getDamage() >= tool.getType().getMaxDurability()) {
- event.getPlayer().getInventory().setItemInMainHand(null);
- event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f);
- }
- }
-
- /**
- * Mining sequence: deeper fills degrade into lighter fills.
- * Returns null when the block should break normally (terminal fill = stone).
- *
- * Chain: gilded_blackstone -> cracked_polished_blackstone_bricks -> blackstone -> smooth_basalt
- * -> cracked_deepslate_tiles -> cobbled_deepslate -> deepslate -> tuff -> cobblestone
- * -> andesite -> stone -> null (breaks normally)
- */
- private Material getNextTier(Material material) {
- return switch (material) {
- case GILDED_BLACKSTONE -> Material.CRACKED_POLISHED_BLACKSTONE_BRICKS;
- case CRACKED_POLISHED_BLACKSTONE_BRICKS -> Material.BLACKSTONE;
- case BLACKSTONE -> Material.SMOOTH_BASALT;
- case SMOOTH_BASALT -> Material.CRACKED_DEEPSLATE_TILES;
- case CRACKED_DEEPSLATE_TILES -> Material.COBBLED_DEEPSLATE;
- case COBBLED_DEEPSLATE -> Material.DEEPSLATE;
- case DEEPSLATE -> Material.TUFF;
- case TUFF -> Material.COBBLESTONE;
- case COBBLESTONE -> Material.ANDESITE;
- case ANDESITE -> Material.STONE;
- default -> null; // Stone and non-fill blocks break normally
- };
}
}
-
diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineBranchState.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineBranchState.java
new file mode 100644
index 0000000..feaa26b
--- /dev/null
+++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineBranchState.java
@@ -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)
+ );
+ }
+}
diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineDirection.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineDirection.java
new file mode 100644
index 0000000..2801fea
--- /dev/null
+++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineDirection.java
@@ -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 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 horizontalDirections() {
+ return HORIZONTAL_DIRECTIONS;
+ }
+
+ public static List directionsFromMask(int exitsMask) {
+ List directions = new ArrayList<>();
+ for (MineDirection direction : values()) {
+ if ((exitsMask & direction.bitMask) != 0) {
+ directions.add(direction);
+ }
+ }
+ return directions;
+ }
+}
diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineGenerationConfig.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineGenerationConfig.java
new file mode 100644
index 0000000..4537fc4
--- /dev/null
+++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineGenerationConfig.java
@@ -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 depthBiomes,
+ List 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 depthBiomes = parseDepthBiomes(
+ configuration.getMapList(BASE_PATH + ".depth-biomes"),
+ defaultBiome
+ );
+ List 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 parseDepthBiomes(List