Mine rework.

master
erikradovan 3 weeks ago
parent 8126729ea7
commit 300cb801da
  1. 20
      pom.xml
  2. 5
      src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java
  3. 30
      src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java
  4. 712
      src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java
  5. 154
      src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java
  6. 50
      src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineBranchState.java
  7. 68
      src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineDirection.java
  8. 191
      src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineGenerationConfig.java
  9. 9
      src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineGenerationPoint.java
  10. 376
      src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineGenerator.java
  11. 12
      src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineGridPosition.java
  12. 181
      src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineRoomLibrary.java
  13. 28
      src/main/java/xyz/soukup/ecoCraftCore/mines/generation/MineRoomPrefab.java
  14. 37
      src/main/resources/config.yml
  15. 1
      src/main/resources/messages.yml

@ -28,6 +28,16 @@
<target>${java.version}</target> <target>${java.version}</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>slime</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>io.ebean</groupId> <groupId>io.ebean</groupId>
@ -88,6 +98,10 @@
</build> </build>
<repositories> <repositories>
<repository>
<id>enginehub</id>
<url>https://maven.enginehub.org/repo/</url>
</repository>
<repository> <repository>
<id>codemc-releases</id> <id>codemc-releases</id>
<url>https://repo.codemc.io/repository/maven-releases/</url> <url>https://repo.codemc.io/repository/maven-releases/</url>
@ -140,6 +154,12 @@
<version>1.21.10-R0.1-SNAPSHOT</version> <version>1.21.10-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>com.sk89q.worldedit</groupId>
<artifactId>worldedit-bukkit</artifactId>
<version>7.3.12</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>com.github.stefvanschie.inventoryframework</groupId> <groupId>com.github.stefvanschie.inventoryframework</groupId>
<artifactId>IF</artifactId> <artifactId>IF</artifactId>

@ -88,9 +88,12 @@ public final class EcoCraftCore extends JavaPlugin {
prepareSlimeWorldsSaver(); prepareSlimeWorldsSaver();
}catch (IOException e) { }catch (IOException e) {
MineWorldManager.init(); e.printStackTrace();
getLogger().severe("Failed to save island templates.");
} }
MineWorldManager.init();
this.getServer().getScheduler().runTaskTimer(this, /* Lambda: */ task -> { this.getServer().getScheduler().runTaskTimer(this, /* Lambda: */ task -> {
VirtualChest.saveCache(); VirtualChest.saveCache();
} , 6000, 6000); } , 6000, 6000);

@ -6,6 +6,7 @@ import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands; import io.papermc.paper.command.brigadier.Commands;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import xyz.soukup.ecoCraftCore.messages.Messages; import xyz.soukup.ecoCraftCore.messages.Messages;
@ -26,29 +27,34 @@ public class MineCommand {
} }
private static int regenerateMines(CommandContext<CommandSourceStack> context) { private static int regenerateMines(CommandContext<CommandSourceStack> context) {
CommandSender sender = context.getSource().getSender();
World world = MineWorldManager.getWorld(); World world = MineWorldManager.getWorld();
if (world == null) { if (world == null) {
Messages.send(context.getSource().getSender(), "mine.error.no-world"); Messages.send(sender, "mine.error.no-world");
return 0; 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 player : world.getPlayers()) {
for (Player p : world.getPlayers()) { player.teleport(player.getServer().getWorlds().getFirst().getSpawnLocation());
p.teleport(p.getServer().getWorlds().getFirst().getSpawnLocation()); Messages.send(player, "mine.teleported-out");
Messages.send(p, "mine.teleported-out");
} }
// Delete old world, create fresh one, then generate MineWorldManager.recreateWorld(newWorld -> {
MineWorldManager.recreateWorld(newWorld -> MineManager.regenerate(newWorld)); boolean success = MineManager.regenerate(newWorld);
if (success) {
Messages.send(sender, "mine.regenerate-complete");
} else {
Messages.send(sender, "mine.error.regenerate-failed");
}
});
return 1; return 1;
} }
private static int teleportToMines(CommandContext<CommandSourceStack> context) { private static int teleportToMines(CommandContext<CommandSourceStack> context) {
Player player = (Player) context.getSource().getSender(); Player player = (Player) context.getSource().getSender();
World world = MineWorldManager.getWorld(); World world = MineWorldManager.getWorld();
if (world == null) { if (world == null) {
@ -57,9 +63,13 @@ public class MineCommand {
} }
Location spawn = MineWorldManager.getSpawnLocation(); Location spawn = MineWorldManager.getSpawnLocation();
if (spawn == null) {
Messages.send(player, "mine.error.no-world");
return 0;
}
player.teleport(spawn); player.teleport(spawn);
Messages.send(player, "mine.teleporting"); Messages.send(player, "mine.teleporting");
return 1; return 1;
} }
} }

