Compare commits

...

1 Commits

  1. 12
      src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java
  2. 65
      src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java
  3. 469
      src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java
  4. 275
      src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java
  5. 7
      src/main/resources/messages.yml

@ -9,11 +9,14 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import xyz.soukup.ecoCraftCore.mines.MineCommand;
import xyz.soukup.ecoCraftCore.mines.MineWorldManager;
import xyz.soukup.ecoCraftCore.money.MoneyCommand; import xyz.soukup.ecoCraftCore.money.MoneyCommand;
import xyz.soukup.ecoCraftCore.player.PreparePlayer; import xyz.soukup.ecoCraftCore.player.PreparePlayer;
import xyz.soukup.ecoCraftCore.positionMarker.RulerCommand; import xyz.soukup.ecoCraftCore.positionMarker.RulerCommand;
import xyz.soukup.ecoCraftCore.shop.ShopCommand; import xyz.soukup.ecoCraftCore.shop.ShopCommand;
import xyz.soukup.ecoCraftCore.database.objects.Account; import xyz.soukup.ecoCraftCore.database.objects.Account;
import xyz.soukup.ecoCraftCore.database.objects.Island;
import xyz.soukup.ecoCraftCore.database.objects.Shop; import xyz.soukup.ecoCraftCore.database.objects.Shop;
import xyz.soukup.ecoCraftCore.database.objects.Transaction; import xyz.soukup.ecoCraftCore.database.objects.Transaction;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
@ -46,6 +49,7 @@ public final class EcoCraftCore extends JavaPlugin {
prepareDatabase(); prepareDatabase();
registerCommands(); registerCommands();
registerEvents(); registerEvents();
MineWorldManager.init();
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); e.printStackTrace();
getLogger().severe("Failed to initialize database."); getLogger().severe("Failed to initialize database.");
@ -74,8 +78,8 @@ public final class EcoCraftCore extends JavaPlugin {
} }
private void prepareDatabase() throws SQLException { private void prepareDatabase() throws SQLException {
String databaseUrl = "jdbc:mysql://localhost:3306/ecc"; String databaseUrl = "jdbc:mysql://u8089_PlbP6lACvk:%3DGz!yBPu%3DGTvywi4ot%40lbEKw@camelot.vagonbrei.eu:3306/s8089_ecc";
connectionSource = new JdbcConnectionSource(databaseUrl, "ecc", "ecc"); connectionSource = new JdbcConnectionSource(databaseUrl, "u8089_PlbP6lACvk", "=Gz!yBPu=GTvywi4ot@lbEKw");
Logger.getLogger("com.j256.ormlite.table.TableUtils").setLevel(Level.OFF); Logger.getLogger("com.j256.ormlite.table.TableUtils").setLevel(Level.OFF);
@ -83,11 +87,13 @@ public final class EcoCraftCore extends JavaPlugin {
TableUtils.createTableIfNotExists(connectionSource, Shop.class); TableUtils.createTableIfNotExists(connectionSource, Shop.class);
TableUtils.createTableIfNotExists(connectionSource, VirtualChest.class); TableUtils.createTableIfNotExists(connectionSource, VirtualChest.class);
TableUtils.createTableIfNotExists(connectionSource, Account.class); TableUtils.createTableIfNotExists(connectionSource, Account.class);
TableUtils.createTableIfNotExists(connectionSource, Island.class);
DaoRegistry.setTransactionDao(DaoManager.createDao(connectionSource, Transaction.class)); DaoRegistry.setTransactionDao(DaoManager.createDao(connectionSource, Transaction.class));
DaoRegistry.setShopDao(DaoManager.createDao(connectionSource, Shop.class)); DaoRegistry.setShopDao(DaoManager.createDao(connectionSource, Shop.class));
DaoRegistry.setVirtualChestDao(DaoManager.createDao(connectionSource, VirtualChest.class)); DaoRegistry.setVirtualChestDao(DaoManager.createDao(connectionSource, VirtualChest.class));
DaoRegistry.setAccountDao(DaoManager.createDao(connectionSource, Account.class)); DaoRegistry.setAccountDao(DaoManager.createDao(connectionSource, Account.class));
DaoRegistry.setIslandDaoo(DaoManager.createDao(connectionSource, Island.class));
} }
@ -97,6 +103,7 @@ public final class EcoCraftCore extends JavaPlugin {
lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(ShopCommand.createCommand().build())); lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(ShopCommand.createCommand().build()));
lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(RulerCommand.createCommand().build())); lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(RulerCommand.createCommand().build()));
lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MoneyCommand.createCommand().build())); lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MoneyCommand.createCommand().build()));
lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MineCommand.createCommand().build()));
} }
private void registerEvents(){ private void registerEvents(){
@ -106,6 +113,7 @@ public final class EcoCraftCore extends JavaPlugin {
pm.registerEvents(new VirtualChestLogic(), this); pm.registerEvents(new VirtualChestLogic(), this);
pm.registerEvents(new ShopLogic(), this); pm.registerEvents(new ShopLogic(), this);
pm.registerEvents(new PreparePlayer(), this); pm.registerEvents(new PreparePlayer(), this);
pm.registerEvents(new MineWorldManager(), this);
} }

@ -0,0 +1,65 @@
package xyz.soukup.ecoCraftCore.mines;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
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.entity.Player;
import xyz.soukup.ecoCraftCore.messages.Messages;
@SuppressWarnings("UnstableApiUsage")
public class MineCommand {
public static LiteralArgumentBuilder<CommandSourceStack> createCommand() {
LiteralArgumentBuilder<CommandSourceStack> regenerate = Commands.literal("regenerate")
.executes(MineCommand::regenerateMines);
LiteralArgumentBuilder<CommandSourceStack> tp = Commands.literal("tp")
.requires(source -> source.getSender() instanceof Player)
.executes(MineCommand::teleportToMines);
return Commands.literal("mine")
.then(regenerate)
.then(tp);
}
private static int regenerateMines(CommandContext<CommandSourceStack> context) {
World world = MineWorldManager.getWorld();
if (world == null) {
Messages.send(context.getSource().getSender(), "mine.error.no-world");
return 0;
}
Messages.send(context.getSource().getSender(), "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");
}
// Delete old world, create fresh one, then generate
MineWorldManager.recreateWorld(newWorld -> MineManager.regenerate(newWorld));
return 1;
}
private static int teleportToMines(CommandContext<CommandSourceStack> context) {
Player player = (Player) context.getSource().getSender();
World world = MineWorldManager.getWorld();
if (world == null) {
Messages.send(player, "mine.error.no-world");
return 0;
}
Location spawn = MineWorldManager.getSpawnLocation();
player.teleport(spawn);
Messages.send(player, "mine.teleporting");
return 1;
}
}

