Compare commits
18 Commits
island-exp
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
64850aaf45 | 3 weeks ago |
|
|
11f0a1e910 | 3 weeks ago |
|
|
04436cb6ab | 3 weeks ago |
|
|
cc6cac939f | 3 weeks ago |
|
|
b34c89f1c2 | 3 weeks ago |
|
|
300cb801da | 3 weeks ago |
|
|
375f5983af | 4 weeks ago |
|
|
4f6bfdf26d | 1 month ago |
|
|
d664f51214 | 1 month ago |
|
|
8126729ea7 | 1 month ago |
|
|
e59547c73f | 1 month ago |
|
|
13e411f97d | 1 month ago |
|
|
06dcad47ab | 1 month ago |
|
|
0d24e2a9ef | 1 month ago |
|
|
d5f76058a8 | 1 month ago |
|
|
29742f576b | 1 month ago |
|
|
6c398ed9a1 | 1 month ago |
|
|
0dd69c9069 | 2 months ago |
32 changed files with 2557 additions and 55 deletions
@ -0,0 +1,18 @@ |
|||||||
|
package xyz.soukup.ecoCraftCore.messages; |
||||||
|
|
||||||
|
import org.bukkit.event.EventHandler; |
||||||
|
import org.bukkit.event.Listener; |
||||||
|
import org.bukkit.event.player.PlayerJoinEvent; |
||||||
|
import org.bukkit.event.player.PlayerQuitEvent; |
||||||
|
|
||||||
|
public class JoinLeaveMessageSupress implements Listener { |
||||||
|
@EventHandler |
||||||
|
public void onJoin(PlayerJoinEvent event) { |
||||||
|
event.joinMessage(null); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onQuit(PlayerQuitEvent event) { |
||||||
|
event.quitMessage(null); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,75 @@ |
|||||||
|
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.command.CommandSender; |
||||||
|
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) { |
||||||
|
CommandSender sender = context.getSource().getSender(); |
||||||
|
World world = MineWorldManager.getWorld(); |
||||||
|
|
||||||
|
if (world == null) { |
||||||
|
Messages.send(sender, "mine.error.no-world"); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
Messages.send(sender, "mine.regenerating"); |
||||||
|
|
||||||
|
for (Player player : world.getPlayers()) { |
||||||
|
player.teleport(player.getServer().getWorlds().getFirst().getSpawnLocation()); |
||||||
|
Messages.send(player, "mine.teleported-out"); |
||||||
|
} |
||||||
|
|
||||||
|
MineWorldManager.recreateWorld(newWorld -> { |
||||||
|
boolean success = MineManager.regenerate(newWorld); |
||||||
|
if (success) { |
||||||
|
Messages.send(sender, "mine.regenerate-complete"); |
||||||
|
} else { |
||||||
|
Messages.send(sender, "mine.error.regenerate-failed"); |
||||||
|
} |
||||||
|
}); |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
private static int teleportToMines(CommandContext<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(); |
||||||
|
if (spawn == null) { |
||||||
|
Messages.send(player, "mine.error.no-world"); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
player.teleport(spawn); |
||||||
|
Messages.send(player, "mine.teleporting"); |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
package xyz.soukup.ecoCraftCore.mines; |
||||||
|
|
||||||
|
import org.bukkit.Location; |
||||||
|
import org.bukkit.World; |
||||||
|
import xyz.soukup.ecoCraftCore.mines.generation.MineGenerationConfig; |
||||||
|
import xyz.soukup.ecoCraftCore.mines.generation.MineGenerator; |
||||||
|
import xyz.soukup.ecoCraftCore.mines.generation.MineRoomLibrary; |
||||||
|
|
||||||
|
import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; |
||||||
|
|
||||||
|
public final class MineManager { |
||||||
|
|
||||||
|
private MineManager() { |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean regenerate(World world) { |
||||||
|
MineGenerationConfig generationConfig = MineGenerationConfig.fromConfiguration(plugin.getConfig(), world); |
||||||
|
MineRoomLibrary roomLibrary = MineRoomLibrary.load( |
||||||
|
plugin.getDataFolder(), |
||||||
|
generationConfig.roomsDirectory(), |
||||||
|
plugin.getLogger() |
||||||
|
); |
||||||
|
|
||||||
|
if (roomLibrary.isEmpty()) { |
||||||
|
plugin.getLogger().severe("[MineGen] No valid room prefabs were loaded from: " |
||||||
|
+ roomLibrary.roomsDirectory().getAbsolutePath()); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
MineGenerator.Result generationResult = new MineGenerator( |
||||||
|
world, |
||||||
|
generationConfig, |
||||||
|
roomLibrary, |
||||||
|
plugin.getLogger() |
||||||
|
).generate(); |
||||||
|
|
||||||
|
if (!generationResult.success()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
Location spawn = getConfiguredSpawnLocation(world); |
||||||
|
world.setSpawnLocation(spawn); |
||||||
|
|
||||||
|
plugin.getLogger().info("[MineGen] Mine generated with " + generationResult.roomsPlaced() + " rooms."); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
public static Location getConfiguredSpawnLocation(World world) { |
||||||
|
Location fallback = new Location(world, 5.5, world.getMaxHeight() - 9, 5.5, 0f, 0f); |
||||||
|
|
||||||
|
if (!plugin.getConfig().isConfigurationSection("mine.spawn")) { |
||||||
|
return fallback; |
||||||
|
} |
||||||
|
|
||||||
|
double x = plugin.getConfig().getDouble("mine.spawn.x", fallback.getX()); |
||||||
|
double y = plugin.getConfig().getDouble("mine.spawn.y", fallback.getY()); |
||||||
|
double z = plugin.getConfig().getDouble("mine.spawn.z", fallback.getZ()); |
||||||
|
float yaw = (float) plugin.getConfig().getDouble("mine.spawn.yaw", 0.0); |
||||||
|
float pitch = (float) plugin.getConfig().getDouble("mine.spawn.pitch", 0.0); |
||||||
|
return new Location(world, x, y, z, yaw, pitch); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,230 @@ |
|||||||
|
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.Bukkit; |
||||||
|
import org.bukkit.GameRule; |
||||||
|
import org.bukkit.Location; |
||||||
|
import org.bukkit.Material; |
||||||
|
import org.bukkit.World; |
||||||
|
import org.bukkit.entity.Player; |
||||||
|
import org.bukkit.event.EventHandler; |
||||||
|
import org.bukkit.event.Listener; |
||||||
|
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.potion.PotionEffect; |
||||||
|
import org.bukkit.potion.PotionEffectType; |
||||||
|
import xyz.soukup.ecoCraftCore.database.objects.Island; |
||||||
|
import xyz.soukup.ecoCraftCore.islands.DatabaseIslandLoader; |
||||||
|
|
||||||
|
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(); |
||||||
|
DatabaseIslandLoader loader = new DatabaseIslandLoader(); |
||||||
|
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { |
||||||
|
try { |
||||||
|
SlimeWorld slimeWorld = null; |
||||||
|
|
||||||
|
if (loader.worldExists(MINE_WORLD_NAME)) { |
||||||
|
try { |
||||||
|
slimeWorld = asp.readWorld(loader, MINE_WORLD_NAME, false, new SlimePropertyMap()); |
||||||
|
} catch (Exception exception) { |
||||||
|
plugin.getLogger().warning("Mine world data is corrupted, recreating..."); |
||||||
|
loader.deleteWorld(MINE_WORLD_NAME); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (slimeWorld == null) { |
||||||
|
SlimePropertyMap properties = new SlimePropertyMap(); |
||||||
|
properties.setValue(SlimeProperties.ENVIRONMENT, "NORMAL"); |
||||||
|
|
||||||
|
Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null); |
||||||
|
island.save(); |
||||||
|
|
||||||
|
slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, properties, 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); |
||||||
|
if (plugin.getConfig().getBoolean("mine.paste-on-startup", false)) { |
||||||
|
MineManager.regenerate(mineWorld); |
||||||
|
} |
||||||
|
plugin.getLogger().info("Mine world loaded successfully."); |
||||||
|
} |
||||||
|
}); |
||||||
|
} catch (Exception exception) { |
||||||
|
exception.printStackTrace(); |
||||||
|
plugin.getLogger().severe("Failed to load mine world."); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public static World getWorld() { |
||||||
|
return mineWorld; |
||||||
|
} |
||||||
|
|
||||||
|
public static Location getSpawnLocation() { |
||||||
|
if (mineWorld == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return MineManager.getConfiguredSpawnLocation(mineWorld); |
||||||
|
} |
||||||
|
|
||||||
|
public static void recreateWorld(Consumer<World> callback) { |
||||||
|
AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); |
||||||
|
DatabaseIslandLoader loader = new DatabaseIslandLoader(); |
||||||
|
|
||||||
|
plugin.getLogger().info("[MineWorld] Unloading old mine world..."); |
||||||
|
|
||||||
|
if (mineWorld != null) { |
||||||
|
Bukkit.unloadWorld(mineWorld, false); |
||||||
|
mineWorld = null; |
||||||
|
} |
||||||
|
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { |
||||||
|
try { |
||||||
|
loader.deleteWorld(MINE_WORLD_NAME); |
||||||
|
plugin.getLogger().info("[MineWorld] Old mine world metadata deleted."); |
||||||
|
|
||||||
|
SlimePropertyMap properties = new SlimePropertyMap(); |
||||||
|
properties.setValue(SlimeProperties.ENVIRONMENT, "NORMAL"); |
||||||
|
|
||||||
|
Island island = new Island("mine", MINE_WORLD_NAME, "Mine World", "Shared mine world", "server", "system", null); |
||||||
|
island.save(); |
||||||
|
|
||||||
|
SlimeWorld slimeWorld = asp.createEmptyWorld(MINE_WORLD_NAME, false, properties, loader); |
||||||
|
|
||||||
|
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 exception) { |
||||||
|
exception.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); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onWorldChange(PlayerChangedWorldEvent event) { |
||||||
|
Player player = event.getPlayer(); |
||||||
|
|
||||||
|
if (isInMineWorld(player)) { |
||||||
|
applyMiningFatigue(player); |
||||||
|
} |
||||||
|
|
||||||
|
if (isInMineWorld(event.getFrom())) { |
||||||
|
player.removePotionEffect(PotionEffectType.MINING_FATIGUE); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onJoin(PlayerJoinEvent event) { |
||||||
|
Player player = event.getPlayer(); |
||||||
|
if (isInMineWorld(player)) { |
||||||
|
applyMiningFatigue(player); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@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, |
||||||
|
true, |
||||||
|
false, |
||||||
|
false |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onItemDamage(PlayerItemDamageEvent event) { |
||||||
|
if (!isInMineWorld(event.getPlayer())) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (event.getItem().getType().getMaxDurability() <= 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
event.setDamage(event.getDamage() * 2); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onEntityExplode(EntityExplodeEvent event) { |
||||||
|
if (!isInMineWorld(event.getEntity().getWorld())) { |
||||||
|
return; |
||||||
|
} |
||||||
|
event.blockList().removeIf(block -> block.getType() == Material.BEDROCK); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onBlockExplode(BlockExplodeEvent event) { |
||||||
|
if (!isInMineWorld(event.getBlock().getWorld())) { |
||||||
|
return; |
||||||
|
} |
||||||
|
event.blockList().removeIf(block -> block.getType() == Material.BEDROCK); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onBlockBreak(BlockBreakEvent event) { |
||||||
|
if (!isInMineWorld(event.getPlayer())) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (event.getBlock().getType() == Material.BEDROCK) { |
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -1,36 +1,319 @@ |
|||||||
package xyz.soukup.ecoCraftCore.regions; |
package xyz.soukup.ecoCraftCore.regions; |
||||||
|
|
||||||
|
import org.bukkit.Location; |
||||||
|
import org.bukkit.NamespacedKey; |
||||||
|
import org.bukkit.block.Block; |
||||||
|
import org.bukkit.entity.Entity; |
||||||
|
import org.bukkit.entity.Item; |
||||||
|
import org.bukkit.entity.Minecart; |
||||||
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; |
||||||
import org.bukkit.event.player.PlayerInteractEvent; |
import org.bukkit.event.block.Action; |
||||||
import org.bukkit.event.player.PlayerJoinEvent; |
import org.bukkit.event.block.BlockBreakEvent; |
||||||
|
import org.bukkit.event.block.BlockPlaceEvent; |
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent; |
||||||
|
import org.bukkit.event.inventory.InventoryPickupItemEvent; |
||||||
|
import org.bukkit.event.player.*; |
||||||
|
import org.bukkit.event.vehicle.VehicleDestroyEvent; |
||||||
|
import org.bukkit.event.vehicle.VehicleEntityCollisionEvent; |
||||||
|
import org.bukkit.inventory.ItemStack; |
||||||
|
import org.bukkit.persistence.PersistentDataType; |
||||||
import xyz.soukup.ecoCraftCore.database.objects.Region; |
import xyz.soukup.ecoCraftCore.database.objects.Region; |
||||||
import xyz.soukup.ecoCraftCore.database.objects.RegionMember; |
import xyz.soukup.ecoCraftCore.database.objects.RegionMember; |
||||||
|
|
||||||
|
import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; |
||||||
|
|
||||||
public class RegionEvents implements Listener { |
public class RegionEvents implements Listener { |
||||||
|
|
||||||
@EventHandler |
@EventHandler |
||||||
public void onPlayerInteract(PlayerInteractEvent event){ |
public void onPlayerInteract(PlayerInteractEvent event){ |
||||||
event.setCancelled(!isRegionMember(event.getClickedBlock().getWorld().getName(), event.getPlayer(), event.getClickedBlock().getX(), event.getClickedBlock().getY())); |
switch (event.getAction()) { |
||||||
|
case RIGHT_CLICK_BLOCK: |
||||||
|
case LEFT_CLICK_BLOCK: |
||||||
|
break; |
||||||
|
default: |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
Block block = event.getClickedBlock(); |
||||||
|
ItemStack item = event.getItem(); |
||||||
|
if (block == null) return; |
||||||
|
|
||||||
|
if (item != null && item.getType().isBlock() && !block.getType().isInteractable()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
block.getWorld().getName(), |
||||||
|
event.getPlayer(), |
||||||
|
block.getX(), |
||||||
|
block.getY(), |
||||||
|
block.getZ() |
||||||
|
); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
} |
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onPlayerInteractPhysical(PlayerInteractEvent event){ |
||||||
|
if (!event.getAction().equals(Action.PHYSICAL)){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
private boolean isRegionMember(String island, Player player, int x, int y){ |
Player player = event.getPlayer(); |
||||||
Region region = Region.findRegion(x, y, island); |
Location location = player.getLocation(); |
||||||
|
|
||||||
if (region == null){ |
boolean allowed = isAllowedToInteract( |
||||||
return false; |
location.getWorld().getName(), |
||||||
} |
player, |
||||||
|
location.getBlockX(), |
||||||
|
location.getBlockY(), |
||||||
|
location.getBlockZ() |
||||||
|
); |
||||||
|
|
||||||
String name = player.getName(); |
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
for (RegionMember regionMember : region.getRegionMembers()){ |
event.setCancelled(true); |
||||||
if (regionMember.getMembertype().equals("player") && regionMember.getName().equals(name)){ |
|
||||||
return true; |
} |
||||||
} |
|
||||||
} |
@EventHandler |
||||||
|
public void onBlockPlace(BlockPlaceEvent event) { |
||||||
|
Block block = event.getBlockPlaced(); |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
block.getWorld().getName(), |
||||||
|
event.getPlayer(), |
||||||
|
block.getX(), |
||||||
|
block.getY(), |
||||||
|
block.getZ() |
||||||
|
); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onBlockBreak(BlockBreakEvent event) { |
||||||
|
|
||||||
|
Block block = event.getBlock(); |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
block.getWorld().getName(), |
||||||
|
event.getPlayer(), |
||||||
|
block.getX(), |
||||||
|
block.getY(), |
||||||
|
block.getZ() |
||||||
|
); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onBucketUse(PlayerBucketEmptyEvent event) { |
||||||
|
Block clickedBlock = event.getBlockClicked(); |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
clickedBlock.getWorld().getName(), |
||||||
|
event.getPlayer(), |
||||||
|
clickedBlock.getX(), |
||||||
|
clickedBlock.getY(), |
||||||
|
clickedBlock.getZ() |
||||||
|
); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onBucketUse(PlayerBucketFillEvent event) { |
||||||
|
Block clickedBlock = event.getBlockClicked(); |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
clickedBlock.getWorld().getName(), |
||||||
|
event.getPlayer(), |
||||||
|
clickedBlock.getX(), |
||||||
|
clickedBlock.getY(), |
||||||
|
clickedBlock.getZ() |
||||||
|
); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onEntityInteract(PlayerInteractEntityEvent event){ |
||||||
|
Entity entity = event.getRightClicked(); |
||||||
|
Location location = entity.getLocation(); |
||||||
|
|
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
location.getWorld().getName(), |
||||||
|
event.getPlayer(), |
||||||
|
location.getBlockX(), |
||||||
|
location.getBlockY(), |
||||||
|
location.getBlockZ()); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onEntityHit(EntityDamageByEntityEvent event){ |
||||||
|
|
||||||
|
if (!(event.getDamager() instanceof Player player)){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Entity entity = event.getEntity(); |
||||||
|
Location location = entity.getLocation(); |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
location.getWorld().getName(), |
||||||
|
player, |
||||||
|
location.getBlockX(), |
||||||
|
location.getBlockY(), |
||||||
|
location.getBlockZ()); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onMinecartPush(VehicleEntityCollisionEvent event) { |
||||||
|
|
||||||
|
|
||||||
|
if (!(event.getEntity() instanceof Player player)){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Location location = event.getVehicle().getLocation(); |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
location.getWorld().getName(), |
||||||
|
player, |
||||||
|
location.getBlockX(), |
||||||
|
location.getBlockY(), |
||||||
|
location.getBlockZ()); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
//Preventing hopper interaction :P
|
||||||
|
@EventHandler |
||||||
|
public void onDrop(PlayerDropItemEvent event) { |
||||||
|
Player player = event.getPlayer(); |
||||||
|
Location location = player.getLocation(); |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
location.getWorld().getName(), |
||||||
|
player, |
||||||
|
location.getBlockX(), |
||||||
|
location.getBlockY(), |
||||||
|
location.getBlockZ()); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Item item = event.getItemDrop(); |
||||||
|
|
||||||
|
item.getPersistentDataContainer().set(new NamespacedKey(plugin, "hopper_stopper"), PersistentDataType.BYTE, (byte) 1); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onHopperPickup(InventoryPickupItemEvent event) { |
||||||
|
Item item = event.getItem(); |
||||||
|
|
||||||
|
if (!item.getPersistentDataContainer().has(new NamespacedKey(plugin, "hopper_stopper"), PersistentDataType.BYTE)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onVehicleDestroy(VehicleDestroyEvent event) { |
||||||
|
if (!(event.getAttacker() instanceof Player player)){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Location location = event.getVehicle().getLocation(); |
||||||
|
|
||||||
|
boolean allowed = isAllowedToInteract( |
||||||
|
location.getWorld().getName(), |
||||||
|
player, |
||||||
|
location.getBlockX(), |
||||||
|
location.getBlockY(), |
||||||
|
location.getBlockZ()); |
||||||
|
|
||||||
|
if (allowed){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private boolean isAllowedToInteract(String island, Player player, Location location){ |
||||||
|
return isAllowedToInteract(island, player, location.getBlockX(), location.getBlockY(), location.getBlockZ()); |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isAllowedToInteract(String island, Player player, int x, int y, int z){ |
||||||
|
if (player.isOp()){ |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
Region region = Region.findRegion(x, y, z, island); |
||||||
|
|
||||||
|
if (region == null){ |
||||||
return false; |
return false; |
||||||
|
} |
||||||
|
|
||||||
|
String name = player.getName(); |
||||||
|
|
||||||
|
for (RegionMember regionMember : region.getRegionMembers()){ |
||||||
|
|
||||||
|
if (regionMember.getMembertype().equals("player") && regionMember.getName().equals(name)){ |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
} |
} |
||||||
|
|
||||||
|
|
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,187 @@ |
|||||||
|
package xyz.soukup.ecoCraftCore.sign; |
||||||
|
|
||||||
|
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 io.papermc.paper.dialog.Dialog; |
||||||
|
import io.papermc.paper.dialog.DialogResponseView; |
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton; |
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase; |
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction; |
||||||
|
import io.papermc.paper.registry.data.dialog.input.DialogInput; |
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType; |
||||||
|
import net.kyori.adventure.audience.Audience; |
||||||
|
import net.kyori.adventure.text.Component; |
||||||
|
import net.kyori.adventure.text.event.ClickCallback; |
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage; |
||||||
|
import org.bukkit.Location; |
||||||
|
import org.bukkit.block.Block; |
||||||
|
import org.bukkit.block.BlockState; |
||||||
|
import org.bukkit.block.Sign; |
||||||
|
import org.bukkit.block.sign.Side; |
||||||
|
import org.bukkit.block.sign.SignSide; |
||||||
|
import org.bukkit.entity.Player; |
||||||
|
import org.bukkit.persistence.PersistentDataType; |
||||||
|
import xyz.soukup.ecoCraftCore.database.objects.Shop; |
||||||
|
import xyz.soukup.ecoCraftCore.messages.Messages; |
||||||
|
import xyz.soukup.ecoCraftCore.regions.RegionManager; |
||||||
|
import xyz.soukup.ecoCraftCore.utilities.PDC; |
||||||
|
|
||||||
|
import javax.sound.sampled.Line; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
|
||||||
|
public class SignEditCommand { |
||||||
|
|
||||||
|
private static final HashMap<Player, List<String>> clipBoard = new HashMap<>(); |
||||||
|
|
||||||
|
public static LiteralArgumentBuilder<CommandSourceStack> getCommand(){ |
||||||
|
return Commands.literal("edit-sign") |
||||||
|
.requires(source -> source.getSender() instanceof Player) |
||||||
|
.executes(SignEditCommand::openEditMenu); |
||||||
|
} |
||||||
|
|
||||||
|
private static int openEditMenu(CommandContext<CommandSourceStack> context){ |
||||||
|
Player player = (Player) context.getSource().getSender(); |
||||||
|
Block block = player.getTargetBlock(null, 10); |
||||||
|
BlockState blockState = block.getState(); |
||||||
|
|
||||||
|
if (!(blockState instanceof Sign sign)){ |
||||||
|
Messages.send(player, "sign-edit.error.not-sign"); |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
Location location = sign.getLocation(); |
||||||
|
|
||||||
|
if (!RegionManager.isAllowedToInteract(player, location)){ |
||||||
|
Messages.send(player, "region.error.not-allowed-to-interact"); |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
Dialog dialog = buildEditDialog(sign, player, null); |
||||||
|
player.showDialog(dialog); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static Dialog buildEditDialog(Sign sign, Player player, List<String> lines){ |
||||||
|
|
||||||
|
if (lines == null){ |
||||||
|
lines = getLines(sign); |
||||||
|
} |
||||||
|
|
||||||
|
List<ActionButton> actionButtons = new ArrayList<>(); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
actionButtons.add(ActionButton.builder(Messages.get("gui.sign-edit.buttons.copy")) |
||||||
|
.action(DialogAction.customClick((view, audience) -> copySignText(view, player, sign), ClickCallback.Options.builder().build())) |
||||||
|
.build()); |
||||||
|
actionButtons.add(ActionButton.builder(Messages.get("gui.sign-edit.buttons.paste")) |
||||||
|
.action(DialogAction.customClick((view, audience) -> pasteSignText(sign, player), ClickCallback.Options.builder().build())) |
||||||
|
.build()); |
||||||
|
actionButtons.add(ActionButton.builder(Messages.get("gui.sign-edit.buttons.confirm")) |
||||||
|
.action(DialogAction.customClick((view, audience) -> editSign(view, sign, player), ClickCallback.Options.builder().build())) |
||||||
|
.build()); |
||||||
|
actionButtons.add(ActionButton.builder(Messages.get("gui.sign-edit.buttons.cancel")) |
||||||
|
.action(DialogAction.customClick((view, audience) -> audience.closeDialog(), ClickCallback.Options.builder().build())) |
||||||
|
.build()); |
||||||
|
|
||||||
|
|
||||||
|
List<String> finalLines = lines; |
||||||
|
|
||||||
|
return Dialog.create(builder -> builder.empty() |
||||||
|
.base(DialogBase.builder(Messages.get("gui.sign-edit.title")) |
||||||
|
.inputs(List.of( |
||||||
|
DialogInput.text("line1", Messages.get("gui.sign-edit.inputs.line1")) |
||||||
|
.initial(finalLines.getFirst()) |
||||||
|
.maxLength(256) |
||||||
|
.build(), |
||||||
|
DialogInput.text("line2", Messages.get("gui.sign-edit.inputs.line2")) |
||||||
|
.initial(finalLines.get(1)) |
||||||
|
.maxLength(256) |
||||||
|
.build(), |
||||||
|
DialogInput.text("line3", Messages.get("gui.sign-edit.inputs.line3")) |
||||||
|
.initial(finalLines.get(2)) |
||||||
|
.maxLength(256) |
||||||
|
.build(), |
||||||
|
DialogInput.text("line4", Messages.get("gui.sign-edit.inputs.line4")) |
||||||
|
.initial(finalLines.get(3)) |
||||||
|
.maxLength(256) |
||||||
|
.build() |
||||||
|
)) |
||||||
|
.build()) |
||||||
|
.type(DialogType.multiAction(actionButtons).build()) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private static List<String> getLines(Sign sign){ |
||||||
|
List<String> lines = new ArrayList<>(); |
||||||
|
|
||||||
|
if (PDC.getUniversal(sign, "line1", PersistentDataType.STRING) != null){ |
||||||
|
for (int i = 1; i<5; i++){ |
||||||
|
lines.add((String) PDC.getUniversal(sign, "line" + i,PersistentDataType.STRING)); |
||||||
|
} |
||||||
|
return lines; |
||||||
|
} |
||||||
|
|
||||||
|
SignSide signSide = sign.getSide(Side.FRONT); |
||||||
|
List<Component> componentLines = signSide.lines(); |
||||||
|
|
||||||
|
for (Component componentLine : componentLines){ |
||||||
|
lines.add(MiniMessage.miniMessage().serialize(componentLine)); |
||||||
|
} |
||||||
|
|
||||||
|
return lines; |
||||||
|
} |
||||||
|
|
||||||
|
private static void copySignText(DialogResponseView view, Player player, Sign sign){ |
||||||
|
List<String> lines = new ArrayList<>(); |
||||||
|
|
||||||
|
lines.add(view.getText("line1")); |
||||||
|
lines.add(view.getText("line2")); |
||||||
|
lines.add(view.getText("line3")); |
||||||
|
lines.add(view.getText("line4")); |
||||||
|
|
||||||
|
clipBoard.put(player, lines); |
||||||
|
Messages.send(player, "gui.sign-edit.success.copied"); |
||||||
|
pasteSignText(sign, player); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static void pasteSignText(Sign sign, Player player){ |
||||||
|
Dialog dialog = buildEditDialog(sign, player, clipBoard.get(player)); |
||||||
|
player.showDialog(dialog); |
||||||
|
} |
||||||
|
|
||||||
|
private static void editSign(DialogResponseView view, Sign sign, Audience audience){ |
||||||
|
MiniMessage mm = MiniMessage.miniMessage(); |
||||||
|
SignSide signSide = sign.getSide(Side.FRONT); |
||||||
|
|
||||||
|
List<Component> lines = new ArrayList<>(); |
||||||
|
|
||||||
|
|
||||||
|
for (int i = 1; i < 5; i++) { |
||||||
|
String key = "line" + i; |
||||||
|
String line = view.getText(key); |
||||||
|
|
||||||
|
if (line == null){ |
||||||
|
line = ""; |
||||||
|
} |
||||||
|
|
||||||
|
PDC.setUniversal(sign, key, line, PersistentDataType.STRING); |
||||||
|
signSide.line(i-1, mm.deserialize(line)); |
||||||
|
|
||||||
|
} |
||||||
|
sign.update(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,142 @@ |
|||||||
|
package xyz.soukup.ecoCraftCore.sit; |
||||||
|
|
||||||
|
import org.bukkit.Location; |
||||||
|
import org.bukkit.World; |
||||||
|
import org.bukkit.block.Block; |
||||||
|
import org.bukkit.block.data.Bisected; |
||||||
|
import org.bukkit.block.data.type.Stairs; |
||||||
|
import org.bukkit.entity.ArmorStand; |
||||||
|
import org.bukkit.entity.Entity; |
||||||
|
import org.bukkit.entity.Player; |
||||||
|
import org.bukkit.event.EventHandler; |
||||||
|
import org.bukkit.event.Listener; |
||||||
|
import org.bukkit.event.block.Action; |
||||||
|
import org.bukkit.event.player.PlayerInteractEvent; |
||||||
|
import org.bukkit.event.player.PlayerQuitEvent; |
||||||
|
import org.bukkit.event.player.PlayerTeleportEvent; |
||||||
|
import org.bukkit.event.vehicle.VehicleExitEvent; |
||||||
|
import org.bukkit.persistence.PersistentDataType; |
||||||
|
import xyz.soukup.ecoCraftCore.utilities.PDC; |
||||||
|
|
||||||
|
|
||||||
|
public class LetMeSit implements Listener { |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void onStairsClick(PlayerInteractEvent event){ |
||||||
|
Player player = event.getPlayer(); |
||||||
|
|
||||||
|
if (player.isSneaking()){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Block block = event.getClickedBlock(); |
||||||
|
|
||||||
|
if (block == null){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!(block.getBlockData() instanceof Stairs stairs)){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (stairs.getHalf() == Bisected.Half.TOP){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
switch (stairs.getShape()) { |
||||||
|
case INNER_LEFT: |
||||||
|
case INNER_RIGHT: |
||||||
|
case OUTER_LEFT: |
||||||
|
case OUTER_RIGHT: |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
float yaw = switch (stairs.getFacing()) { |
||||||
|
case NORTH -> 0f; |
||||||
|
case WEST -> -90f; |
||||||
|
case EAST -> 90f; |
||||||
|
default -> 180f; |
||||||
|
}; |
||||||
|
|
||||||
|
Location location = block.getLocation(); |
||||||
|
location.add(0.5, 0.5, 0.5); |
||||||
|
location.setYaw(yaw); |
||||||
|
|
||||||
|
World world = location.getWorld(); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for (Entity entity : world.getNearbyEntities(location, 0.5, 1,0.5)){ |
||||||
|
if (!(entity instanceof ArmorStand armorStand)){ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (PDC.getUniversal(armorStand, "chair", PersistentDataType.BOOLEAN) == null){ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (!armorStand.getPassengers().isEmpty()){ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
player.setRotation(armorStand.getYaw(), 0f); |
||||||
|
armorStand.addPassenger(player); |
||||||
|
event.setCancelled(true); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
ArmorStand armorStand = world.spawn(location, ArmorStand.class, stand -> { |
||||||
|
stand.setInvisible(true); |
||||||
|
stand.setInvulnerable(true); |
||||||
|
stand.setGravity(false); |
||||||
|
stand.setSilent(true); |
||||||
|
stand.setMarker(true); |
||||||
|
}); |
||||||
|
|
||||||
|
PDC.setUniversal(armorStand, "chair", true, PersistentDataType.BOOLEAN); |
||||||
|
player.setRotation(yaw, 0f); |
||||||
|
armorStand.addPassenger(player); |
||||||
|
event.setCancelled(true); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void unmount(VehicleExitEvent event){ |
||||||
|
Location location = event.getVehicle().getLocation(); |
||||||
|
cleanArmorStand(location); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void playerLeave(PlayerQuitEvent event){ |
||||||
|
Location location = event.getPlayer().getLocation(); |
||||||
|
cleanArmorStand(location); |
||||||
|
} |
||||||
|
|
||||||
|
@EventHandler |
||||||
|
public void teleport(PlayerTeleportEvent event){ |
||||||
|
Location location = event.getFrom(); |
||||||
|
cleanArmorStand(location); |
||||||
|
} |
||||||
|
|
||||||
|
private void cleanArmorStand(Location location){ |
||||||
|
World world = location.getWorld(); |
||||||
|
for (Entity entity : world.getNearbyEntities(location, 0.5, 1,0.5)){ |
||||||
|
if (!(entity instanceof ArmorStand armorStand)){ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (PDC.getUniversal(armorStand, "chair", PersistentDataType.BOOLEAN) == null){ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (!armorStand.getPassengers().isEmpty()){ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
armorStand.remove(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue