Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0dd69c9069 | 2 weeks ago |
5 changed files with 826 additions and 2 deletions
@ -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
|
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in new issue