@ -1,696 +1,62 @@
package xyz.soukup.ecoCraftCore.mines; package xyz.soukup.ecoCraftCore.mines;
import org.bukkit.Bukkit; import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block; import xyz.soukup.ecoCraftCore.mines.generation.MineGenerationConfig;
import org.bukkit.block.BlockFace; import xyz.soukup.ecoCraftCore.mines.generation.MineGenerator;
import xyz.soukup.ecoCraftCore.mines.generation.MineRoomLibrary;
import java.util.*;
import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin;
public class MineManager { public final 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;
}
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);
// If the broken block isn't a fill material, no ore roll private MineManager() {
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);
} }
/** public static boolean regenerate(World world) {
* Looks at the 6 blocks surrounding the broken block and finds the deepest fill material. MineGenerationConfig generationConfig = MineGenerationConfig.fromConfiguration(plugin.getConfig(), world);
*/ MineRoomLibrary roomLibrary = MineRoomLibrary.load(
private static Material detectDeepestSurroundingFill(Block block) { plugin.getDataFolder(),
Material deepest = null; generationConfig.roomsDirectory(),
int deepestIndex = -1; plugin.getLogger()
);
for (BlockFace face : DIRECTIONS) {
Block neighbor = block.getRelative(face);
Material type = neighbor.getType();
int index = ALL_FILLS_ORDERED.indexOf(type); if (roomLibrary.isEmpty()) {
if (index > deepestIndex) { plugin.getLogger().severe("[MineGen] No valid room prefabs were loaded from: "
deepestIndex = index; + roomLibrary.roomsDirectory().getAbsolutePath());
deepest = type; return false;
}
} }
return deepest; MineGenerator.Result generationResult = new MineGenerator(
} world,
generationConfig,
roomLibrary,
plugin.getLogger()
).generate();
/** if (!generationResult.success()) {
* Picks a random ore from the table using weights. return false;
*/
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 ===================== Location spawn = getConfiguredSpawnLocation(world);
world.setSpawnLocation(spawn);
private static int[][] getPerpOffsets(BlockFace dir) { plugin.getLogger().info("[MineGen] Mine generated with " + generationResult.roomsPlaced() + " rooms.");
return switch (dir) { return true;
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 ===================== public static Location getConfiguredSpawnLocation(World world) {
Location fallback = new Location(world, 5.5, world.getMaxHeight() - 9, 5.5, 0f, 0f);
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 if (!plugin.getConfig().isConfigurationSection("mine.spawn")) {
List<BlockPlacement> dirtPlaceholders = new ArrayList<>(fillPlacements.size()); return fallback;
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;
}
private static int decodeY(long key) {
return (int) ((key >> 26) & 0xFFF) - 2048;
}
private static int decodeZ(long key) { double x = plugin.getConfig().getDouble("mine.spawn.x", fallback.getX());
return (int) ((key >> 38) & 0x3FFFFFF) - 30000000; 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);
} }
} }