@ -0,0 +1,469 @@
package xyz.soukup.ecoCraftCore.mines;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.BlockFace;
import java.util.*;
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 = 15;
private static final int FIRST_BRANCH_MIN_SIZE = 12;
private static final int FIRST_BRANCH_SPLIT_DELAY = 12; // first branch: no split chance until after 12 blocks
private static final int MAX_BRANCH_SIZE = 50;
private static final double BASE_SPLIT_INCREMENT = 0.01;
private static final double SPLIT_DECAY_PER_BRANCH = 0.000125;
private static final int BLOCKS_PER_TICK = 800;
// Biome boundaries (distance thresholds)
private static final int STONE_END = 40;
private static final int TUFF_END = 80;
private static final int DEEPSLATE_END = 120;
private static final int BASALT_END = 160;
// Blackstone: 160+
// 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 ore tables ---
private record OreEntry(Material material, double chance) {}
private static final OreEntry[] STONE_ORES = {
new OreEntry(Material.GRAVEL, 0.08),
new OreEntry(Material.COAL_ORE, 0.05),
new OreEntry(Material.COPPER_ORE, 0.04),
};
private static final OreEntry[] TUFF_ORES = {
new OreEntry(Material.IRON_ORE, 0.04),
new OreEntry(Material.GOLD_ORE, 0.025),
new OreEntry(Material.LAPIS_ORE, 0.02),
};
private static final OreEntry[] DEEPSLATE_ORES = {
new OreEntry(Material.DEEPSLATE_DIAMOND_ORE, 0.03),
new OreEntry(Material.DEEPSLATE_EMERALD_ORE, 0.015),
new OreEntry(Material.DEEPSLATE_REDSTONE_ORE, 0.02),
};
private static final OreEntry[] BASALT_ORES = {
new OreEntry(Material.RAW_IRON_BLOCK, 0.015),
new OreEntry(Material.RAW_COPPER_BLOCK, 0.015),
};
private static final OreEntry[] BLACKSTONE_ORES = {
new OreEntry(Material.ANCIENT_DEBRIS, 0.003),
new OreEntry(Material.RAW_GOLD_BLOCK, 0.01),
};
// Branch data class
private static class Branch {
final BlockFace direction;
final int startX, startY, startZ;
final int distanceFromStart;
final int maxSize;
final boolean isFirst;
double chanceToSplit;
Branch(BlockFace direction, int startX, int startY, int startZ, int distanceFromStart, int maxSize, boolean isFirst) {
this.direction = direction;
this.startX = startX;
this.startY = startY;
this.startZ = startZ;
this.distanceFromStart = distanceFromStart;
this.maxSize = maxSize;
this.isFirst = isFirst;
this.chanceToSplit = 0;
}
}
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;
}
}
public static void regenerate(World world) {
int startY = world.getMaxHeight() - 11;
int minY = world.getMinHeight() + 5;
int maxY = world.getMaxHeight();
int worldMinY = world.getMinHeight();
// Run the heavy data generation async, then schedule block placement on main thread
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
Set<Long> fillPositions = new HashSet<>();
List<BlockPlacement> fillPlacements = new ArrayList<>();
// Phase 1: Generate tunnel fill data
generateBranches(fillPlacements, fillPositions, startY, minY);
plugin.getLogger().info("Mine generation phase 1 done: " + fillPlacements.size() + " fill blocks, " + fillPositions.size() + " unique positions.");
// Phase 2: Generate wall data
List<BlockPlacement> wallPlacements = new ArrayList<>();
generateWalls(fillPositions, wallPlacements, startY);
plugin.getLogger().info("Mine generation phase 2 done: " + wallPlacements.size() + " wall blocks.");
// Phase 3: Schedule block placement on main thread in batches
Bukkit.getScheduler().runTask(plugin, () ->
scheduleBlockPlacements(world, fillPlacements, wallPlacements, startY, worldMinY, maxY));
});
}
private static void generateBranches(List<BlockPlacement> placements, Set<Long> fillPositions, int startY, int minY) {
Deque<Branch> queue = new ArrayDeque<>();
// Shared mutable counter: [0] = total branches ever created
int[] branchCount = {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, 0, startY, 0, 0, firstSize, true);
queue.add(initial);
branchCount[0]++;
plugin.getLogger().info("[MineGen] Starting generation at Y=" + startY + ", minY=" + minY);
while (!queue.isEmpty()) {
Branch branch = queue.pollFirst();
double currentIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH));
plugin.getLogger().info("[MineGen] Processing branch #" + branchCount[0]
+ " 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, startY, minY, branchCount);
}
plugin.getLogger().info("[MineGen] Branch generation complete. Total branches=" + branchCount[0] + " fillBlocks=" + fillPositions.size() + " placements=" + placements.size());
}
private static void generateSingleBranch(Branch branch, Deque<Branch> queue, List<BlockPlacement> placements,
Set<Long> fillPositions, int startY, 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;
// Generate the 11x11 cross section at this position
generateCrossSection(cx, cy, cz, branch.direction, distance, placements, fillPositions);
// 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 > startY + 5) break;
continue;
}
// Calculate the current split increment based on how many branches exist
double splitIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH));
// If increment has reached 0, no more splitting is possible — skip the roll entirely
if (splitIncrement <= 0) {
cx += branch.direction.getModX();
cy += branch.direction.getModY();
cz += branch.direction.getModZ();
if (cy < minY || cy > startY + 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);
// Skip UP if it would go above startY
if (dir == BlockFace.UP && cy + branchMaxSize > startY) {
plugin.getLogger().info("[MineGen] Skipped UP branch at Y=" + cy + " (would exceed startY=" + startY + ")");
continue;
}
// Skip DOWN if it would go below minY
if (dir == BlockFace.DOWN && cy - branchMaxSize < minY) {
plugin.getLogger().info("[MineGen] Skipped DOWN branch at Y=" + cy + " (would go below minY=" + minY + ")");
continue;
}
queue.add(new Branch(dir, cx, cy, cz, distance, branchMaxSize, false));
branchCount[0]++;
created++;
}
plugin.getLogger().info("[MineGen] Split at step=" + step + " pos=(" + cx + "," + cy + "," + cz + ") created=" + created + " branches, total=" + branchCount[0] + " splitInc=" + String.format("%.5f", splitIncrement));
}
// Advance position along the branch direction
cx += branch.direction.getModX();
cy += branch.direction.getModY();
cz += branch.direction.getModZ();
if (cy < minY || cy > startY + 5) break;
}
}
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;
}
private static void generateCrossSection(int cx, int cy, int cz, BlockFace direction, int distance,
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 = pickBlockForDistance(distance);
placements.add(new BlockPlacement(bx, by, bz, mat));
}
}
}
// --- Biome fill + ore + transition logic ---
private static Material pickBlockForDistance(int distance) {
// Determine primary biome and check if we're in a transition zone
Material primaryFill = getPrimaryFill(distance);
OreEntry[] primaryOres = getOreTable(primaryFill);
// Check transition: are we near a biome border?
int[] border = getNearestBorder(distance);
// border[0] = border distance threshold, border[1] = signed distance to border (negative = before, positive = after)
if (border != null && Math.abs(border[1]) <= TRANSITION_RANGE) {
// We're in a transition zone — blend with the neighboring biome
Material neighborFill = (border[1] <= 0)
? getNextFill(primaryFill) // approaching next biome
: getPrevFill(primaryFill); // just crossed into this biome from previous
OreEntry[] neighborOres = getOreTable(neighborFill);
// Blend ratio: 0.0 at edge of transition, 0.5 at the border itself
double blendRatio = 0.5 * (1.0 - (double) Math.abs(border[1]) / TRANSITION_RANGE);
// Roll for neighbor fill block
if (random.nextDouble() < blendRatio) {
// Use neighbor biome's fill + ores
Material ore = rollOreTable(neighborOres);
return (ore != null) ? ore : neighborFill;
}
}
// Primary biome: roll for ore
Material ore = rollOreTable(primaryOres);
return (ore != null) ? ore : primaryFill;
}
private static Material rollOreTable(OreEntry[] table) {
double roll = random.nextDouble();
double cumulative = 0;
for (OreEntry entry : table) {
cumulative += entry.chance;
if (roll < cumulative) return entry.material;
}
return null;
}
private static Material getPrimaryFill(int distance) {
if (distance < STONE_END) return Material.STONE;
if (distance < TUFF_END) return Material.TUFF;
if (distance < DEEPSLATE_END) return Material.DEEPSLATE;
if (distance < BASALT_END) return Material.SMOOTH_BASALT;
return Material.BLACKSTONE;
}
private static OreEntry[] getOreTable(Material fill) {
return switch (fill) {
case STONE -> STONE_ORES;
case TUFF -> TUFF_ORES;
case DEEPSLATE -> DEEPSLATE_ORES;
case SMOOTH_BASALT -> BASALT_ORES;
case BLACKSTONE -> BLACKSTONE_ORES;
default -> STONE_ORES;
};
}
private static Material getNextFill(Material fill) {
return switch (fill) {
case STONE -> Material.TUFF;
case TUFF -> Material.DEEPSLATE;
case DEEPSLATE -> Material.SMOOTH_BASALT;
case SMOOTH_BASALT -> Material.BLACKSTONE;
default -> Material.BLACKSTONE; // blackstone has no next
};
}
private static Material getPrevFill(Material fill) {
return switch (fill) {
case TUFF -> Material.STONE;
case DEEPSLATE -> Material.TUFF;
case SMOOTH_BASALT -> Material.DEEPSLATE;
case BLACKSTONE -> Material.SMOOTH_BASALT;
default -> Material.STONE; // stone has no prev
};
}
/**
* Returns the nearest biome border info, or null if not near any.
* Result: [0] = border distance, [1] = signed distance to border (negative = before border, positive = after)
*/
private static int[] getNearestBorder(int distance) {
int[] borders = {STONE_END, TUFF_END, DEEPSLATE_END, BASALT_END};
int closestDist = Integer.MAX_VALUE;
int closestBorder = -1;
for (int b : borders) {
int diff = distance - b;
if (Math.abs(diff) < Math.abs(closestDist)) {
closestDist = diff;
closestBorder = b;
}
}
if (closestBorder == -1 || Math.abs(closestDist) > TRANSITION_RANGE) return null;
return new int[]{closestBorder, closestDist};
}
// --- Perpendicular axes ---
private static int[][] getPerpOffsets(BlockFace dir) {
return switch (dir) {
case UP, DOWN -> new int[][]{{1, 0, 0}, {0, 0, 1}};
case NORTH, SOUTH -> new int[][]{{1, 0, 0}, {0, 1, 0}};
case EAST, WEST -> new int[][]{{0, 0, 1}, {0, 1, 0}};
default -> new int[][]{{1, 0, 0}, {0, 0, 1}};
};
}
// --- Wall generation ---
private static void generateWalls(Set<Long> fillPositions, List<BlockPlacement> wallPlacements, int startY) {
Set<Long> wallPositions = new HashSet<>();
plugin.getLogger().info("[MineGen] Generating 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.POLISHED_BLACKSTONE));
}
}
}
}
}
// --- Block placement scheduling ---
private static void scheduleBlockPlacements(World world, List<BlockPlacement> fillPlacements,
List<BlockPlacement> wallPlacements, int startY, int minY, int maxY) {
List<BlockPlacement> allPlacements = new ArrayList<>(fillPlacements.size() + wallPlacements.size() + 200);
allPlacements.addAll(fillPlacements);
allPlacements.addAll(wallPlacements);
// 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 (" + fillPlacements.size() + " fill + " + wallPlacements.size() + " walls) 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;
}
}

@ -0,0 +1,275 @@
package xyz.soukup.ecoCraftCore.mines;
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.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
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;
import xyz.soukup.ecoCraftCore.islands.IslandLoader;
import java.util.function.Consumer;
import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin;
public class MineWorldManager implements Listener {
public static final String MINE_WORLD_NAME = "mine_world";
private static World mineWorld;
public static void init() {
AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance();
IslandLoader loader = new IslandLoader();
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
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) {
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");
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 finalWorld = slimeWorld;
Bukkit.getScheduler().runTask(plugin, () -> {
asp.loadWorld(finalWorld, true);
mineWorld = Bukkit.getWorld(MINE_WORLD_NAME);
if (mineWorld != null) {
mineWorld.setGameRule(GameRule.DO_MOB_SPAWNING, false);
mineWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
mineWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
mineWorld.setTime(6000);
plugin.getLogger().info("Mine world loaded successfully.");
}
});
} catch (Exception e) {
e.printStackTrace();
plugin.getLogger().severe("Failed to load mine world.");
}
});
}
public static World getWorld() {
return mineWorld;
}
public static Location getSpawnLocation() {
if (mineWorld == null) return null;
int startY = mineWorld.getMaxHeight() - 10;
return new Location(mineWorld, 5, startY + 1, 5);
}
/**
* 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) {
AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance();
IslandLoader loader = new IslandLoader();
plugin.getLogger().info("[MineWorld] Unloading old mine world...");
// Unload the old world (no save - we're deleting it anyway)
if (mineWorld != null) {
Bukkit.unloadWorld(mineWorld, false);
mineWorld = null;
}
// Delete and recreate async
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
// Delete old world data from the loader/DB
if (loader.worldExists(MINE_WORLD_NAME)) {
loader.deleteWorld(MINE_WORLD_NAME);
plugin.getLogger().info("[MineWorld] Old mine world deleted.");
}
// Create fresh empty world
SlimePropertyMap props = new SlimePropertyMap();
props.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);
// Load the new world on the main thread
Bukkit.getScheduler().runTask(plugin, () -> {
asp.loadWorld(slimeWorld, true);
mineWorld = Bukkit.getWorld(MINE_WORLD_NAME);
if (mineWorld != null) {
mineWorld.setGameRule(GameRule.DO_MOB_SPAWNING, false);
mineWorld.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
mineWorld.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
mineWorld.setTime(6000);
plugin.getLogger().info("[MineWorld] Fresh mine world created and loaded.");
callback.accept(mineWorld);
} else {
plugin.getLogger().severe("[MineWorld] Failed to load fresh mine world.");
}
});
} catch (Exception e) {
e.printStackTrace();
plugin.getLogger().severe("[MineWorld] Failed to recreate mine world.");
}
});
}
private boolean isInMineWorld(Player player) {
return player.getWorld().equals(mineWorld);
}
private boolean isInMineWorld(World world) {
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();
if (isInMineWorld(player)) {
applyMiningFatigue(player);
}
}
// Re-apply fatigue on respawn in mine world
@EventHandler
public void onDeath(PlayerDeathEvent event) {
Bukkit.getScheduler().runTaskLater(plugin, () -> {
Player player = event.getPlayer();
if (isInMineWorld(player)) {
applyMiningFatigue(player);
}
}, 1L);
}
private void applyMiningFatigue(Player player) {
player.addPotionEffect(new PotionEffect(
PotionEffectType.MINING_FATIGUE,
Integer.MAX_VALUE,
0, // level I (amplifier 0)
true, // ambient
false, // no particles
false // no icon
));
}
// 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;
event.setDamage(event.getDamage() * 2);
}
// Prevent entity explosions (creepers, tnt, etc.) from destroying polished blackstone
@EventHandler
public void onEntityExplode(EntityExplodeEvent event) {
if (!isInMineWorld(event.getEntity().getWorld())) return;
event.blockList().removeIf(block -> block.getType() == Material.POLISHED_BLACKSTONE);
}
// Prevent block explosions (beds, respawn anchors, etc.) from destroying polished blackstone
@EventHandler
public void onBlockExplode(BlockExplodeEvent event) {
if (!isInMineWorld(event.getBlock().getWorld())) return;
event.blockList().removeIf(block -> block.getType() == Material.POLISHED_BLACKSTONE);
}
// Block break logic: prevent polished blackstone, handle mining sequence
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
if (!isInMineWorld(event.getPlayer())) return;
Material type = event.getBlock().getType();
// Polished blackstone is unbreakable (mine walls)
if (type == Material.POLISHED_BLACKSTONE) {
event.setCancelled(true);
return;
}
// Mining sequence: each block degrades to the next tier before breaking
Material nextTier = getNextTier(type);
if (nextTier != null) {
event.setCancelled(true);
event.getBlock().setType(nextTier);
event.getBlock().getState().update(true);
// Damage the tool manually since we cancelled the event (only if the tool has durability)
ItemStack tool = event.getPlayer().getInventory().getItemInMainHand();
if (!tool.getType().isAir() && tool.getType().getMaxDurability() > 0 && tool.getItemMeta() instanceof Damageable damageable) {
damageable.setDamage(damageable.getDamage() + 2); // doubled durability
tool.setItemMeta(damageable);
// Break tool if fully damaged
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);
}
}
}
// Stone breaks normally (event not cancelled)
}
private Material getNextTier(Material material) {
return switch (material) {
case BLACKSTONE -> Material.SMOOTH_BASALT;
case SMOOTH_BASALT -> Material.DEEPSLATE;
case DEEPSLATE -> Material.TUFF;
case TUFF -> Material.STONE;
default -> null; // Stone and ores break normally
};
}
}

@ -65,4 +65,11 @@ gui:
title: "<dark_green>Obchod" title: "<dark_green>Obchod"
buy: "<green>Koupit <amount>ks za <price>$" buy: "<green>Koupit <amount>ks za <price>$"
sell: "<yellow>Prodat <amount>ks za <price>$" sell: "<yellow>Prodat <amount>ks za <price>$"
mine:
regenerating: "<green>Regenerace dolů začala..."
regenerate-complete: "<green>Regenerace dolů dokončena."
teleporting: "<green>Teleportuješ se do dolů."
teleported-out: "<yellow>Byl jsi teleportován z dolů kvůli regeneraci."
error:
no-world: "<red>Důlní svět není načtený."

Loading…
Cancel
Save