@ -4,7 +4,11 @@ import com.infernalsuite.asp.api.AdvancedSlimePaperAPI;
import com.infernalsuite.asp.api.world.SlimeWorld; import com.infernalsuite.asp.api.world.SlimeWorld;
import com.infernalsuite.asp.api.world.properties.SlimeProperties; import com.infernalsuite.asp.api.world.properties.SlimeProperties;
import com.infernalsuite.asp.api.world.properties.SlimePropertyMap; 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.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; 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.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerItemDamageEvent; import org.bukkit.event.player.PlayerItemDamageEvent;
import org.bukkit.event.player.PlayerJoinEvent; 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.PotionEffect;
import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionEffectType;
import xyz.soukup.ecoCraftCore.database.objects.Island; import xyz.soukup.ecoCraftCore.database.objects.Island;
@ -39,25 +41,23 @@ public class MineWorldManager implements Listener {
try { try {
SlimeWorld slimeWorld = null; SlimeWorld slimeWorld = null;
// Try to load existing world
if (loader.worldExists(MINE_WORLD_NAME)) { if (loader.worldExists(MINE_WORLD_NAME)) {
try { try {
slimeWorld = asp.readWorld(loader, MINE_WORLD_NAME, false, new SlimePropertyMap()); 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..."); plugin.getLogger().warning("Mine world data is corrupted, recreating...");
loader.deleteWorld(MINE_WORLD_NAME); loader.deleteWorld(MINE_WORLD_NAME);
} }
} }
// Create fresh world if loading failed or didn't exist
if (slimeWorld == null) { if (slimeWorld == null) {
SlimePropertyMap props = new SlimePropertyMap(); SlimePropertyMap properties = new SlimePropertyMap();
props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL"); properties.setValue(SlimeProperties.ENVIRONMENT, "NORMAL");
Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null); Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null);
island.save(); island.save();
slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, props, loader); slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, properties, loader);
} }
SlimeWorld finalWorld = slimeWorld; SlimeWorld finalWorld = slimeWorld;
@ -70,11 +70,14 @@ public class MineWorldManager implements Listener {
mineWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false); mineWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
mineWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false); mineWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
mineWorld.setTime(6000); mineWorld.setTime(6000);
if (plugin.getConfig().getBoolean("mine.paste-on-startup", false)) {
MineManager.regenerate(mineWorld);
}
plugin.getLogger().info("Mine world loaded successfully."); plugin.getLogger().info("Mine world loaded successfully.");
} }
}); });
} catch (Exception e) { } catch (Exception exception) {
e.printStackTrace(); exception.printStackTrace();
plugin.getLogger().severe("Failed to load mine world."); plugin.getLogger().severe("Failed to load mine world.");
} }
}); });
@ -85,16 +88,12 @@ public class MineWorldManager implements Listener {
} }
public static Location getSpawnLocation() { public static Location getSpawnLocation() {
if (mineWorld == null) return null; if (mineWorld == null) {
int startY = mineWorld.getMaxHeight() - 10; return null;
return new Location(mineWorld, 5, startY + 1, 5); }
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<World> callback) { public static void recreateWorld(Consumer<World> callback) {
AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance();
DatabaseIslandLoader loader = new DatabaseIslandLoader(); DatabaseIslandLoader loader = new DatabaseIslandLoader();
@ -113,13 +112,13 @@ public class MineWorldManager implements Listener {
plugin.getLogger().info("[MineWorld] Old mine world deleted."); plugin.getLogger().info("[MineWorld] Old mine world deleted.");
} }
SlimePropertyMap props = new SlimePropertyMap(); SlimePropertyMap properties = new SlimePropertyMap();
props.setValue(SlimeProperties.ENVIRONMENT, "NORMAL"); properties.setValue(SlimeProperties.ENVIRONMENT, "NORMAL");
Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null); Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null);
island.save(); 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, () -> { Bukkit.getScheduler().runTask(plugin, () -> {
asp.loadWorld(slimeWorld, true); asp.loadWorld(slimeWorld, true);
@ -136,8 +135,8 @@ public class MineWorldManager implements Listener {
plugin.getLogger().severe("[MineWorld] Failed to load fresh mine world."); plugin.getLogger().severe("[MineWorld] Failed to load fresh mine world.");
} }
}); });
} catch (Exception e) { } catch (Exception exception) {
e.printStackTrace(); exception.printStackTrace();
plugin.getLogger().severe("[MineWorld] Failed to recreate mine world."); plugin.getLogger().severe("[MineWorld] Failed to recreate mine world.");
} }
}); });
@ -151,23 +150,19 @@ public class MineWorldManager implements Listener {
return world.equals(mineWorld); return world.equals(mineWorld);
} }
// Apply invisible mining fatigue when entering mine world
@EventHandler @EventHandler
public void onWorldChange(PlayerChangedWorldEvent event) { public void onWorldChange(PlayerChangedWorldEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
// Entering mine world
if (isInMineWorld(player)) { if (isInMineWorld(player)) {
applyMiningFatigue(player); applyMiningFatigue(player);
} }
// Leaving mine world
if (isInMineWorld(event.getFrom())) { if (isInMineWorld(event.getFrom())) {
player.removePotionEffect(PotionEffectType.MINING_FATIGUE); player.removePotionEffect(PotionEffectType.MINING_FATIGUE);
} }
} }
// Re-apply fatigue on join if player is in mine world
@EventHandler @EventHandler
public void onJoin(PlayerJoinEvent event) { public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
@ -176,7 +171,6 @@ public class MineWorldManager implements Listener {
} }
} }
// Re-apply fatigue on respawn in mine world
@EventHandler @EventHandler
public void onDeath(PlayerDeathEvent event) { public void onDeath(PlayerDeathEvent event) {
Bukkit.getScheduler().runTaskLater(plugin, () -> { Bukkit.getScheduler().runTaskLater(plugin, () -> {
@ -191,114 +185,48 @@ public class MineWorldManager implements Listener {
player.addPotionEffect(new PotionEffect( player.addPotionEffect(new PotionEffect(
PotionEffectType.MINING_FATIGUE, PotionEffectType.MINING_FATIGUE,
Integer.MAX_VALUE, Integer.MAX_VALUE,
0, // level I (amplifier 0) 0,
true, // ambient true,
false, // no particles false,
false // no icon false
)); ));
} }
// Double tool durability damage in mine world
@EventHandler @EventHandler
public void onItemDamage(PlayerItemDamageEvent event) { public void onItemDamage(PlayerItemDamageEvent event) {
if (!isInMineWorld(event.getPlayer())) return; if (!isInMineWorld(event.getPlayer())) {
if (event.getItem().getType().getMaxDurability() <= 0) return; return;
}
if (event.getItem().getType().getMaxDurability() <= 0) {
return;
}
event.setDamage(event.getDamage() * 2); event.setDamage(event.getDamage() * 2);
} }
// Prevent entity explosions from destroying bedrock walls
@EventHandler @EventHandler
public void onEntityExplode(EntityExplodeEvent event) { 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); event.blockList().removeIf(block -> block.getType() == Material.BEDROCK);
} }
// Prevent block explosions from destroying bedrock walls
@EventHandler @EventHandler
public void onBlockExplode(BlockExplodeEvent event) { 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); event.blockList().removeIf(block -> block.getType() == Material.BEDROCK);
} }
// Block break logic: bedrock unbreakable, ore-on-break for ALL fills, mining sequence
@EventHandler @EventHandler
public void onBlockBreak(BlockBreakEvent event) { public void onBlockBreak(BlockBreakEvent event) {
if (!isInMineWorld(event.getPlayer())) return; if (!isInMineWorld(event.getPlayer())) {
Material type = event.getBlock().getType();
// Bedrock is completely unbreakable (mine walls)
if (type == Material.BEDROCK) {
event.setCancelled(true);
return; return;
} }
// For any fill material (at any stage of the mining sequence), roll for ore first. if (event.getBlock().getType() == Material.BEDROCK) {
// 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) {
event.setCancelled(true); 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
};
} }
} }

@ -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);
}
}

@ -11,4 +11,39 @@ database:
cache: cache:
save-interval: 6000 save-interval: 6000
islands: islands:
spawn: null spawn: null
mine:
paste-on-startup: false
spawn:
x: 5.5
y: 311.0
z: 5.5
yaw: 0.0
pitch: 0.0
generator:
rooms-directory: "rooms"
starter-room: "ST15"
default-biome: "ST"
room-size: 16
max-depth: 64
max-rooms: 320
ignore-air: false
origin:
x: 0
y: 310
z: 0
depth-biomes:
- biome: "ST"
min-depth: 0
- biome: "DS"
min-depth: 20
- biome: "MS"
min-depth: 40
natural-biomes:
- biome: "DS"
chance: 0.08
length: 6
- biome: "MS"
chance: 0.05
length: 5

@ -84,4 +84,5 @@ mine:
teleported-out: "<yellow>Byl jsi teleportován z dolů kvůli regeneraci." teleported-out: "<yellow>Byl jsi teleportován z dolů kvůli regeneraci."
error: error:
no-world: "<red>Důlní svět není načtený." no-world: "<red>Důlní svět není načtený."
regenerate-failed: "<red>Regenerace dolů selhala. Zkontroluj room prefab soubory a server log."

Loading…
Cancel
Save