diff --git a/pom.xml b/pom.xml index 59f0843..38fc361 100644 --- a/pom.xml +++ b/pom.xml @@ -82,11 +82,16 @@ src/main/resources true + + + codemc-releases + https://repo.codemc.io/repository/maven-releases/ + is-releases https://repo.infernalsuite.com/repository/maven-releases/ @@ -103,6 +108,8 @@ jitpack.io https://jitpack.io + + @@ -143,5 +150,16 @@ MatrixColorAPI v1.0.7 + + com.github.retrooper + packetevents-spigot + 2.11.2 + provided + + + com.zaxxer + HikariCP + 6.2.1 + diff --git a/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java b/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java index ce8362f..aa96cc4 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/EcoCraftCore.java @@ -1,24 +1,47 @@ package xyz.soukup.ecoCraftCore; +import com.github.retrooper.packetevents.event.EventManager; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.infernalsuite.asp.api.AdvancedSlimePaperAPI; +import com.infernalsuite.asp.api.world.SlimeWorldInstance; import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.jdbc.DataSourceConnectionSource; import com.j256.ormlite.jdbc.JdbcConnectionSource; +import com.j256.ormlite.stmt.UpdateBuilder; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.table.TableUtils; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; +import xyz.soukup.ecoCraftCore.database.objects.*; +import xyz.soukup.ecoCraftCore.islands.IslandAdminCommand; +import xyz.soukup.ecoCraftCore.islands.IslandSelectorCommand; +import xyz.soukup.ecoCraftCore.mines.MineCommand; +import xyz.soukup.ecoCraftCore.mines.MineWorldManager; import xyz.soukup.ecoCraftCore.money.MoneyCommand; +import xyz.soukup.ecoCraftCore.player.OnKill; import xyz.soukup.ecoCraftCore.player.PreparePlayer; -import xyz.soukup.ecoCraftCore.positionMarker.RulerCommand; +import xyz.soukup.ecoCraftCore.player.TeleportRequestsHandler; +import xyz.soukup.ecoCraftCore.positionMarker.MarkerCommand; +import xyz.soukup.ecoCraftCore.regions.RegionAdminCommand; +import xyz.soukup.ecoCraftCore.regions.RegionEvents; import xyz.soukup.ecoCraftCore.shop.ShopCommand; import xyz.soukup.ecoCraftCore.database.objects.Account; +import xyz.soukup.ecoCraftCore.database.objects.Island; import xyz.soukup.ecoCraftCore.database.objects.Shop; import xyz.soukup.ecoCraftCore.database.objects.Transaction; import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; import xyz.soukup.ecoCraftCore.inventory.VirtualChest; -import xyz.soukup.ecoCraftCore.positionMarker.RulerMarking; +import xyz.soukup.ecoCraftCore.positionMarker.MarkerEvent; import xyz.soukup.ecoCraftCore.database.DaoRegistry; import xyz.soukup.ecoCraftCore.messages.Messages; import xyz.soukup.ecoCraftCore.shop.ShopLogic; @@ -29,28 +52,43 @@ import java.sql.SQLException; import java.util.logging.Level; import java.util.logging.Logger; +import com.github.retrooper.packetevents.PacketEvents; +import xyz.soukup.ecoCraftCore.islands.ChunkModifier; public final class EcoCraftCore extends JavaPlugin { public static EcoCraftCore plugin; public static ConnectionSource connectionSource; + public static FileConfiguration config; @Override public void onEnable() { + this.getLogger().info("plugin starting out"); + plugin = this; + config = getConfig(); + + + this.getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord"); + + PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this)); + PacketEvents.getAPI().load(); + PacketEvents.getAPI().init(); + try { + plugin.saveConfig(); + saveIslandTemplates(); Messages.init(); prepareDatabase(); registerCommands(); registerEvents(); - } catch (SQLException e) { - e.printStackTrace(); - getLogger().severe("Failed to initialize database."); - } catch (IOException e) { - throw new RuntimeException(e); + prepareSlimeWorldsSaver(); + + }catch (IOException e) { + MineWorldManager.init(); } this.getServer().getScheduler().runTaskTimer(this, /* Lambda: */ task -> { @@ -62,53 +100,196 @@ public final class EcoCraftCore extends JavaPlugin { @Override public void onDisable() { + plugin.saveConfig(); VirtualChest.saveCache(); + saveSlimeWorlds(); + adiosDatabase(); - try { - if (connectionSource != null) { - connectionSource.close(); + PacketEvents.getAPI().terminate(); + + } + + private void prepareSlimeWorldsSaver(){ + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> { + AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); + + for (SlimeWorldInstance slimeWorldInstance : asp.getLoadedWorlds()){ + try { + asp.saveWorld(slimeWorldInstance); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } catch (Exception e) { - e.printStackTrace(); + }, 12000L, 12000L); + } + + private void prepareWorldInactivityUnloader(){ + Bukkit.getScheduler().runTaskTimer(plugin, () -> { + + long timeoutMillis = 30 * 60 * 1000; + long now = System.currentTimeMillis(); + + AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); + for (SlimeWorldInstance slimeWorldInstance : asp.getLoadedWorlds()) { + + World world = slimeWorldInstance.getBukkitWorld(); + + if (!world.getPlayers().isEmpty()){ + world.removeMetadata("last_empty_time", plugin); + continue; + } + + if (!world.hasMetadata("last_empty_time")) { + world.setMetadata("last_empty_time", new FixedMetadataValue(plugin, now)); + continue; + } + + long emptySince = world.getMetadata("last_empty_time").get(0).asLong(); + + + if ((now - emptySince) >= timeoutMillis) { + Bukkit.unloadWorld(world, true); + } + + + + } + + }, 1200L, 1200L); + + } + + private void saveSlimeWorlds(){ + AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); + + for (SlimeWorldInstance slimeWorldInstance : asp.getLoadedWorlds()){ + try { + asp.saveWorld(slimeWorldInstance); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + try { + UpdateBuilder updateBuilder = DaoRegistry.getIslandDao().updateBuilder(); + updateBuilder.where().eq("active_on", config.getString("server.name")); + updateBuilder.updateColumnValue("active_on", ""); + updateBuilder.update(); + } catch (SQLException e) { + throw new RuntimeException(e); } } - private void prepareDatabase() throws SQLException { - String databaseUrl = "jdbc:mysql://localhost:3306/ecc"; - connectionSource = new JdbcConnectionSource(databaseUrl, "ecc", "ecc"); - Logger.getLogger("com.j256.ormlite.table.TableUtils").setLevel(Level.OFF); + private void saveIslandTemplates(){ + plugin.saveResource("island_templates/flat_grass.slime", false); + plugin.saveResource("island_templates/flat_sand.slime", false); + plugin.saveResource("island_templates/flat_hell.slime", false); - TableUtils.createTableIfNotExists(connectionSource, Transaction.class); - TableUtils.createTableIfNotExists(connectionSource, Shop.class); - TableUtils.createTableIfNotExists(connectionSource, VirtualChest.class); - TableUtils.createTableIfNotExists(connectionSource, Account.class); + } - DaoRegistry.setTransactionDao(DaoManager.createDao(connectionSource, Transaction.class)); - DaoRegistry.setShopDao(DaoManager.createDao(connectionSource, Shop.class)); - DaoRegistry.setVirtualChestDao(DaoManager.createDao(connectionSource, VirtualChest.class)); - DaoRegistry.setAccountDao(DaoManager.createDao(connectionSource, Account.class)); + private void prepareDatabase() { + + try { + String databaseHost = config.getString("database.host"); + String databasePort = config.getString("database.port"); + String databaseName = config.getString("database.database"); + String databaseUsername = config.getString("database.user"); + String databasePassword = config.getString("database.password"); + + String databaseUrl = "jdbc:mysql://" + databaseHost + ":" + databasePort + "/" + databaseName; + + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(databaseUrl); + hikariConfig.setUsername(databaseUsername); + hikariConfig.setPassword(databasePassword); + hikariConfig.setMaximumPoolSize(5); + hikariConfig.setKeepaliveTime(60000); + hikariConfig.setMaxLifetime(1800000); + + HikariDataSource dataSource = new HikariDataSource(hikariConfig); + + connectionSource = new DataSourceConnectionSource(dataSource, databaseUrl); + Logger.getLogger("com.j256.ormlite.table.TableUtils").setLevel(Level.OFF); + + TableUtils.createTableIfNotExists(connectionSource, ActiveServer.class); + TableUtils.createTableIfNotExists(connectionSource, Transaction.class); + TableUtils.createTableIfNotExists(connectionSource, Shop.class); + TableUtils.createTableIfNotExists(connectionSource, VirtualChest.class); + TableUtils.createTableIfNotExists(connectionSource, Account.class); + TableUtils.createTableIfNotExists(connectionSource, Island.class); + TableUtils.createTableIfNotExists(connectionSource, TeleportRequest.class); + TableUtils.createTableIfNotExists(connectionSource, Region.class); + TableUtils.createTableIfNotExists(connectionSource, RegionMember.class); + + DaoRegistry.setActiveServerDao(DaoManager.createDao(connectionSource, ActiveServer.class)); + DaoRegistry.setTransactionDao(DaoManager.createDao(connectionSource, Transaction.class)); + DaoRegistry.setIslandDaoo(DaoManager.createDao(connectionSource, Island.class)); + DaoRegistry.setShopDao(DaoManager.createDao(connectionSource, Shop.class)); + DaoRegistry.setVirtualChestDao(DaoManager.createDao(connectionSource, VirtualChest.class)); + DaoRegistry.setAccountDao(DaoManager.createDao(connectionSource, Account.class)); + DaoRegistry.setTeleportRequestsDao(DaoManager.createDao(connectionSource, TeleportRequest.class)); + DaoRegistry.setRegionDao(DaoManager.createDao(connectionSource, Region.class)); + DaoRegistry.setRegionMemberDao(DaoManager.createDao(connectionSource, RegionMember.class)); + + + ActiveServer activeServer = new ActiveServer(config.getString("server.name")); + activeServer.save(); + } catch (SQLException e) { + e.printStackTrace(); + } + + + + } + + private void adiosDatabase(){ + + + try { + ActiveServer activeServer = new ActiveServer(config.getString("server.name")); + activeServer.delete(); + + if (connectionSource != null) { + connectionSource.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } } private void registerCommands() { @NotNull LifecycleEventManager<@NotNull Plugin> lm = this.getLifecycleManager(); - lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(ShopCommand.createCommand().build())); - lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(RulerCommand.createCommand().build())); - lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MoneyCommand.createCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(ShopCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MarkerCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MoneyCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(IslandAdminCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(IslandSelectorCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(RegionAdminCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(ShopCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MarkerCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MoneyCommand.getCommand().build())); + lm.registerEventHandler(LifecycleEvents.COMMANDS, event -> event.registrar().register(MineCommand.createCommand().build())); } private void registerEvents(){ PluginManager pm = this.getServer().getPluginManager(); - pm.registerEvents(new RulerMarking(), this); + pm.registerEvents(new TeleportRequestsHandler(), this); + pm.registerEvents(new OnKill(), this); + pm.registerEvents(new MarkerEvent(), this); pm.registerEvents(new VirtualChestLogic(), this); pm.registerEvents(new ShopLogic(), this); pm.registerEvents(new PreparePlayer(), this); + pm.registerEvents(new RegionEvents(), this); + pm.registerEvents(new MineWorldManager(), this); - } + EventManager events = PacketEvents.getAPI().getEventManager(); + events.registerListener(new ChunkModifier(this), PacketListenerPriority.NORMAL); + } diff --git a/src/main/java/xyz/soukup/ecoCraftCore/database/DaoRegistry.java b/src/main/java/xyz/soukup/ecoCraftCore/database/DaoRegistry.java index 46c9389..9b62b01 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/database/DaoRegistry.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/database/DaoRegistry.java @@ -10,7 +10,42 @@ public class DaoRegistry { private static Dao virtualChestDao; private static Dao accountDao; private static Dao islandDao; + private static Dao teleportRequestsDao; + private static Dao activeServerDao; + private static Dao regionDao; + private static Dao regionMemberDao; + public static Dao getRegionMemberDao() { + return regionMemberDao; + } + + public static void setRegionMemberDao(Dao regionMemberDao) { + DaoRegistry.regionMemberDao = regionMemberDao; + } + + public static Dao getRegionDao() { + return regionDao; + } + + public static void setRegionDao(Dao regionDao) { + DaoRegistry.regionDao = regionDao; + } + + public static Dao getTeleportRequestsDao() { + return teleportRequestsDao; + } + + public static void setTeleportRequestsDao(Dao teleportRequestsDao) { + DaoRegistry.teleportRequestsDao = teleportRequestsDao; + } + + public static Dao getActiveServerDao() { + return activeServerDao; + } + + public static void setActiveServerDao(Dao activeServerDao) { + DaoRegistry.activeServerDao = activeServerDao; + } public static Dao getShopDao() { return shopDao; diff --git a/src/main/java/xyz/soukup/ecoCraftCore/database/objects/ActiveServer.java b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/ActiveServer.java new file mode 100644 index 0000000..f02ce5b --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/ActiveServer.java @@ -0,0 +1,34 @@ +package xyz.soukup.ecoCraftCore.database.objects; + + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import xyz.soukup.ecoCraftCore.database.DaoRegistry; + +import java.sql.SQLException; + +@DatabaseTable(tableName = "active_servers") +public class ActiveServer { + @DatabaseField(unique = true, id = true) + private String name; + + public ActiveServer(){ + + } + + public ActiveServer(String name){ + this.name = name; + } + + public void save() throws SQLException { + DaoRegistry.getActiveServerDao().createIfNotExists(this); + } + + public void delete() throws SQLException { + DaoRegistry.getActiveServerDao().delete(this); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/database/objects/Island.java b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/Island.java index edd47c5..0d10702 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/database/objects/Island.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/Island.java @@ -15,9 +15,8 @@ public class Island { @DatabaseField(canBeNull = false, unique = true) private String uuid; - @DatabaseField(canBeNull = false, unique = true) - private String name; - + @DatabaseField(canBeNull = false) + private String type; @DatabaseField(columnName = "display_name") private String displayName; @@ -28,21 +27,26 @@ public class Island { @DatabaseField(canBeNull = false) private String owner; - @DatabaseField(canBeNull = false) + @DatabaseField(columnName = "owner_type", canBeNull = false) private String ownerType; @DatabaseField(canBeNull = false, dataType = DataType.BYTE_ARRAY, columnDefinition = "LONGBLOB") private byte[] data; + @DatabaseField(columnName = "is_public", canBeNull = false, defaultValue = "false") + private Boolean isPublic; + @DatabaseField(defaultValue = "", columnName = "active_on") private String activeOn; + + public Island(){ } - public Island(String name, String uuid, String displayName, String descritpion, String owner, String ownerType, byte[] data) { - this.name = name; + public Island(String type, String uuid, String displayName, String descritpion, String owner, String ownerType, byte[] data) { + this.type = type; this.uuid = uuid; this.displayName = displayName; this.descritpion = descritpion; @@ -52,6 +56,13 @@ public class Island { } + public Boolean getPublic() { + return isPublic; + } + public void setPublic(Boolean isPublic) { + this.isPublic = isPublic; + } + public void setData(byte[] data) { this.data = data; } @@ -60,8 +71,8 @@ public class Island { return id; } - public String getName() { - return name; + public String getType() { + return type; } public String getDisplayName() { diff --git a/src/main/java/xyz/soukup/ecoCraftCore/database/objects/Region.java b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/Region.java new file mode 100644 index 0000000..ea06c7c --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/Region.java @@ -0,0 +1,213 @@ +package xyz.soukup.ecoCraftCore.database.objects; + +import com.j256.ormlite.dao.ForeignCollection; +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.field.ForeignCollectionField; +import com.j256.ormlite.table.DatabaseTable; +import xyz.soukup.ecoCraftCore.database.DaoRegistry; + +import java.util.HashMap; +import java.util.List; + +@DatabaseTable(tableName = "regions") +public class Region { + + public static final HashMap> cache = new HashMap<>(); + + @DatabaseField(generatedId = true) + private int id; + + @DatabaseField(canBeNull = false) + private String island; + + @DatabaseField(columnName = "region_type", canBeNull = false) + private int regionType; + + @DatabaseField(canBeNull = false) + private int x1; + + @DatabaseField(canBeNull = false) + private int y1; + + @DatabaseField(canBeNull = false) + private int z1; + + @DatabaseField(canBeNull = false) + private int x2; + + @DatabaseField(canBeNull = false) + private int y2; + + @DatabaseField(canBeNull = false) + private int z2; + + @ForeignCollectionField(eager = true) + private ForeignCollection regionMembers; + + @DatabaseField() + private Integer value; + + public Region() { + + } + + public Region(String island, int regionType, int x1, int y1, int z1, int x2, int y2, int z2) { + this.island = island; + this.regionType = regionType; + this.x1 = Math.min(x1, x2); + this.y1 = Math.min(y1, y2); + this.x2 = Math.max(x1, x2); + this.y2 = Math.max(y1, y2); + this.z1 = Math.min(z1, z2); + this.z2 = Math.max(z1, z2); + + } + public int getId() { + return id; + } + + public int getRegionType() { + return regionType; + } + + public int getX1() { + return x1; + } + + public int getZ1() { + return z1; + } + + public int getZ2() { + return z2; + } + + public int getY1() { + return y1; + } + + public int getX2() { + return x2; + } + + public int getY2() { + return y2; + } + + public ForeignCollection getRegionMembers() { + return regionMembers; + } + + public Integer getValue() { + return value; + } + + public String getIsland() { + return island; + } + + + + + + public void setRegionType(int regionType) { + this.regionType = regionType; + } + + public void setValue(Integer value) { + this.value = value; + } + + public void save(){ + try { + + DaoRegistry.getRegionDao().createOrUpdate(this); + List regions = cache.get(island); + + if (regions == null) { + return; + } + + regions.removeIf(region -> region.getId() == this.id); + regions.add(this); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void addRegionMember(String memberType, String member, String membershipType){ + RegionMember regionMember = new RegionMember(this, memberType, member, membershipType); + regionMember.save(); + + Region updatedRegion = Region.findById(this.id); + List regions = cache.get(island); + + if (regions == null) { + return; + } + + regions.removeIf(region -> region.getId() == this.id); + regions.add(updatedRegion); + + } + + public boolean isInside(int x, int y, int z) { + return x >= this.x1 && x <= this.x2 && y >= this.y1 && y <= this.y2 && z >= this.z1 && z <= this.z2; + } + + public static Region findById(int id) { + try { + return DaoRegistry.getRegionDao().queryForId(id); + } catch (Exception e) { + return null; + } + } + + public static void cacheRegions(String island) { + try { + List regions = DaoRegistry.getRegionDao().queryBuilder() + .where() + .eq("island", island) + .query(); + cache.put(island, regions); + } catch (Exception e) { + + } + } + + public static Region findRegion(int x, int y, int z, String island) { + + Region region = null; + int highestType = -1; + + if (!cache.containsKey(island)) { + cacheRegions(island); + } + + for (Region cachedRegion : cache.get(island)) { + if (!cachedRegion.isInside(x, y, z)) { + continue; + } + if (highestType >= cachedRegion.getRegionType()){ + continue; + } + region = cachedRegion; + } + + return region; + + + } + + public void changeBoundaries(int x1, int y1, int z1, int x2, int y2, int z2){ + this.x1 = Math.min(x1, x2); + this.y1 = Math.min(y1, y2); + this.x2 = Math.max(x1, x2); + this.y2 = Math.max(y1, y2); + this.z1 = Math.min(z1, z2); + this.z2 = Math.max(z1, z2); + } + + +} + diff --git a/src/main/java/xyz/soukup/ecoCraftCore/database/objects/RegionMember.java b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/RegionMember.java new file mode 100644 index 0000000..610d47a --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/RegionMember.java @@ -0,0 +1,65 @@ +package xyz.soukup.ecoCraftCore.database.objects; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import xyz.soukup.ecoCraftCore.database.DaoRegistry; + +import java.sql.SQLException; + +@DatabaseTable(tableName = "region_members") +public class RegionMember { + + @DatabaseField(generatedId = true) + private int id; + + @DatabaseField(foreign = true, foreignAutoRefresh = true) + private Region region; + + @DatabaseField(columnName = "member_type", canBeNull = false) + private String membertype; + + @DatabaseField(columnName = "member_name", canBeNull = false) + private String memberName; + + @DatabaseField(columnName = "membership_type", canBeNull = false) + private String membershipType; + + public RegionMember(){ + + } + + public RegionMember(Region region, String membertype, String memberName, String membershipType){ + this.region = region; + this.membertype = membertype; + this.memberName = memberName; + this.membershipType = membershipType; + } + + public Region getRegion() { + return region; + } + + public String getMembertype() { + return membertype; + } + + public String getName() { + return memberName; + } + + public String getMembershipType() { + return membershipType; + } + + public void setMembershipType(String membershipType) { + this.membershipType = membershipType; + } + + public void save(){ + try { + DaoRegistry.getRegionMemberDao().createOrUpdate(this); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/database/objects/TeleportRequest.java b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/TeleportRequest.java new file mode 100644 index 0000000..0dc48bf --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/database/objects/TeleportRequest.java @@ -0,0 +1,96 @@ +package xyz.soukup.ecoCraftCore.database.objects; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import org.bukkit.Location; +import xyz.soukup.ecoCraftCore.database.DaoRegistry; + +import java.sql.SQLException; + +@DatabaseTable(tableName = "teleport_requests") +public class TeleportRequest { + + @DatabaseField(unique = true, id = true) + private String player; + + @DatabaseField(canBeNull = false) + private String server; + + @DatabaseField(canBeNull = false) + private String world; + + @DatabaseField + private Integer x; + + @DatabaseField + private Integer y; + + @DatabaseField + private Integer z; + + @DatabaseField + private Float yaw; + + @DatabaseField + private Float pitch; + + public TeleportRequest() { + } + + public TeleportRequest(String player, String server, String world) { + this.player = player; + this.server = server; + this.world = world; + } + + public TeleportRequest(String player, String server, String world, Integer x, Integer y, Integer z, Float yaw, Float pitch) { + this.player = player; + this.server = server; + this.world = world; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + } + + public String getServer() { + return server; + } + + public String getWorld() { + return world; + } + + public String getPlayer() { + return player; + } + + public Integer getX() { + return x; + } + + public Integer getY() { + return y; + } + + public Integer getZ() { + return z; + } + + public Float getYaw() { + return yaw; + } + + public Float getPitch() { + return pitch; + } + + public void save() throws SQLException { + DaoRegistry.getTeleportRequestsDao().createOrUpdate(this); + } + + public void delete() throws SQLException { + DaoRegistry.getTeleportRequestsDao().delete(this); + } +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/gui/GuiItemBuilder.java b/src/main/java/xyz/soukup/ecoCraftCore/gui/GuiItemBuilder.java index 66f95c2..3c38f46 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/gui/GuiItemBuilder.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/gui/GuiItemBuilder.java @@ -2,16 +2,19 @@ package xyz.soukup.ecoCraftCore.gui; import com.github.stefvanschie.inventoryframework.gui.GuiItem; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; public class GuiItemBuilder { - private ItemStack itemStack; - private ItemMeta itemMeta; + private final ItemStack itemStack; + private final ItemMeta itemMeta; public GuiItemBuilder(Material material){ this.itemStack = new ItemStack(material); @@ -30,6 +33,14 @@ public class GuiItemBuilder { return this; } + public GuiItemBuilder setRawLore(String rawLore){ + List lore = Arrays.stream(rawLore.split("\\R")) + .map(line -> MiniMessage.miniMessage().deserialize(line)) + .collect(Collectors.toList()); + itemMeta.lore(lore); + return this; + } + public GuiItem build(){ this.itemStack.setItemMeta(this.itemMeta); return new GuiItem(this.itemStack); diff --git a/src/main/java/xyz/soukup/ecoCraftCore/islands/ChunkModifier.java b/src/main/java/xyz/soukup/ecoCraftCore/islands/ChunkModifier.java new file mode 100644 index 0000000..2ae90a1 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/islands/ChunkModifier.java @@ -0,0 +1,257 @@ +package xyz.soukup.ecoCraftCore.islands; + +import com.github.retrooper.packetevents.event.PacketListener; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.world.chunk.BaseChunk; +import com.github.retrooper.packetevents.protocol.world.chunk.Column; +import com.github.retrooper.packetevents.protocol.world.chunk.LightData; +import com.github.retrooper.packetevents.protocol.world.chunk.TileEntity; +import com.github.retrooper.packetevents.protocol.world.chunk.impl.v_1_18.Chunk_v1_18; +import com.github.retrooper.packetevents.protocol.world.chunk.palette.DataPalette; +import com.github.retrooper.packetevents.protocol.world.chunk.palette.PaletteType; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.java.JavaPlugin; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.BitSet; +import java.util.Arrays; +import java.util.HashMap; + +public class ChunkModifier implements PacketListener { + + private final JavaPlugin plugin; + private final NamespacedKey keyX1, keyX2, keyZ1, keyZ2, keyType; + + HashMap blockStates = new HashMap<>(); + + public ChunkModifier(JavaPlugin plugin) { + this.plugin = plugin; + this.keyX1 = new NamespacedKey(plugin, "borderx1"); + this.keyX2 = new NamespacedKey(plugin, "borderx2"); + this.keyZ1 = new NamespacedKey(plugin, "bordery1"); // Based on your spec + this.keyZ2 = new NamespacedKey(plugin, "borderx2"); // Based on your spec + this.keyType = new NamespacedKey(plugin, "island_type"); + } + + @Override + public void onPacketSend(PacketSendEvent event) { + if (event.getPacketType() != PacketType.Play.Server.CHUNK_DATA) return; + + Player bukkitPlayer = Bukkit.getPlayer(event.getUser().getUUID()); + if (bukkitPlayer == null) return; + + World world = bukkitPlayer.getWorld(); + PersistentDataContainer pdc = world.getPersistentDataContainer(); + + String type = pdc.get(keyType, PersistentDataType.STRING); + if (type == null) return; + + String block; + + switch (type){ + case "flat_grass": + case "flat_sand": + block = "WATER"; + break; + case "flat_hell": + block = "LAVA"; + break; + default: + return; + + } + + Integer x1 = pdc.get(keyX1, PersistentDataType.INTEGER); + Integer x2 = pdc.get(keyX2, PersistentDataType.INTEGER); + Integer z1 = pdc.get(keyZ1, PersistentDataType.INTEGER); + Integer z2 = pdc.get(keyZ2, PersistentDataType.INTEGER); + if (x1 == null || x2 == null || z1 == null || z2 == null) return; + + int minCX = Math.min(x1 >> 4, x2 >> 4) - 1; + int maxCX = Math.max(x1 >> 4, x2 >> 4) + 1; + int minCZ = Math.min(z1 >> 4, z2 >> 4) - 1; + int maxCZ = Math.max(z1 >> 4, z2 >> 4) + 1; + + WrapperPlayServerChunkData wrapper = new WrapperPlayServerChunkData(event); + + Column original = wrapper.getColumn(); + if (original == null) return; + + int chunkX = original.getX(); + int chunkZ = original.getZ(); + if (chunkX >= minCX && chunkX <= maxCX && chunkZ >= minCZ && chunkZ <= maxCZ) return; + + Column ghostColumn = createGhostColumn(world, original, resolveBlockStateID(block)); + if (ghostColumn == null) return; + + wrapper.setColumn(ghostColumn); + + wrapper.setLightData(buildFullBrightLightData(world)); + } + + private static LightData buildFullBrightLightData(World world) { + int sections = (world.getMaxHeight() - world.getMinHeight()) >> 4; + if (sections <= 0) sections = 24; + + int lightCount = sections + 2; + + byte[] fullSky = new byte[2048]; + Arrays.fill(fullSky, (byte) 0xFF); + + byte[] noBlock = new byte[2048]; + + + boolean hasSkyLight = world.getEnvironment() != World.Environment.NETHER + && world.getEnvironment() != World.Environment.THE_END; // adjust if you have custom dims + + BitSet skyMask = new BitSet(lightCount); + BitSet blockMask = new BitSet(lightCount); + BitSet emptySkyMask = new BitSet(lightCount); + BitSet emptyBlockMask = new BitSet(lightCount); + + byte[][] skyArray; + int skyCount; + if (hasSkyLight) { + skyMask.set(0, lightCount); + skyCount = lightCount; + skyArray = new byte[skyCount][]; + for (int i = 0; i < skyCount; i++) { + skyArray[i] = fullSky; + } + } else { + // no skylight dimension + emptySkyMask.set(0, lightCount); + skyCount = 0; + skyArray = new byte[0][]; + } + + blockMask.set(0, lightCount); + int blockCount = lightCount; + byte[][] blockArray = new byte[blockCount][]; + for (int i = 0; i < blockCount; i++) { + blockArray[i] = noBlock; + } + + LightData ld = new LightData(); + ld.setTrustEdges(true); + ld.setSkyLightMask(skyMask); + ld.setBlockLightMask(blockMask); + ld.setEmptySkyLightMask(emptySkyMask); + ld.setEmptyBlockLightMask(emptyBlockMask); + ld.setSkyLightCount(skyCount); + ld.setBlockLightCount(blockCount); + ld.setSkyLightArray(skyArray); + ld.setBlockLightArray(blockArray); + return ld; + } + + private Column createGhostColumn(World world, Column original, int blockID) { + int sections = (world.getMaxHeight() - world.getMinHeight()) >> 4; + if (sections <= 0) sections = 24; + + BaseChunk[] originalChunks = original.getChunks(); + + // Find any existing biome palette from the original packet; we will reuse it. + DataPalette fallbackBiomePalette = null; + if (originalChunks != null) { + for (BaseChunk bc : originalChunks) { + if (bc instanceof Chunk_v1_18 c) { + fallbackBiomePalette = c.getBiomeData(); + break; + } + } + } + + + BaseChunk[] chunks = new BaseChunk[sections]; + + for (int sectionIndex = 0; sectionIndex < sections; sectionIndex++) { + DataPalette biomePalette = fallbackBiomePalette; + + if (originalChunks != null && sectionIndex < originalChunks.length && originalChunks[sectionIndex] instanceof Chunk_v1_18 c) { + // Prefer the matching section's biome palette if present + biomePalette = c.getBiomeData(); + } + + DataPalette blockPalette = PaletteType.CHUNK.create(); + + Chunk_v1_18 section = new Chunk_v1_18(0, blockPalette, biomePalette); + section.set(0, 0, 0, 0); + + chunks[sectionIndex] = section; + } + + if (chunks[0] instanceof Chunk_v1_18 section0) { + for (int localY = 0; localY <= 1; localY++) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + section0.set(x, localY, z, blockID); + } + } + } + } + + return new Column( + original.getX(), + original.getZ(), + true, + chunks, + new TileEntity[0], + original.getHeightMaps() + ); + } + + private int resolveBlockStateID(String name) { + Integer stateID = blockStates.get(name); + if (stateID != null) return stateID; + + int resolved = 0; + + try { + Object blockState = resolveNmsBlockState(name); + if (blockState != null) { + Integer id = tryGetBlockStateIdViaBlockGetId(blockState); + if (id != null && id > 0) { + resolved = id; + } + } + } catch (ReflectiveOperationException ignored) { + + } + + blockStates.put(name, resolved); + return resolved; + } + + private static Object resolveNmsBlockState(String name) throws ReflectiveOperationException { + Class blocksClass = Class.forName("net.minecraft.world.level.block.Blocks"); + Field waterField = blocksClass.getField(name); + Object waterBlock = waterField.get(null); + + Method defaultBlockState = waterBlock.getClass().getMethod("defaultBlockState"); + return defaultBlockState.invoke(waterBlock); + } + + private static Integer tryGetBlockStateIdViaBlockGetId(Object blockState) throws ReflectiveOperationException { + // Block.getId(BlockState) exists on many modern versions + Class blockClass = Class.forName("net.minecraft.world.level.block.Block"); + Class blockStateClass = Class.forName("net.minecraft.world.level.block.state.BlockState"); + + try { + Method getId = blockClass.getMethod("getId", blockStateClass); + Object idObj = getId.invoke(null, blockState); + return (idObj instanceof Integer i) ? i : null; + } catch (NoSuchMethodException ignored) { + return null; + } + } + +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandLoader.java b/src/main/java/xyz/soukup/ecoCraftCore/islands/DatabaseIslandLoader.java similarity index 95% rename from src/main/java/xyz/soukup/ecoCraftCore/islands/IslandLoader.java rename to src/main/java/xyz/soukup/ecoCraftCore/islands/DatabaseIslandLoader.java index 1431f76..93c8b6e 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandLoader.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/islands/DatabaseIslandLoader.java @@ -10,7 +10,7 @@ import java.sql.SQLException; import java.util.List; import java.util.stream.Collectors; -public class IslandLoader implements SlimeLoader { +public class DatabaseIslandLoader implements SlimeLoader { @Override @@ -38,6 +38,8 @@ public class IslandLoader implements SlimeLoader { .setCountOf(true) .where() .eq("uuid", worldName) + .and() + .isNotNull("data") .countOf(); return count > 0; diff --git a/src/main/java/xyz/soukup/ecoCraftCore/islands/FileIslandLoader.java b/src/main/java/xyz/soukup/ecoCraftCore/islands/FileIslandLoader.java new file mode 100644 index 0000000..e4ebff8 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/islands/FileIslandLoader.java @@ -0,0 +1,72 @@ +package xyz.soukup.ecoCraftCore.islands; + +import com.infernalsuite.asp.api.exceptions.UnknownWorldException; +import com.infernalsuite.asp.api.loaders.SlimeLoader; +import xyz.soukup.ecoCraftCore.EcoCraftCore; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class FileIslandLoader implements SlimeLoader { + + private final Path storagePath; + private static final String EXTENSION = ".slime"; + + public FileIslandLoader() { + // Creates a directory named 'islands' inside your plugin folder + this.storagePath = EcoCraftCore.plugin.getDataFolder().toPath().resolve("island_templates"); + + if (!Files.exists(storagePath)) { + storagePath.toFile().mkdirs(); + } + } + + private Path getWorldPath(String worldName) { + return storagePath.resolve(worldName + EXTENSION); + } + + @Override + public byte[] readWorld(String worldName) throws UnknownWorldException, IOException { + Path path = getWorldPath(worldName); + if (!Files.exists(path)) { + throw new UnknownWorldException(worldName); + } + return Files.readAllBytes(path); + } + + @Override + public boolean worldExists(String worldName) throws IOException { + return Files.exists(getWorldPath(worldName)); + } + + @Override + public void saveWorld(String worldName, byte[] serializedWorld) throws IOException { + // Files.write will create or overwrite the file automatically + Files.write(getWorldPath(worldName), serializedWorld); + } + + @Override + public void deleteWorld(String worldName) throws IOException { + Files.deleteIfExists(getWorldPath(worldName)); + } + + @Override + public List listWorlds() throws IOException { + File folder = storagePath.toFile(); + File[] files = folder.listFiles((dir, name) -> name.endsWith(EXTENSION)); + + if (files == null) { + return Collections.emptyList(); + } + + return Arrays.stream(files) + .map(file -> file.getName().replace(EXTENSION, "")) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandAdminCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandAdminCommand.java new file mode 100644 index 0000000..5d7cd81 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandAdminCommand.java @@ -0,0 +1,324 @@ +package xyz.soukup.ecoCraftCore.islands; + +import com.infernalsuite.asp.api.AdvancedSlimePaperAPI; +import com.infernalsuite.asp.api.world.SlimeWorld; +import com.j256.ormlite.stmt.QueryBuilder; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +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.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import xyz.soukup.ecoCraftCore.database.DaoRegistry; +import xyz.soukup.ecoCraftCore.database.objects.Island; +import xyz.soukup.ecoCraftCore.messages.Messages; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.List; + +import static xyz.soukup.ecoCraftCore.EcoCraftCore.config; +import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; + +public class IslandAdminCommand { + private final AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); + + + public static LiteralArgumentBuilder getCommand() { + + + LiteralArgumentBuilder tp = Commands.literal("tp") + .then(Commands.argument("uuid", StringArgumentType.word()) + .executes(IslandAdminCommand::teleport) + .suggests(((context, builder) -> { + try { + QueryBuilder queryBuilder = DaoRegistry.getIslandDao().queryBuilder(); + queryBuilder.selectColumns("uuid"); + List islands = queryBuilder.query(); + for (Island island : islands) { + builder.suggest(island.getUuid()); + } + return builder.buildFuture(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }))); + + LiteralArgumentBuilder create = Commands.literal("create") + .then(Commands.argument("type", StringArgumentType.word()) + .suggests(((context, builder) -> { + try { + IslandManager islandManager = new IslandManager(); + FileIslandLoader fileLoader = islandManager.fileLoader; + for (String world: fileLoader.listWorlds()){ + builder.suggest(world); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return builder.buildFuture(); + })) + .then(Commands.argument("display_name", StringArgumentType.string()) + .then(Commands.argument("description", StringArgumentType.greedyString()) + .executes(IslandAdminCommand::createWorld)))); + + LiteralArgumentBuilder load = Commands.literal("load") + .then(Commands.argument("uuid", StringArgumentType.word()) + .suggests(((context, builder) -> { + try { + QueryBuilder queryBuilder = DaoRegistry.getIslandDao().queryBuilder(); + queryBuilder.selectColumns("uuid"); + List islands = queryBuilder.query(); + for (Island island : islands) { + builder.suggest(island.getUuid()); + } + return builder.buildFuture(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + })) + .executes(IslandAdminCommand::loadWorld)); + + LiteralArgumentBuilder loadTemplate = Commands.literal("loadTemplate") + .then(Commands.argument("name", StringArgumentType.word()) + .suggests(((context, builder) -> { + IslandManager islandManager = new IslandManager(); + FileIslandLoader fileLoader = islandManager.fileLoader; + + try { + for (String world: fileLoader.listWorlds()){ + builder.suggest(world); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return builder.buildFuture(); + })) + .executes(IslandAdminCommand::loadTemplate)); + + LiteralArgumentBuilder template = Commands.literal("template") + .then(Commands.argument("uuid", StringArgumentType.word()) + .suggests((context, builder) -> { + try { + QueryBuilder queryBuilder = DaoRegistry.getIslandDao().queryBuilder(); + queryBuilder.selectColumns("uuid"); + List islands = queryBuilder.query(); + for (Island island : islands) { + builder.suggest(island.getUuid()); + } + return builder.buildFuture(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }) + .then(Commands.argument("templateName", StringArgumentType.word()) + .executes(IslandAdminCommand::createTemplate))); + + LiteralArgumentBuilder metadata = Commands.literal("metadata") + .then(Commands.argument("key", StringArgumentType.word()) + .then(Commands.argument("value", IntegerArgumentType.integer()) + .executes(IslandAdminCommand::setMetadata))); + + LiteralArgumentBuilder metadataString = Commands.literal("metadataString") + .then(Commands.argument("key", StringArgumentType.word()) + .then(Commands.argument("value", StringArgumentType.string()) + .executes(IslandAdminCommand::setMetadataString))); + + LiteralArgumentBuilder expand = Commands.literal("expand") + .then(Commands.argument("lenght", IntegerArgumentType.integer()) + .executes(IslandAdminCommand::expandIsland)); + + LiteralArgumentBuilder listMetadata = Commands.literal("listMetadata") + .executes(IslandAdminCommand::readAllMetadata); + + LiteralArgumentBuilder spawn = Commands.literal("spawn") + .executes(IslandAdminCommand::setSpawn); + + LiteralArgumentBuilder enviroment = Commands.literal("environment") + .then(Commands.argument("environment", StringArgumentType.word()) + .executes(IslandAdminCommand::setEnvironment) + .suggests(((context, builder) -> { + builder.suggest("normal"); + builder.suggest("nether"); + builder.suggest("the_end"); + return builder.buildFuture(); + }))); + + LiteralArgumentBuilder setDefaultIsland = Commands.literal("setDefaultIsland") + .executes(IslandAdminCommand::setDefualtIsland); + + return Commands.literal("island-admin") + .requires(commandSourceStack -> commandSourceStack.getSender().isOp()) + .then(tp) + .then(create) + .then(load) + .then(template) + .then(metadata) + .then(metadataString) + .then(listMetadata) + .then(loadTemplate) + .then(spawn) + .then(enviroment) + .then(expand) + .then(setDefaultIsland); + + + + } + + private static int setDefualtIsland(CommandContext context){ + if (!(context.getSource().getSender() instanceof Player player)) return 0; + String uuid = player.getWorld().getName(); + config.set("islands.spawn", uuid); + plugin.saveConfig(); + player.sendMessage("done."); + return 0; + + } + + private static int setSpawn(CommandContext context) { + if (!(context.getSource().getSender() instanceof Player player)) return 0; + IslandManager islandManager = new IslandManager(); + Location spawn = player.getLocation(); + String uuid = player.getWorld().getName(); + islandManager.changeSpawn(spawn, uuid); + Messages.send(player, "island.setSpawn.success"); + + return 0; + } + + private static int setEnvironment(CommandContext context) { + if (!(context.getSource().getSender() instanceof Player player)) return 0; + IslandManager islandManager = new IslandManager(); + String uuid = player.getWorld().getName(); + islandManager.changeEnviroment(context.getArgument("environment", String.class), uuid); + Messages.send(player, "island.setEnvironment.success"); + + return 0; + } + + + private static int teleport(CommandContext context) { + IslandManager islandManager = new IslandManager(); + try { + Integer status = islandManager.teleport((Player) context.getSource().getSender(), context.getArgument("uuid", String.class)); + context.getSource().getSender().sendMessage(String.valueOf(status)); + } catch (Exception e) { + throw new RuntimeException(e); + } + return 0; + } + + private static int createWorld(CommandContext context) { + IslandManager islandManager = new IslandManager(); + String type = context.getArgument("type", String.class); + String displayName = context.getArgument("display_name", String.class); + String description = context.getArgument("description", String.class); + String owner = context.getSource().getSender().getName(); + String uuid = islandManager.createIsland(type, displayName, description, owner, "player"); + context.getSource().getSender().sendMessage("Created island: " + uuid); + + return 0; + } + + private static int loadWorld(CommandContext context) { + IslandManager islandManager = new IslandManager(); + islandManager.loadIsland(context.getArgument("uuid", String.class)); + context.getSource().getSender().sendMessage("done."); + return 0; + } + + private static int loadTemplate(CommandContext context) { + IslandManager islandManager = new IslandManager(); + islandManager.loadIslandTemplate(context.getArgument("name", String.class)); + context.getSource().getSender().sendMessage("done."); + return 0; + } + + private static int createTemplate(CommandContext context) { + IslandManager islandManager = new IslandManager(); + String uuid = StringArgumentType.getString(context, "uuid"); + String templateName = StringArgumentType.getString(context, "templateName"); + context.getSource().getSender().sendMessage("§aCreating template '" + templateName + "' from world '" + uuid + "'..."); + islandManager.createIslandTemplate(uuid, templateName); + context.getSource().getSender().sendMessage("§aTemplate created successfully!"); + return 1; + } + + private static int setMetadata(CommandContext context) { + if (!(context.getSource().getSender() instanceof Player player)) return 0; + + String keyName = StringArgumentType.getString(context, "key"); + Integer value = IntegerArgumentType.getInteger(context, "value"); + + player.getWorld().getPersistentDataContainer().set(new NamespacedKey(plugin, keyName), PersistentDataType.INTEGER, value); + context.getSource().getSender().sendMessage(keyName + " set to " + value); + return 1; + } + + private static int setMetadataString(CommandContext context) { + if (!(context.getSource().getSender() instanceof Player player)) return 0; + + String keyName = StringArgumentType.getString(context, "key"); + String value = StringArgumentType.getString(context, "value"); + + player.getWorld().getPersistentDataContainer().set(new NamespacedKey(plugin, keyName), PersistentDataType.STRING, value); + context.getSource().getSender().sendMessage(keyName + " set to " + value); + + return 1; + } + + private static int readAllMetadata(CommandContext context) { + if (!(context.getSource().getSender() instanceof Player player)) { + context.getSource().getSender().sendMessage("§cOnly players can use this."); + return 0; + } + + + PersistentDataContainer pdc = player.getWorld().getPersistentDataContainer(); + if (pdc.getKeys().isEmpty()) { + player.sendMessage("§eNo metadata found."); + return 1; + } + + player.sendMessage("§6--- World Metadata ---"); + for (NamespacedKey key : pdc.getKeys()) { + String val = "unknown"; + // Logic to determine type for display + if (pdc.has(key, PersistentDataType.STRING)) val = pdc.get(key, PersistentDataType.STRING); + else if (pdc.has(key, PersistentDataType.INTEGER)) val = String.valueOf(pdc.get(key, PersistentDataType.INTEGER)); + + player.sendMessage("§b" + key.getKey() + "§7: §f" + val); + } + return 1; + } + + private static int expandIsland(CommandContext context){ + + Player player = (Player) context.getSource().getSender(); + float yaw = player.getLocation().getYaw(); + int lenght = IntegerArgumentType.getInteger(context, "lenght"); + String uuid = player.getWorld().getName(); + + IslandManager islandManager = new IslandManager(); + islandManager.expandIsland(yaw, lenght, uuid); + + + return 0; + } + + private static void saveSlimeWorld(SlimeWorld world, Player player, String key, String val) { + try { + AdvancedSlimePaperAPI.instance().saveWorld(world); + player.sendMessage("§aMetadata set: §f" + key + " §7= §f" + val); + } catch (IOException e) { + player.sendMessage("§cFailed to save world metadata!"); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandCommand.java deleted file mode 100644 index 283b225..0000000 --- a/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandCommand.java +++ /dev/null @@ -1,32 +0,0 @@ -package xyz.soukup.ecoCraftCore.islands; - -import com.mojang.brigadier.arguments.StringArgumentType; -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; - - -public class IslandCommand { - - LiteralArgumentBuilder tp = Commands.literal("tp") - .then(Commands.argument("uuid", StringArgumentType.word()) - .executes(IslandCommand::teleport)); - - LiteralArgumentBuilder create = Commands.literal("create") - .then(Commands.argument("name", StringArgumentType.word()) - .then(Commands.argument("display_name", StringArgumentType.string()))); - - private static int teleport(CommandContext context) { - return 0; - } - - private static int createWorld(CommandContext context) { - return 0; - } - - private static int loadWorld(CommandContext context) { - return 0; - } - -} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandManager.java b/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandManager.java index 939b960..301594f 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandManager.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandManager.java @@ -1,85 +1,459 @@ package xyz.soukup.ecoCraftCore.islands; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; import com.infernalsuite.asp.api.AdvancedSlimePaperAPI; +import com.infernalsuite.asp.api.exceptions.CorruptedWorldException; +import com.infernalsuite.asp.api.exceptions.NewerFormatException; +import com.infernalsuite.asp.api.exceptions.UnknownWorldException; +import com.infernalsuite.asp.api.exceptions.WorldAlreadyExistsException; 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 com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.GenericRawResults; +import com.j256.ormlite.stmt.QueryBuilder; +import com.j256.ormlite.stmt.UpdateBuilder; +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataType; import xyz.soukup.ecoCraftCore.database.objects.Island; import xyz.soukup.ecoCraftCore.database.DaoRegistry; +import xyz.soukup.ecoCraftCore.database.objects.Region; +import xyz.soukup.ecoCraftCore.database.objects.TeleportRequest; +import xyz.soukup.ecoCraftCore.utilities.PDC; +import java.io.IOException; import java.sql.SQLException; +import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import static xyz.soukup.ecoCraftCore.EcoCraftCore.config; import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; public class IslandManager { private final AdvancedSlimePaperAPI asp = AdvancedSlimePaperAPI.instance(); - private final IslandLoader loader = new IslandLoader(); + private final DatabaseIslandLoader databaseLoader = new DatabaseIslandLoader(); + public final FileIslandLoader fileLoader = new FileIslandLoader(); + private final Dao dao = DaoRegistry.getIslandDao(); + public void createIslandTemplate(String uuid, String templateName){ + try { + SlimeWorld slimeWorld = asp.getLoadedWorld(uuid).clone(templateName, fileLoader); + asp.saveWorld(slimeWorld); + } catch (WorldAlreadyExistsException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } - public void createIsland(String name, String displayName, String descritpion, String owner, String ownerType) { + public String createIsland(String type, String displayName, String descritpion, String owner, String ownerType) { String uuid = UUID.randomUUID().toString(); - - SlimePropertyMap props = new SlimePropertyMap(); - props.setValue(SlimeProperties.ENVIRONMENT, "overworld"); - props.setValue(SlimeProperties.WORLD_TYPE, "flat"); - - // Create empty world in ASWM try { - // Note: createEmptyWorld is fast, so we can run some parts sync if needed, - // but it's best to run the whole chain async. Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { - // Create the database entry first so the loader has a row to update - - Island island = new Island(name, uuid, displayName, descritpion, owner, ownerType, null); + SlimeWorld slimeWorld; + Island island = new Island(type, uuid, displayName, descritpion, owner, ownerType, null); island.save(); - SlimeWorld slimeWorld = asp.createEmptyWorld(uuid, false, props, loader); + if (fileLoader.worldExists(type)){ + slimeWorld = asp.readWorld(fileLoader, type, false, new SlimePropertyMap()).clone(uuid, databaseLoader); + asp.saveWorld(slimeWorld); + + }else { + SlimePropertyMap props = new SlimePropertyMap(); + props.setValue(SlimeProperties.ENVIRONMENT, "normal"); + props.setValue(SlimeProperties.WORLD_TYPE, "flat"); + props.setValue(SlimeProperties.ALLOW_ANIMALS, false); + props.setValue(SlimeProperties.ALLOW_MONSTERS, false); + props.setValue(SlimeProperties.SPAWN_X, 0); + props.setValue(SlimeProperties.SPAWN_Y, 2); + props.setValue(SlimeProperties.SPAWN_Z, 0); + + slimeWorld = asp.createEmptyWorld(uuid, false, props, databaseLoader); + asp.saveWorld(slimeWorld); + + } - Bukkit.getScheduler().runTask(plugin, () -> { - asp.loadWorld(slimeWorld, true); - }); } catch (Exception e) { e.printStackTrace(); } }); } catch (Exception e) { e.printStackTrace(); } + + createWorldRegion(uuid, owner, ownerType, type); + + return uuid; } - // 2. Retrieve and Load existing island - public int loadIsland(String uuid) { - if (Bukkit.getWorld(uuid) != null) { - return 0; + + public int teleport(Player player, String uuid) throws Exception { + return teleport(player, uuid, null, null, null, null, null); + + } + + public int teleport(Player player, String uuid, Integer x, Integer y, Integer z, Float yaw, Float pitch) throws Exception { + + if(uuid == null) return 4; + + if (Bukkit.getWorld(uuid) != null){ + teleportLocally(player, uuid, x, y, z, yaw, pitch); + return 2; } - try { - Island island = DaoRegistry.getIslandDao().queryBuilder() - .selectColumns("active_on") - .where() - .eq("uuid", uuid) - .queryForFirst(); - - if (!island.getActiveOn().isEmpty()){ - return 1; + String whereIsActive = whereIsActive(uuid); + if (whereIsActive != null && !whereIsActive.isEmpty()){ + plugin.getLogger().info("dd: "+ whereIsActive); + sendPlayerAway(player, whereIsActive, uuid, x, y, z, yaw, pitch); + return 3; + } + + QueryBuilder queryBuilder = dao.queryBuilder().setCountOf(true); + queryBuilder.where().eq("uuid", uuid); + + if (dao.countOf(queryBuilder.prepare()) < 1){ + return 1; + } + + if (player.getVirtualHost() != null){ + String emptiestServer = getEmptiestServer(); + if (!Objects.equals(emptiestServer, config.getString("server.name"))){ + sendPlayerAway(player, emptiestServer, uuid, x, y, z, yaw, pitch); } - } catch (SQLException e) { - return 2; + } + teleportLocally(player, uuid, x, y, z, yaw, pitch); + return 0; + + } + + public void teleportLocally(Player player, String uuid, + Integer x, Integer y, Integer z, + Float yaw, Float pitch) { + + loadIsland(uuid).thenAccept(world -> { + + Location location = world.getSpawnLocation(); + + if (x != null) { + location = new Location(world, x, y, z, yaw, pitch); + } + + player.teleport(location); + + }).exceptionally(ex -> { + ex.printStackTrace(); + player.sendMessage("§cFailed to load island."); + return null; + }); + } + + private String whereIsActive(String uuid) throws SQLException { + Island island = dao.queryBuilder() + .selectColumns("active_on") + .where() + .eq("uuid", uuid) + .queryForFirst(); + + if (island == null){ + return null; + } + if (island.getActiveOn() == null){ + return null; + } + + return island.getActiveOn(); + } + + private void sendPlayerAway(Player player, String server, String uuid, Integer x, Integer y, Integer z, Float yaw, Float pitch) throws SQLException { + TeleportRequest teleportRequest = new TeleportRequest(player.getName(), server, uuid, x, y, z, yaw, pitch); + teleportRequest.save(); + + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("Connect"); + out.writeUTF(server); + player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); + } + + public void loadIslandTemplate(String name){ + SlimeWorld slimeWorld = null; + try { + slimeWorld = asp.readWorld(fileLoader, name, false, new SlimePropertyMap()); + asp.loadWorld(slimeWorld, true); + + } catch (UnknownWorldException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (CorruptedWorldException e) { + throw new RuntimeException(e); + } catch (NewerFormatException e) { + throw new RuntimeException(e); + } + + } + + public CompletableFuture loadIsland(String uuid) { + + CompletableFuture future = new CompletableFuture<>(); + + if (Bukkit.getWorld(uuid) != null) { + future.complete(Bukkit.getWorld(uuid)); + return future; + } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { - SlimeWorld slimeWorld = asp.readWorld(loader, uuid, false, new SlimePropertyMap()); + SlimeWorld slimeWorld = asp.readWorld(databaseLoader, uuid, false, new SlimePropertyMap()); + Bukkit.getScheduler().runTask(plugin, () -> { - asp.loadWorld(slimeWorld, true); + try { + + UpdateBuilder updateBuilder = dao.updateBuilder(); + updateBuilder.where().eq("uuid", uuid); + updateBuilder.updateColumnValue("active_on", config.getString("server.name")); + updateBuilder.update(); + + asp.loadWorld(slimeWorld, true); + + World world = Bukkit.getWorld(uuid); + if (world != null) { + future.complete(world); + } else { + future.completeExceptionally( + new IllegalStateException("World loaded but Bukkit returned null")); + } + + } catch (Exception e) { + future.completeExceptionally(e); + } }); - } catch (Exception e) { e.printStackTrace(); } + + } catch (Exception e) { + future.completeExceptionally(e); + } }); - return 0; + + return future; + } + + private void createWorldRegion(String uuid, String owner, String ownerType, String islandType) { + int x1 = -32; + int x2 = 31; + int y1 = -64; + int y2 = 320; + int z1 = -32; + int z2 = 31; + + + Region region = new Region(uuid, -1, x1, y1, z1, x2, y2, z2); + region.save(); + region.addRegionMember(ownerType, owner, "owner"); + } + + public void changeSpawn(Location location, String uuid) { + SlimeWorld slimeWorld = asp.getLoadedWorld(uuid); + SlimePropertyMap slimePropertyMap = slimeWorld.getPropertyMap(); + slimePropertyMap.setValue(SlimeProperties.SPAWN_X, location.getBlockX()); + slimePropertyMap.setValue(SlimeProperties.SPAWN_Y, location.getBlockY()); + slimePropertyMap.setValue(SlimeProperties.SPAWN_Z, location.getBlockZ()); + + try { + asp.saveWorld(slimeWorld); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void changeEnviroment( String environment, String uuid) { + SlimeWorld slimeWorld = asp.getLoadedWorld(uuid); + SlimePropertyMap slimePropertyMap = slimeWorld.getPropertyMap(); + slimePropertyMap.setValue(SlimeProperties.ENVIRONMENT, environment); + + try { + asp.saveWorld(slimeWorld); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void expandIsland(float yaw, int lenght, String uuid){ + World world = Bukkit.getWorld(uuid); + + if (world == null){ + return; + } + + String islandType = (String) PDC.getUniversal(world, "island_type", PersistentDataType.STRING); + switch (islandType){ + case "flat_grass" -> flatExpander(world, yaw, lenght, Material.GRASS_BLOCK, Material.STONE, Material.WATER); + case "flat_sand" -> flatExpander(world, yaw, lenght, Material.SAND, Material.STONE, Material.WATER); + case "flat_hell" -> flatExpander(world, yaw, lenght, Material.SOUL_SAND, Material.NETHERRACK, Material.LAVA); + + + } + + + } + + public void flatExpander(World world, float yaw, int lenght, Material surface, Material base, Material surrounding){ + + int x1 = (int) PDC.getUniversal(world, "borderx1", PersistentDataType.INTEGER); + int x2 = (int) PDC.getUniversal(world, "borderx2", PersistentDataType.INTEGER); + int z1 = (int) PDC.getUniversal(world, "bordery1", PersistentDataType.INTEGER); + int z2 = (int) PDC.getUniversal(world, "bordery2", PersistentDataType.INTEGER); + int y = world.getMinHeight(); + + + float yawNormalized = yaw % 360f; + if (yawNormalized < 0) yawNormalized += 360f; + + int quadrant = Math.round(yawNormalized / 90.0f) & 3; + + + int xMin; + int xMax; + int zMin; + int zMax; + + switch (quadrant) { + case 0: + //+Z + xMin = Math.min(x1, x2); + xMax = Math.max(x1, x2); + zMin = Math.max(z1, z2); + zMax = zMin + lenght; + z2 = Math.min(z1, z2); + z1 = zMax; + + break; + case 1: + //-X + zMin = Math.min(z1, z2); + zMax = Math.max(z1, z2); + xMax = Math.min(x1, x2); + xMin = xMax - lenght; + x2 = Math.max(x1, x2); + x1 = xMin; + + break; + case 2: + //-Z + xMin = Math.min(x1, x2); + xMax = Math.max(x1, x2); + zMax = Math.min(z1, z2); + zMin = zMax - lenght; + z2 = Math.max(z1, z2); + z1 = zMin; + + break; + default: + //+X + zMin = Math.min(z1, z2); + zMax = Math.max(z1, z2); + xMin = Math.max(x1, x2); + xMax = xMin + lenght; + x2 = Math.min(x1, x2); + x1 = xMax; + + break; + + } + + for (int x = xMin; x < (xMax + 1); x++) { + for (int z = zMin; z < (zMax + 1); z++) { + world.setType(x, y, z, base); + world.setType(x, y+1, z, base); + + if (((quadrant == 0 || quadrant == 2) && (x == xMin || x == xMax)) + || ((quadrant == 1 || quadrant == 3) && (z == zMin || z == zMax))) { + continue; + } + + if ((quadrant == 0 && z == zMax) + || (quadrant == 1 && x == xMin) + || (quadrant == 2 && z == zMin) + || (quadrant == 3 && x == xMax)){ + continue; + } + + world.setType(x, y+2, z, surface); + } + } + + changeBoundaries(world, x1, x2, z1, z2); + + int chunkMaxX = ((Math.max(x1, x2) + 15) & ~15) + 16; + int chunkMinX = (Math.min(x1, x2) & ~15) - 16; + int chunkMaxZ = ((Math.max(z1, z2) + 15) & ~15) + 16; + int chunkMinZ = (Math.min(z1, z2) & ~15) - 16; + + xMin = Math.min(x1, x2); + xMax = Math.max(x1, x2); + zMin = Math.min(z1, z2); + zMax = Math.max(z1, z2); + + for (int x = chunkMinX; x < chunkMaxX + 1; x ++) { + for (int z = chunkMinZ; z < chunkMaxZ + 1; z ++) { + if ((x >= xMin && x <= xMax) && (z >= zMin && z <= zMax)) { + continue; + } + world.getBlockAt(x,y,z).setType(surrounding, false); + world.getBlockAt(x,y+1,z).setType(surrounding, false); + + } + } + ; + } + + private void changeBoundaries(World world, int x1, int x2, int z1, int z2){ + PDC.setUniversal(world, "borderx1", x1, PersistentDataType.INTEGER); + PDC.setUniversal(world, "borderx2", x2, PersistentDataType.INTEGER); + PDC.setUniversal(world, "bordery1", z1, PersistentDataType.INTEGER); + PDC.setUniversal(world, "bordery2", z2, PersistentDataType.INTEGER); + + QueryBuilder queryBuilder = DaoRegistry.getRegionDao().queryBuilder(); + try { + queryBuilder.where() + .eq("island", world.getName()) + .and() + .eq("region_type", 0); + Region region = queryBuilder.queryForFirst(); + region.changeBoundaries(x1, 320, z1, x2,-64, z2); + region.save(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + + + private String getEmptiestServer() throws Exception { + String query = "SELECT active_servers.name " + + "FROM active_servers " + + "LEFT JOIN islands ON active_servers.name = islands.active_on " + + "GROUP BY active_servers.name " + + "ORDER BY COUNT(islands.uuid) ASC " + + "LIMIT 1"; + + GenericRawResults rawResults = + DaoRegistry.getActiveServerDao().queryRaw(query); + + String[] firstResult = rawResults.getFirstResult(); + rawResults.close(); + + if (firstResult != null && firstResult.length > 0) { + String serverName = firstResult[0]; + return firstResult[0]; + } + + return null; } diff --git a/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandSelectorCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandSelectorCommand.java new file mode 100644 index 0000000..546e875 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/islands/IslandSelectorCommand.java @@ -0,0 +1,188 @@ +package xyz.soukup.ecoCraftCore.islands; + +import com.github.stefvanschie.inventoryframework.gui.GuiItem; +import com.github.stefvanschie.inventoryframework.gui.type.ChestGui; +import com.github.stefvanschie.inventoryframework.gui.type.HopperGui; +import com.github.stefvanschie.inventoryframework.pane.OutlinePane; +import com.github.stefvanschie.inventoryframework.pane.PaginatedPane; +import com.github.stefvanschie.inventoryframework.pane.component.PagingButtons; +import com.github.stefvanschie.inventoryframework.pane.util.Slot; +import com.j256.ormlite.stmt.QueryBuilder; +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 net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import xyz.soukup.ecoCraftCore.database.DaoRegistry; +import xyz.soukup.ecoCraftCore.database.objects.Island; +import xyz.soukup.ecoCraftCore.database.objects.Region; +import xyz.soukup.ecoCraftCore.database.objects.RegionMember; +import xyz.soukup.ecoCraftCore.gui.GuiItemBuilder; +import xyz.soukup.ecoCraftCore.messages.Messages; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class IslandSelectorCommand { + + public static LiteralArgumentBuilder getCommand(){ + return Commands.literal("is") + .executes(IslandSelectorCommand::displayIslandListSelectorGui); + } + + private static int displayIslandListSelectorGui(CommandContext context) { + String title = LegacyComponentSerializer.legacySection().serialize(Messages.get("menu.island-selector.title")); + HopperGui hopperGui = new HopperGui(title); + hopperGui.setOnGlobalClick(event -> event.setCancelled(true)); + + OutlinePane outlinePane = new OutlinePane(0, 0, 5, 1); + Player player = (Player) context.getSource().getSender(); + + outlinePane.addItem(selectorItem("menu.island-selector.my-islands", Material.GRASS_BLOCK, getMyIslands(player))); + outlinePane.addItem(selectorItem("menu.island-selector.shared-islands", Material.MOSS_BLOCK, getSharedIslands(player))); + outlinePane.addItem(selectorItem("menu.island-selector.public-islands", Material.SAND, getPublicIslands())); + + hopperGui.getSlotsComponent().addPane(outlinePane); + + if (player.isOp()){ + outlinePane.addItem(selectorItem("menu.island-selector.all-islands", Material.CRIMSON_NYLIUM, getAllIslands())); + } + + hopperGui.show(player); + return 0; + } + + private static List getAllIslands(){ + QueryBuilder queryBuilder = DaoRegistry.getIslandDao().queryBuilder(); + queryBuilder.selectColumns("display_name", "uuid", "descritpion", "type"); + try { + return queryBuilder.query(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private static List getPublicIslands(){ + QueryBuilder queryBuilder = DaoRegistry.getIslandDao().queryBuilder(); + queryBuilder.selectColumns("display_name", "uuid", "descritpion", "type"); + try { + queryBuilder.where().eq("is_public", true); + return queryBuilder.query(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private static List getMyIslands(Player player){ + QueryBuilder queryBuilder = DaoRegistry.getIslandDao().queryBuilder(); + queryBuilder.selectColumns("display_name", "uuid", "descritpion", "type"); + try { + queryBuilder.where().eq("owner", player.getName()).and().eq("owner_type", "player"); + return queryBuilder.query(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private static List getSharedIslands(Player player){ + QueryBuilder memberQb = DaoRegistry.getRegionMemberDao().queryBuilder(); + memberQb.selectColumns("region_id"); + try { + memberQb.where().eq("member_name", player.getName()).and().eq("member_type", "player"); + + QueryBuilder regionQb = DaoRegistry.getRegionDao().queryBuilder(); + regionQb.selectColumns("island"); + regionQb.where().in("id", memberQb); + + return DaoRegistry.getIslandDao().queryBuilder() + .where() + .in("uuid", regionQb) + .query(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + } + + public static GuiItem selectorItem(String key, Material material, List islands){ + GuiItemBuilder guiItemBuilder = new GuiItemBuilder(material); + guiItemBuilder.setName(Messages.get(key)); + GuiItem guiItem = guiItemBuilder.build(); + guiItem.setAction(event -> openIslandListGui((Player) event.getWhoClicked(), islands)); + return guiItem; + } + + + private static void openIslandListGui(Player player, List islands){ + String title = LegacyComponentSerializer.legacySection().serialize(Messages.get("menu.island-selector.title")); + ChestGui chestGui = new ChestGui(4, title); + chestGui.setOnGlobalClick(event -> event.setCancelled(true)); + + PaginatedPane paginatedPane = new PaginatedPane(0, 0, 9, 3); + + paginatedPane.populateWithGuiItems(itemsFromIslands(player, islands)); + PagingButtons pagingButtons = new PagingButtons(Slot.fromXY(0, 3), 9, paginatedPane); + + chestGui.addPane(paginatedPane); + chestGui.addPane(pagingButtons); + + chestGui.show(player); + } + + + private static List itemsFromIslands(Player player, List islands){ + List guiItems = new ArrayList<>(); + IslandManager islandManager = new IslandManager(); + for (Island island : islands){ + Material material; + TextColor color; + + switch (island.getType()){ + case "flat_grass": + material = Material.GRASS_BLOCK; + color = TextColor.color(0x02bd02); + break; + case "flat_sand": + material = Material.SAND; + color = TextColor.color(0xfccf03); + break; + case "flat_hell": + material = Material.SOUL_SAND; + color = TextColor.color(0xf00707); + break; + case "void": + material = Material.GLASS; + color = TextColor.color(0xffffff); + break; + default: + material = Material.WHITE_WOOL; + color = TextColor.color(0xffffff); + break; + } + + + GuiItemBuilder guiItemBuilder = new GuiItemBuilder(material); + guiItemBuilder.setName(Component.text(island.getDisplayName(), color)); + guiItemBuilder.setRawLore(island.getDescritpion()); + + GuiItem guiItem = guiItemBuilder.build(); + guiItem.setAction(event -> { + try { + islandManager.teleport(player, island.getUuid()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + guiItems.add(guiItem); + + } + return guiItems; + } +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/islands/UnloadWorld.java b/src/main/java/xyz/soukup/ecoCraftCore/islands/UnloadIsland.java similarity index 61% rename from src/main/java/xyz/soukup/ecoCraftCore/islands/UnloadWorld.java rename to src/main/java/xyz/soukup/ecoCraftCore/islands/UnloadIsland.java index 63977a8..9a14b2f 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/islands/UnloadWorld.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/islands/UnloadIsland.java @@ -1,4 +1,4 @@ package xyz.soukup.ecoCraftCore.islands; -public class UnloadWorld { +public class UnloadIsland { } diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java new file mode 100644 index 0000000..25cd2f4 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineCommand.java @@ -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 createCommand() { + LiteralArgumentBuilder regenerate = Commands.literal("regenerate") + .executes(MineCommand::regenerateMines); + + LiteralArgumentBuilder 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 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 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; + } +} + diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java new file mode 100644 index 0000000..614575d --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineManager.java @@ -0,0 +1,696 @@ +package xyz.soukup.ecoCraftCore.mines; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +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 = 30; + private static final int FIRST_BRANCH_MIN_SIZE = 20; + private static final int FIRST_BRANCH_SPLIT_DELAY = 12; + private static final int MAX_BRANCH_SIZE = 80; + private static final double BASE_SPLIT_INCREMENT = 0.004; + private static final double SPLIT_DECAY_PER_BRANCH = 0.0004; + private static final int BLOCKS_PER_TICK = 800; + + // Transition zone: how many blocks before/after a border we blend + private static final int TRANSITION_RANGE = 8; + + private static final Random random = new Random(); + + private static final BlockFace[] DIRECTIONS = { + BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.DOWN, BlockFace.UP + }; + + // --- Biome definitions --- + + public enum MineBiome { + NORMAL, DEEP, SUPER_DEEP + } + + // Fill progression per biome + private static final Material[] NORMAL_FILLS = {Material.STONE, Material.ANDESITE, Material.COBBLESTONE, Material.TUFF}; + private static final int[] NORMAL_THRESHOLDS = {0, 30, 60, 90}; + + private static final Material[] DEEP_FILLS = {Material.DEEPSLATE, Material.COBBLED_DEEPSLATE, Material.CRACKED_DEEPSLATE_TILES}; + private static final int[] DEEP_THRESHOLDS = {0, 40, 80}; + + private static final Material[] SUPER_DEEP_FILLS = {Material.SMOOTH_BASALT, Material.BLACKSTONE, Material.CRACKED_POLISHED_BLACKSTONE_BRICKS, Material.GILDED_BLACKSTONE}; + private static final int[] SUPER_DEEP_THRESHOLDS = {0, 40, 80, 120}; + + // Special inline blocks (placed during generation, not ores) + private static final double DIRT_CHANCE = 0.04; + private static final double GRAVEL_CHANCE = 0.05; + private static final double INFESTED_CHANCE = 0.008; + private static final double MUD_CHANCE_SUPER_DEEP = 0.03; + private static final double LAVA_CHANCE_SUPER_DEEP = 0.05; + + // Deep biome lava: starts low and increases through the fills + private static final double LAVA_CHANCE_DEEP_MIN = 0.005; + private static final double LAVA_CHANCE_DEEP_MAX = 0.04; + + // --- Ore tables per fill type (rolled on break) --- + + public record OreEntry(Material material, double weight) {} + + private static final Map ORE_TABLES = new LinkedHashMap<>(); + + static { + ORE_TABLES.put(Material.STONE, new OreEntry[]{ + new OreEntry(Material.COPPER_ORE, 1.0), + }); + ORE_TABLES.put(Material.ANDESITE, new OreEntry[]{ + new OreEntry(Material.COAL_ORE, 1.0), + }); + ORE_TABLES.put(Material.COBBLESTONE, new OreEntry[]{ + new OreEntry(Material.IRON_ORE, 1.0), + }); + ORE_TABLES.put(Material.TUFF, new OreEntry[]{ + new OreEntry(Material.GOLD_ORE, 1.0), + }); + ORE_TABLES.put(Material.DEEPSLATE, new OreEntry[]{ + new OreEntry(Material.DEEPSLATE_EMERALD_ORE, 0.4), + new OreEntry(Material.DEEPSLATE_LAPIS_ORE, 0.6), + }); + ORE_TABLES.put(Material.COBBLED_DEEPSLATE, new OreEntry[]{ + new OreEntry(Material.DEEPSLATE_REDSTONE_ORE, 0.6), + new OreEntry(Material.RAW_COPPER_BLOCK, 0.4), + }); + ORE_TABLES.put(Material.CRACKED_DEEPSLATE_TILES, new OreEntry[]{ + new OreEntry(Material.DEEPSLATE_DIAMOND_ORE, 0.6), + new OreEntry(Material.RAW_IRON_BLOCK, 0.4), + }); + ORE_TABLES.put(Material.SMOOTH_BASALT, new OreEntry[]{ + new OreEntry(Material.COAL_BLOCK, 1.0), + }); + ORE_TABLES.put(Material.BLACKSTONE, new OreEntry[]{ + new OreEntry(Material.RAW_GOLD_BLOCK, 1.0), + }); + ORE_TABLES.put(Material.CRACKED_POLISHED_BLACKSTONE_BRICKS, new OreEntry[]{ + new OreEntry(Material.ANCIENT_DEBRIS, 1.0), + }); + ORE_TABLES.put(Material.GILDED_BLACKSTONE, new OreEntry[]{ + new OreEntry(Material.GOLD_BLOCK, 1.0), + }); + } + + // Ore chance depending on how far this fill is from the detected zone + // Index 0 = current fill, 1 = previous fill (one step lighter), 2 = three fills back, 3+ = any other + private static final double ORE_CHANCE_CURRENT = 0.05; + private static final double ORE_CHANCE_PREVIOUS = 0.10; + private static final double ORE_CHANCE_THREE_BACK = 0.02; + private static final double ORE_CHANCE_OTHER = 0.01; + + // Ordered list of ALL fills from shallowest to deepest (used for distance calculations) + private static final List ALL_FILLS_ORDERED = new ArrayList<>(); + + static { + Collections.addAll(ALL_FILLS_ORDERED, NORMAL_FILLS); + Collections.addAll(ALL_FILLS_ORDERED, DEEP_FILLS); + Collections.addAll(ALL_FILLS_ORDERED, SUPER_DEEP_FILLS); + } + + // --- All known fill materials (for checking in MineWorldManager) --- + + private static final Set ALL_FILL_MATERIALS = new HashSet<>(ALL_FILLS_ORDERED); + + public static boolean isFillMaterial(Material material) { + return ALL_FILL_MATERIALS.contains(material); + } + + // --- Biome Y tracking --- + + private static int normalStartY = 0; + private static int deepStartY = 0; + private static int superDeepStartY = 0; + + // --- Branch data class --- + + private static class Branch { + final BlockFace direction; + final int startX, startY, startZ; + final int distanceFromStart; + final int maxSize; + final boolean isFirst; + final MineBiome biome; + double chanceToSplit; + + int endX, endY, endZ; + + Branch(BlockFace direction, int startX, int startY, int startZ, + int distanceFromStart, int maxSize, boolean isFirst, MineBiome biome) { + this.direction = direction; + this.startX = startX; + this.startY = startY; + this.startZ = startZ; + this.distanceFromStart = distanceFromStart; + this.maxSize = maxSize; + this.isFirst = isFirst; + this.biome = biome; + this.chanceToSplit = 0; + this.endX = startX; + this.endY = startY; + this.endZ = startZ; + } + } + + private static class BlockPlacement { + final int x, y, z; + final Material material; + + BlockPlacement(int x, int y, int z, Material material) { + this.x = x; + this.y = y; + this.z = z; + this.material = material; + } + } + + // ===================== MAIN ENTRY POINT ===================== + + public static void regenerate(World world) { + int startY = world.getMaxHeight() - 11; + int minY = world.getMinHeight() + 5; + int maxY = world.getMaxHeight(); + int worldMinY = world.getMinHeight(); + + normalStartY = startY; + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + Set fillPositions = new HashSet<>(); + List fillPlacements = new ArrayList<>(); + + // ===== Biome 1: NORMAL ===== + plugin.getLogger().info("[MineGen] === Starting NORMAL biome generation ==="); + List normalBranches = new ArrayList<>(); + int[] branchCount = {0}; + generateBiomeBranches(MineBiome.NORMAL, 0, startY, 0, startY, minY, + fillPlacements, fillPositions, normalBranches, branchCount); + plugin.getLogger().info("[MineGen] NORMAL done: " + fillPlacements.size() + " fill blocks, branches=" + branchCount[0]); + + // Find lowest Y from NORMAL + Branch lowestNormal = findLowestBranch(normalBranches); + int deepX = lowestNormal != null ? lowestNormal.endX : 0; + int deepY = lowestNormal != null ? lowestNormal.endY : startY - 100; + int deepZ = lowestNormal != null ? lowestNormal.endZ : 0; + deepStartY = deepY; + + // ===== Biome 2: DEEP ===== + plugin.getLogger().info("[MineGen] === Starting DEEP biome generation at Y=" + deepY + " ==="); + List deepBranches = new ArrayList<>(); + generateBiomeBranches(MineBiome.DEEP, deepX, deepY, deepZ, deepY, minY, + fillPlacements, fillPositions, deepBranches, branchCount); + plugin.getLogger().info("[MineGen] DEEP done: " + fillPlacements.size() + " total fill blocks, branches=" + branchCount[0]); + + // Find lowest Y from DEEP + Branch lowestDeep = findLowestBranch(deepBranches); + int sdX = lowestDeep != null ? lowestDeep.endX : deepX; + int sdY = lowestDeep != null ? lowestDeep.endY : deepY - 100; + int sdZ = lowestDeep != null ? lowestDeep.endZ : deepZ; + superDeepStartY = sdY; + + // ===== Biome 3: SUPER_DEEP ===== + plugin.getLogger().info("[MineGen] === Starting SUPER_DEEP biome generation at Y=" + sdY + " ==="); + List superDeepBranches = new ArrayList<>(); + generateBiomeBranches(MineBiome.SUPER_DEEP, sdX, sdY, sdZ, sdY, minY, + fillPlacements, fillPositions, superDeepBranches, branchCount); + plugin.getLogger().info("[MineGen] SUPER_DEEP done: " + fillPlacements.size() + " total fill blocks, branches=" + branchCount[0]); + + // ===== Walls ===== + List wallPlacements = new ArrayList<>(); + generateWalls(fillPositions, wallPlacements, startY); + plugin.getLogger().info("[MineGen] Walls done: " + wallPlacements.size() + " bedrock blocks."); + + // ===== Schedule placement on main thread ===== + Bukkit.getScheduler().runTask(plugin, () -> + scheduleBlockPlacements(world, fillPlacements, wallPlacements, startY, worldMinY, maxY)); + }); + } + + // ===================== BIOME BRANCH GENERATION ===================== + + private static void generateBiomeBranches(MineBiome biome, int startX, int startY, int startZ, + int ceilingY, int minY, + List placements, Set fillPositions, + List completedBranches, int[] branchCount) { + Deque queue = new ArrayDeque<>(); + + // Reset branch count for this biome so split chance starts fresh + int[] biomeBranchCount = {0}; + + int firstSize = Math.max(FIRST_BRANCH_MIN_SIZE, MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE)); + Branch initial = new Branch(BlockFace.DOWN, startX, startY, startZ, 0, firstSize, true, biome); + queue.add(initial); + biomeBranchCount[0]++; + branchCount[0]++; + + plugin.getLogger().info("[MineGen] [" + biome + "] Starting at (" + startX + "," + startY + "," + startZ + "), minY=" + minY); + + while (!queue.isEmpty()) { + Branch branch = queue.pollFirst(); + double currentIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (biomeBranchCount[0] * SPLIT_DECAY_PER_BRANCH)); + plugin.getLogger().info("[MineGen] [" + biome + "] Processing branch dir=" + branch.direction + + " start=(" + branch.startX + "," + branch.startY + "," + branch.startZ + ")" + + " dist=" + branch.distanceFromStart + " maxSize=" + branch.maxSize + + " first=" + branch.isFirst + + " queued=" + queue.size() + " fillBlocks=" + fillPositions.size() + + " splitIncrement=" + String.format("%.5f", currentIncrement)); + + generateSingleBranch(branch, queue, placements, fillPositions, ceilingY, minY, biomeBranchCount); + completedBranches.add(branch); + } + + plugin.getLogger().info("[MineGen] [" + biome + "] Complete. Branches=" + completedBranches.size()); + } + + private static void generateSingleBranch(Branch branch, Deque queue, List placements, + Set fillPositions, int ceilingY, int minY, int[] branchCount) { + int cx = branch.startX; + int cy = branch.startY; + int cz = branch.startZ; + + for (int step = 0; step < branch.maxSize; step++) { + int distance = branch.distanceFromStart + step; + + generateCrossSection(cx, cy, cz, branch.direction, distance, branch.biome, placements, fillPositions); + + branch.endX = cx; + branch.endY = cy; + branch.endZ = cz; + + // First branch: skip split logic for the first 12 blocks + if (branch.isFirst && step < FIRST_BRANCH_SPLIT_DELAY) { + cx += branch.direction.getModX(); + cy += branch.direction.getModY(); + cz += branch.direction.getModZ(); + if (cy < minY || cy > ceilingY + 5) break; + continue; + } + + double splitIncrement = Math.max(0, BASE_SPLIT_INCREMENT - (branchCount[0] * SPLIT_DECAY_PER_BRANCH)); + + if (splitIncrement <= 0) { + cx += branch.direction.getModX(); + cy += branch.direction.getModY(); + cz += branch.direction.getModZ(); + if (cy < minY || cy > ceilingY + 5) break; + continue; + } + + branch.chanceToSplit += splitIncrement; + + if (random.nextDouble() < branch.chanceToSplit) { + branch.chanceToSplit = 0; + + int splitCount = 1 + random.nextInt(4); + List available = getAvailableDirections(branch.direction); + Collections.shuffle(available, random); + + int created = 0; + for (BlockFace dir : available) { + if (created >= splitCount) break; + + int branchMaxSize = MIN_BRANCH_SIZE + random.nextInt(MAX_BRANCH_SIZE - MIN_BRANCH_SIZE); + + if (dir == BlockFace.UP && cy + branchMaxSize > ceilingY) continue; + if (dir == BlockFace.DOWN && cy - branchMaxSize < minY) continue; + + queue.add(new Branch(dir, cx, cy, cz, distance, branchMaxSize, false, branch.biome)); + branchCount[0]++; + created++; + } + plugin.getLogger().info("[MineGen] [" + branch.biome + "] Split at step=" + step + + " pos=(" + cx + "," + cy + "," + cz + ") created=" + created + + " total=" + branchCount[0]); + } + + cx += branch.direction.getModX(); + cy += branch.direction.getModY(); + cz += branch.direction.getModZ(); + + if (cy < minY || cy > ceilingY + 5) break; + } + } + + private static Branch findLowestBranch(List branches) { + Branch lowest = null; + for (Branch b : branches) { + if (lowest == null || b.endY < lowest.endY) { + lowest = b; + } + } + return lowest; + } + + private static List getAvailableDirections(BlockFace current) { + List dirs = new ArrayList<>(); + BlockFace opposite = current.getOppositeFace(); + for (BlockFace dir : DIRECTIONS) { + if (dir != opposite && dir != current) { + dirs.add(dir); + } + } + return dirs; + } + + // ===================== CROSS SECTION GENERATION ===================== + + private static void generateCrossSection(int cx, int cy, int cz, BlockFace direction, int distance, + MineBiome biome, List placements, Set fillPositions) { + int[][] offsets = getPerpOffsets(direction); + + for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { + for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) { + int bx = cx + offsets[0][0] * a + offsets[1][0] * b; + int by = cy + offsets[0][1] * a + offsets[1][1] * b; + int bz = cz + offsets[0][2] * a + offsets[1][2] * b; + + long key = posKey(bx, by, bz); + if (!fillPositions.add(key)) continue; + + Material mat = pickFillForDistance(distance, biome); + placements.add(new BlockPlacement(bx, by, bz, mat)); + } + } + } + + // ===================== FILL SELECTION (no ores during generation) ===================== + + private static Material pickFillForDistance(int distance, MineBiome biome) { + Material[] fills = getFillsForBiome(biome); + int[] thresholds = getThresholdsForBiome(biome); + + // Determine primary fill + Material primaryFill = fills[0]; + for (int i = thresholds.length - 1; i >= 0; i--) { + if (distance >= thresholds[i]) { + primaryFill = fills[i]; + break; + } + } + + // Check transition zone + Material transitionFill = getTransitionFill(distance, fills, thresholds); + if (transitionFill != null && transitionFill != primaryFill) { + int borderDist = getDistanceToBorder(distance, thresholds); + double blendRatio = 0.5 * (1.0 - (double) Math.abs(borderDist) / TRANSITION_RANGE); + if (random.nextDouble() < blendRatio) { + primaryFill = transitionFill; + } + } + + // Roll for special inline blocks (dirt, gravel, lava, mud, infested) + Material special = rollSpecialBlock(biome, primaryFill, distance); + if (special != null) return special; + + return primaryFill; + } + + private static Material rollSpecialBlock(MineBiome biome, Material fill, int distance) { + double roll = random.nextDouble(); + switch (biome) { + case NORMAL: + if (roll < DIRT_CHANCE) return Material.DIRT; + roll -= DIRT_CHANCE; + if (roll < GRAVEL_CHANCE) return Material.GRAVEL; + roll -= GRAVEL_CHANCE; + // Rare infested variants + if (roll < INFESTED_CHANCE) { + return switch (fill) { + case STONE -> Material.INFESTED_STONE; + case COBBLESTONE -> Material.INFESTED_COBBLESTONE; + default -> null; // andesite, tuff don't have infested variants + }; + } + break; + case DEEP: + // Lava in ALL deep fills, chance increases with distance through the biome + // Deep thresholds max out at 80+ (cracked_deepslate_tiles) + // Total deep distance range is roughly 0-120 + double deepProgress = Math.min(1.0, distance / 120.0); + double lavaChance = LAVA_CHANCE_DEEP_MIN + (LAVA_CHANCE_DEEP_MAX - LAVA_CHANCE_DEEP_MIN) * deepProgress; + if (roll < lavaChance) return Material.LAVA; + break; + case SUPER_DEEP: + if (roll < MUD_CHANCE_SUPER_DEEP) return Material.MUD; + roll -= MUD_CHANCE_SUPER_DEEP; + if (roll < LAVA_CHANCE_SUPER_DEEP) return Material.LAVA; + break; + } + return null; + } + + private static Material getTransitionFill(int distance, Material[] fills, int[] thresholds) { + for (int i = 1; i < thresholds.length; i++) { + int border = thresholds[i]; + int diff = distance - border; + if (Math.abs(diff) <= TRANSITION_RANGE) { + return (diff < 0) ? fills[i] : fills[i - 1]; + } + } + return null; + } + + private static int getDistanceToBorder(int distance, int[] thresholds) { + int closest = Integer.MAX_VALUE; + for (int i = 1; i < thresholds.length; i++) { + int diff = distance - thresholds[i]; + if (Math.abs(diff) < Math.abs(closest)) { + closest = diff; + } + } + return closest; + } + + private static Material[] getFillsForBiome(MineBiome biome) { + return switch (biome) { + case NORMAL -> NORMAL_FILLS; + case DEEP -> DEEP_FILLS; + case SUPER_DEEP -> SUPER_DEEP_FILLS; + }; + } + + private static int[] getThresholdsForBiome(MineBiome biome) { + return switch (biome) { + case NORMAL -> NORMAL_THRESHOLDS; + case DEEP -> DEEP_THRESHOLDS; + case SUPER_DEEP -> SUPER_DEEP_THRESHOLDS; + }; + } + + // ===================== ORE-ON-BREAK SYSTEM (called from MineWorldManager) ===================== + + /** + * Detects the deepest fill material among the 6 surrounding blocks to determine + * which fill zone the player is in. Then rolls for an ore based on the broken block's + * position in the fill sequence relative to the detected zone. + * + * Ore chances: + * 5% for ores from the current (detected) fill + * 10% for ores from the previous fill (one step lighter) + * 2% for ores from three fills back + * 1% for any other fill's ores + * + * Returns null if no ore was rolled. + */ + public static Material rollOreOnBreak(Block brokenBlock, Material brokenType) { + // Detect the deepest fill in the 6 surrounding blocks + Material detectedFill = detectDeepestSurroundingFill(brokenBlock); + if (detectedFill == null) { + // Fallback: use the broken block itself if it's a fill + detectedFill = isFillMaterial(brokenType) ? brokenType : Material.STONE; + } + + int detectedIndex = ALL_FILLS_ORDERED.indexOf(detectedFill); + int brokenIndex = ALL_FILLS_ORDERED.indexOf(brokenType); + + // If the broken block isn't a fill material, no ore roll + if (brokenIndex < 0) return null; + + // How many steps back from the detected fill is this broken block? + // detectedIndex is the deepest (highest index), brokenIndex is where we are in the sequence + int stepsBack = detectedIndex - brokenIndex; + + // Determine ore chance based on distance from detected fill + double chance; + if (stepsBack == 0) { + chance = ORE_CHANCE_CURRENT; // 5% - breaking the detected fill itself + } else if (stepsBack == 1) { + chance = ORE_CHANCE_PREVIOUS; // 10% - one step lighter + } else if (stepsBack == 2) { + chance = ORE_CHANCE_THREE_BACK; // 2% - two steps lighter + } else { + chance = ORE_CHANCE_OTHER; // 1% - anything else + } + + // Roll for ore + if (random.nextDouble() >= chance) return null; + + // Pick a random ore from the broken block's ore table + OreEntry[] oreTable = ORE_TABLES.get(brokenType); + if (oreTable == null || oreTable.length == 0) return null; + + return pickWeightedOre(oreTable); + } + + /** + * Looks at the 6 blocks surrounding the broken block and finds the deepest fill material. + */ + private static Material detectDeepestSurroundingFill(Block block) { + Material deepest = null; + int deepestIndex = -1; + + for (BlockFace face : DIRECTIONS) { + Block neighbor = block.getRelative(face); + Material type = neighbor.getType(); + + int index = ALL_FILLS_ORDERED.indexOf(type); + if (index > deepestIndex) { + deepestIndex = index; + deepest = type; + } + } + + return deepest; + } + + /** + * Picks a random ore from the table using weights. + */ + private static Material pickWeightedOre(OreEntry[] table) { + double totalWeight = 0; + for (OreEntry e : table) totalWeight += e.weight; + + double roll = random.nextDouble() * totalWeight; + double cumulative = 0; + for (OreEntry e : table) { + cumulative += e.weight; + if (roll < cumulative) return e.material; + } + return table[table.length - 1].material; + } + + // ===================== 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 fillPositions, List wallPlacements, int startY) { + Set wallPositions = new HashSet<>(); + plugin.getLogger().info("[MineGen] Generating bedrock walls for " + fillPositions.size() + " fill positions..."); + + for (long key : fillPositions) { + int x = decodeX(key); + int y = decodeY(key); + int z = decodeZ(key); + + for (BlockFace face : DIRECTIONS) { + int nx = x + face.getModX(); + int ny = y + face.getModY(); + int nz = z + face.getModZ(); + + long neighborKey = posKey(nx, ny, nz); + + if (!fillPositions.contains(neighborKey)) { + if (ny > startY) continue; + + if (wallPositions.add(neighborKey)) { + wallPlacements.add(new BlockPlacement(nx, ny, nz, Material.BEDROCK)); + } + } + } + } + } + + // ===================== BLOCK PLACEMENT SCHEDULING ===================== + + private static void scheduleBlockPlacements(World world, List fillPlacements, + List wallPlacements, + int startY, int minY, int maxY) { + // Three-phase placement: 1) Dirt placeholder 2) Bedrock walls 3) Actual fill + List allPlacements = new ArrayList<>(); + + // Phase A: Dirt placeholders at all fill positions + List dirtPlaceholders = new ArrayList<>(fillPlacements.size()); + for (BlockPlacement bp : fillPlacements) { + dirtPlaceholders.add(new BlockPlacement(bp.x, bp.y, bp.z, Material.DIRT)); + } + allPlacements.addAll(dirtPlaceholders); + + // Phase B: Bedrock walls + allPlacements.addAll(wallPlacements); + + // Phase C: Actual fill (overwrites dirt) + allPlacements.addAll(fillPlacements); + + // Air blocks above the entrance opening + for (int a = -TUNNEL_RADIUS; a <= TUNNEL_RADIUS; a++) { + for (int b = -TUNNEL_RADIUS; b <= TUNNEL_RADIUS; b++) { + allPlacements.add(new BlockPlacement(a, startY + 1, b, Material.AIR)); + } + } + + int totalBlocks = allPlacements.size(); + int tickDelay = 0; + + plugin.getLogger().info("[MineGen] Scheduling " + totalBlocks + " block placements (" + + dirtPlaceholders.size() + " placeholder + " + wallPlacements.size() + " walls + " + + fillPlacements.size() + " fill) in batches of " + BLOCKS_PER_TICK); + + for (int i = 0; i < totalBlocks; i += BLOCKS_PER_TICK) { + int from = i; + int to = Math.min(i + BLOCKS_PER_TICK, totalBlocks); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + for (int j = from; j < to; j++) { + BlockPlacement bp = allPlacements.get(j); + if (bp.y < minY || bp.y >= maxY) continue; + world.getBlockAt(bp.x, bp.y, bp.z).setType(bp.material, false); + } + }, tickDelay); + + tickDelay++; + } + + int finalDelay = tickDelay; + Bukkit.getScheduler().runTaskLater(plugin, + () -> plugin.getLogger().info("Mine regeneration complete. " + totalBlocks + " blocks placed."), + finalDelay + 2); + } + + // ===================== POSITION ENCODING/DECODING ===================== + + private static long posKey(int x, int y, int z) { + return ((long) (x + 30000000) & 0x3FFFFFF) + | (((long) (y + 2048) & 0xFFF) << 26) + | (((long) (z + 30000000) & 0x3FFFFFF) << 38); + } + + private static int decodeX(long key) { + return (int) (key & 0x3FFFFFF) - 30000000; + } + + private static int decodeY(long key) { + return (int) ((key >> 26) & 0xFFF) - 2048; + } + + private static int decodeZ(long key) { + return (int) ((key >> 38) & 0x3FFFFFF) - 30000000; + } +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java new file mode 100644 index 0000000..1a2d8d8 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/mines/MineWorldManager.java @@ -0,0 +1,304 @@ +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.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; + + // 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 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 { + if (loader.worldExists(MINE_WORLD_NAME)) { + loader.deleteWorld(MINE_WORLD_NAME); + plugin.getLogger().info("[MineWorld] Old mine world deleted."); + } + + 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); + + 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 from destroying bedrock walls + @EventHandler + public void onEntityExplode(EntityExplodeEvent event) { + if (!isInMineWorld(event.getEntity().getWorld())) return; + event.blockList().removeIf(block -> block.getType() == Material.BEDROCK); + } + + // Prevent block explosions from destroying bedrock walls + @EventHandler + public void onBlockExplode(BlockExplodeEvent event) { + if (!isInMineWorld(event.getBlock().getWorld())) return; + event.blockList().removeIf(block -> block.getType() == Material.BEDROCK); + } + + // Block break logic: bedrock unbreakable, ore-on-break for ALL fills, mining sequence + @EventHandler + public void onBlockBreak(BlockBreakEvent event) { + if (!isInMineWorld(event.getPlayer())) return; + + Material type = event.getBlock().getType(); + + // Bedrock is completely unbreakable (mine walls) + if (type == Material.BEDROCK) { + event.setCancelled(true); + return; + } + + // For any fill material (at any stage of the mining sequence), roll for ore first. + // The ore replaces the block as another mining step — player then breaks the ore normally. + if (MineManager.isFillMaterial(type)) { + Material ore = MineManager.rollOreOnBreak(event.getBlock(), type); + if (ore != null) { + event.setCancelled(true); + event.getBlock().setType(ore); + event.getBlock().getState().update(true); + applyToolDamage(event); + return; + } + } + + // No ore rolled — apply normal mining sequence (deeper fills degrade into lighter fills) + Material nextTier = getNextTier(type); + if (nextTier != null) { + event.setCancelled(true); + event.getBlock().setType(nextTier); + event.getBlock().getState().update(true); + applyToolDamage(event); + return; + } + + // Terminal fill (stone) or ores — break normally + } + + /** + * Applies doubled durability damage to the player's tool when we cancel a break event. + * Only applies if the held item actually has durability. + */ + private void applyToolDamage(BlockBreakEvent event) { + ItemStack tool = event.getPlayer().getInventory().getItemInMainHand(); + if (tool.getType().isAir() || tool.getType().getMaxDurability() <= 0) return; + if (!(tool.getItemMeta() instanceof Damageable damageable)) return; + + damageable.setDamage(damageable.getDamage() + 2); // doubled durability + tool.setItemMeta(damageable); + + if (damageable.getDamage() >= tool.getType().getMaxDurability()) { + event.getPlayer().getInventory().setItemInMainHand(null); + event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f); + } + } + + /** + * Mining sequence: deeper fills degrade into lighter fills. + * Returns null when the block should break normally (terminal fill = stone). + * + * Chain: gilded_blackstone -> cracked_polished_blackstone_bricks -> blackstone -> smooth_basalt + * -> cracked_deepslate_tiles -> cobbled_deepslate -> deepslate -> tuff -> cobblestone + * -> andesite -> stone -> null (breaks normally) + */ + private Material getNextTier(Material material) { + return switch (material) { + case GILDED_BLACKSTONE -> Material.CRACKED_POLISHED_BLACKSTONE_BRICKS; + case CRACKED_POLISHED_BLACKSTONE_BRICKS -> Material.BLACKSTONE; + case BLACKSTONE -> Material.SMOOTH_BASALT; + case SMOOTH_BASALT -> Material.CRACKED_DEEPSLATE_TILES; + case CRACKED_DEEPSLATE_TILES -> Material.COBBLED_DEEPSLATE; + case COBBLED_DEEPSLATE -> Material.DEEPSLATE; + case DEEPSLATE -> Material.TUFF; + case TUFF -> Material.COBBLESTONE; + case COBBLESTONE -> Material.ANDESITE; + case ANDESITE -> Material.STONE; + default -> null; // Stone and non-fill blocks break normally + }; + } +} + diff --git a/src/main/java/xyz/soukup/ecoCraftCore/money/MoneyCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/money/MoneyCommand.java index 2590e1c..8129bc1 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/money/MoneyCommand.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/money/MoneyCommand.java @@ -19,7 +19,7 @@ import xyz.soukup.ecoCraftCore.messages.PHHM; public class MoneyCommand { - public static LiteralArgumentBuilder createCommand() { + public static LiteralArgumentBuilder getCommand() { // 1. Send Branch LiteralArgumentBuilder send = Commands.literal("send") .requires(source -> source.getSender() instanceof Player) diff --git a/src/main/java/xyz/soukup/ecoCraftCore/player/OnKill.java b/src/main/java/xyz/soukup/ecoCraftCore/player/OnKill.java new file mode 100644 index 0000000..51bfd46 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/player/OnKill.java @@ -0,0 +1,27 @@ +package xyz.soukup.ecoCraftCore.player; + +import com.destroystokyo.paper.event.player.PlayerPostRespawnEvent; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import xyz.soukup.ecoCraftCore.islands.IslandManager; + +import static xyz.soukup.ecoCraftCore.EcoCraftCore.config; + +public class OnKill implements Listener { + @EventHandler + public void spawnTeleportOnKill(PlayerPostRespawnEvent event){ + Player player = event.getPlayer(); + + String uuid = config.getString("islands.spawn"); + + IslandManager islandManager = new IslandManager(); + try { + islandManager.teleport(player, uuid); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/player/TeleportRequestsHandler.java b/src/main/java/xyz/soukup/ecoCraftCore/player/TeleportRequestsHandler.java new file mode 100644 index 0000000..56a7c25 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/player/TeleportRequestsHandler.java @@ -0,0 +1,50 @@ +package xyz.soukup.ecoCraftCore.player; + +import com.google.common.eventbus.DeadEvent; +import com.j256.ormlite.stmt.QueryBuilder; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import xyz.soukup.ecoCraftCore.database.DaoRegistry; +import xyz.soukup.ecoCraftCore.database.objects.TeleportRequest; +import xyz.soukup.ecoCraftCore.islands.IslandManager; + +import java.sql.SQLException; + +import static xyz.soukup.ecoCraftCore.EcoCraftCore.config; + +public class TeleportRequestsHandler implements Listener { + + @EventHandler + public void teleportRequestHandler(PlayerJoinEvent event){ + try { + Player player = event.getPlayer(); + + QueryBuilder queryBuilder = DaoRegistry.getTeleportRequestsDao().queryBuilder(); + queryBuilder.where().eq("player", player.getName()); + + TeleportRequest teleportRequest = queryBuilder.queryForFirst(); + + IslandManager islandManager = new IslandManager(); + + + if (teleportRequest == null){ + islandManager.teleport(player, config.getString("islands.spawn")); + return; + } + + + islandManager.teleportLocally(player, teleportRequest.getWorld(), teleportRequest.getX(), teleportRequest.getY(), teleportRequest.getY(), teleportRequest.getYaw(), teleportRequest.getPitch()); + + teleportRequest.delete(); + + } catch (SQLException e) { + throw new RuntimeException(e); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/positionMarker/RulerCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/positionMarker/MarkerCommand.java similarity index 79% rename from src/main/java/xyz/soukup/ecoCraftCore/positionMarker/RulerCommand.java rename to src/main/java/xyz/soukup/ecoCraftCore/positionMarker/MarkerCommand.java index dd09626..d70f180 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/positionMarker/RulerCommand.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/positionMarker/MarkerCommand.java @@ -11,22 +11,21 @@ import org.bukkit.inventory.ItemStack; import xyz.soukup.ecoCraftCore.messages.Messages; import xyz.soukup.ecoCraftCore.utilities.PDC; -public class RulerCommand { - public static LiteralArgumentBuilder createCommand() { +public class MarkerCommand { + public static LiteralArgumentBuilder getCommand() { return Commands.literal("ruler") - .executes(RulerCommand::obtainRuler); + .executes(MarkerCommand::obtainRuler); } private static int obtainRuler(CommandContext context){ CommandSender commandSender = context.getSource().getSender(); - if (!(commandSender instanceof Player)){ + if (!(commandSender instanceof Player player)){ Messages.send(commandSender, "generic.error.not-player"); return 0; } - Player player = (Player) commandSender; ItemStack itemStack = new ItemStack(Material.BLAZE_ROD); PDC.set(itemStack, "ruler", true); diff --git a/src/main/java/xyz/soukup/ecoCraftCore/positionMarker/RulerMarking.java b/src/main/java/xyz/soukup/ecoCraftCore/positionMarker/MarkerEvent.java similarity index 98% rename from src/main/java/xyz/soukup/ecoCraftCore/positionMarker/RulerMarking.java rename to src/main/java/xyz/soukup/ecoCraftCore/positionMarker/MarkerEvent.java index 2f47d46..caba5ab 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/positionMarker/RulerMarking.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/positionMarker/MarkerEvent.java @@ -16,7 +16,7 @@ import xyz.soukup.ecoCraftCore.messages.PHHM; import java.util.HashMap; -public class RulerMarking implements Listener { +public class MarkerEvent implements Listener { public static HashMap primaryLocations = new HashMap<>(); public static HashMap secondaryLocations = new HashMap<>(); diff --git a/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionAdminCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionAdminCommand.java new file mode 100644 index 0000000..a9c9b6d --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionAdminCommand.java @@ -0,0 +1,97 @@ +package xyz.soukup.ecoCraftCore.regions; + +import com.j256.ormlite.stmt.QueryBuilder; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +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.command.CommandSender; +import org.bukkit.entity.Player; +import xyz.soukup.ecoCraftCore.database.DaoRegistry; +import xyz.soukup.ecoCraftCore.database.objects.Region; +import xyz.soukup.ecoCraftCore.messages.Messages; + +import java.sql.SQLException; +import java.util.List; + +import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; + +public class RegionAdminCommand { + public static LiteralArgumentBuilder getCommand() { + LiteralArgumentBuilder create = Commands.literal("create") + .then(Commands.argument("type", IntegerArgumentType.integer(0, 1)) + .executes(RegionAdminCommand::createRegion)); + + LiteralArgumentBuilder addMember = Commands.literal("addMember") + .then(Commands.argument("id", IntegerArgumentType.integer()) + .suggests((context, builder) -> { + try { + QueryBuilder queryBuilder = DaoRegistry.getRegionDao().queryBuilder(); + queryBuilder.selectColumns("id"); + List regions = queryBuilder.query(); + for (Region region : regions) { + builder.suggest(region.getId()); + } + return builder.buildFuture(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }) + .then(Commands.argument("player", StringArgumentType.word()) + .suggests((context, builder) -> { + for (Player onlinePlayer : plugin.getServer().getOnlinePlayers()) { + builder.suggest(onlinePlayer.getName()); + } + return builder.buildFuture(); + }) + .then(Commands.argument("membership", StringArgumentType.word()) + .suggests(((context, builder) -> { + builder.suggest("owner"); + builder.suggest("editor"); + builder.suggest("member"); + return builder.buildFuture(); + })) + .executes(RegionAdminCommand::addMember)))); + + + return Commands.literal("region") + .then(create) + .then(addMember); + } + + private static int createRegion(CommandContext context) { + Integer type = context.getArgument("type", Integer.class); + if(!(context.getSource().getSender() instanceof Player player)){ + Messages.send(context.getSource().getSender(), "generic.error.not-player"); + return 0; + }; + + int status = RegionManager.createRegion(player, type, player.getName(), "player"); + + switch (status){ + case 0 -> Messages.send(player, "generic.success.created"); + case 1 -> Messages.send(player, "region.error.not-marked"); + case 2 -> Messages.send(player, "region.error.not-same-world"); + } + + return 0; + } + + private static int addMember(CommandContext context) { + Integer id = context.getArgument("id", Integer.class); + String player = context.getArgument("player", String.class); + String membership = context.getArgument("membership", String.class); + CommandSender commandSender = context.getSource().getSender(); + + int status = RegionManager.addMember(id, "player", player, membership); + switch (status){ + case 0 -> Messages.send(commandSender, "generic.success.created"); + case 1 -> Messages.send(commandSender, "region.error.not-exist"); + } + return 0; + } + + +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionEvents.java b/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionEvents.java new file mode 100644 index 0000000..151a597 --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionEvents.java @@ -0,0 +1,133 @@ +package xyz.soukup.ecoCraftCore.regions; + +import org.bukkit.block.Block; +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.BlockPlaceEvent; +import org.bukkit.event.player.*; +import org.bukkit.inventory.ItemStack; +import xyz.soukup.ecoCraftCore.database.objects.Region; +import xyz.soukup.ecoCraftCore.database.objects.RegionMember; + +public class RegionEvents implements Listener { + + @EventHandler + public void onPlayerInteract(PlayerInteractEvent event){ + 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() + ); + + event.setCancelled(!allowed); + } + + @EventHandler + public void onBlockPlace(BlockPlaceEvent event) { + Block block = event.getBlockPlaced(); + + boolean allowed = isAllowedToInteract( + block.getWorld().getName(), + event.getPlayer(), + block.getX(), + block.getY(), + block.getZ() + ); + + event.setCancelled(!allowed); + } + + @EventHandler + public void onBlockBreak(BlockBreakEvent event) { + + Block block = event.getBlock(); + + boolean allowed = isAllowedToInteract( + block.getWorld().getName(), + event.getPlayer(), + block.getX(), + block.getY(), + block.getZ() + ); + + event.setCancelled(!allowed); + } + + @EventHandler + public void onBucketUse(PlayerBucketEmptyEvent event) { + Block clickedBlock = event.getBlockClicked(); + + boolean allowed = isAllowedToInteract( + clickedBlock.getWorld().getName(), + event.getPlayer(), + clickedBlock.getX(), + clickedBlock.getY(), + clickedBlock.getZ() + ); + + event.setCancelled(!allowed); + } + + @EventHandler + public void onBucketUse(PlayerBucketFillEvent event) { + Block clickedBlock = event.getBlockClicked(); + + boolean allowed = isAllowedToInteract( + clickedBlock.getWorld().getName(), + event.getPlayer(), + clickedBlock.getX(), + clickedBlock.getY(), + clickedBlock.getZ() + ); + + event.setCancelled(!allowed); + } + + + + 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; + } + + String name = player.getName(); + + for (RegionMember regionMember : region.getRegionMembers()){ + + if (regionMember.getMembertype().equals("player") && regionMember.getName().equals(name)){ + return true; + } + } + + return false; + } + + +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionManager.java b/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionManager.java new file mode 100644 index 0000000..14c480d --- /dev/null +++ b/src/main/java/xyz/soukup/ecoCraftCore/regions/RegionManager.java @@ -0,0 +1,48 @@ +package xyz.soukup.ecoCraftCore.regions; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import xyz.soukup.ecoCraftCore.database.objects.Region; +import xyz.soukup.ecoCraftCore.positionMarker.MarkerEvent; + +public class RegionManager { + public static int createRegion(Player player, Integer type, String owner, String ownerType){ + Location primaryLocation = MarkerEvent.primaryLocations.get(player); + Location secondaryLocation = MarkerEvent.secondaryLocations.get(player); + + if (primaryLocation == null || secondaryLocation == null){ + return 1; + } + + if (primaryLocation.getWorld() != secondaryLocation.getWorld()){ + return 2; + } + + String worldName = primaryLocation.getWorld().getName(); + int x1 = primaryLocation.getBlockX(); + int y1 = primaryLocation.getBlockY(); + int z1 = primaryLocation.getBlockZ(); + int x2 = secondaryLocation.getBlockX(); + int y2 = secondaryLocation.getBlockY(); + int z2 = secondaryLocation.getBlockZ(); + + Region region = new Region(worldName, type, x1, y1, z1, x2, y2, z2); + region.save(); + + region.addRegionMember(ownerType, owner, "owner"); + + return 0; + } + + public static int addMember(int id, String memberType, String member, String membershipType){ + Region region = Region.findById(id); + + if (region == null){ + return 1; + } + + region.addRegionMember(memberType, member, membershipType); + + return 0; + } +} diff --git a/src/main/java/xyz/soukup/ecoCraftCore/shop/ShopCommand.java b/src/main/java/xyz/soukup/ecoCraftCore/shop/ShopCommand.java index 591003e..c53fbe0 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/shop/ShopCommand.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/shop/ShopCommand.java @@ -9,17 +9,16 @@ import org.bukkit.block.Chest; import org.bukkit.block.Sign; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import xyz.soukup.ecoCraftCore.positionMarker.RulerMarking; +import xyz.soukup.ecoCraftCore.positionMarker.MarkerEvent; import xyz.soukup.ecoCraftCore.database.objects.Shop; import xyz.soukup.ecoCraftCore.inventory.VirtualChest; import xyz.soukup.ecoCraftCore.inventory.InventoryUtils; import xyz.soukup.ecoCraftCore.messages.Messages; import xyz.soukup.ecoCraftCore.utilities.PDC; -@SuppressWarnings("UnstableApiUsage") public class ShopCommand { - public static LiteralArgumentBuilder createCommand() { + public static LiteralArgumentBuilder getCommand() { // Define the argument types var amountArg = Commands.argument("amount", IntegerArgumentType.integer(1)); var buyPriceArg = Commands.argument("buy_price", FloatArgumentType.floatArg(0.0F)); @@ -52,8 +51,8 @@ public class ShopCommand { } // 1. Check if blocks are marked - Chest chest = RulerMarking.chests.get(player); - Sign sign = RulerMarking.signs.get(player); + Chest chest = MarkerEvent.chests.get(player); + Sign sign = MarkerEvent.signs.get(player); if (sign == null || chest == null) { Messages.send(player, "shop.error.not-marked"); diff --git a/src/main/java/xyz/soukup/ecoCraftCore/utilities/PDC.java b/src/main/java/xyz/soukup/ecoCraftCore/utilities/PDC.java index 0d14c28..a79c19e 100644 --- a/src/main/java/xyz/soukup/ecoCraftCore/utilities/PDC.java +++ b/src/main/java/xyz/soukup/ecoCraftCore/utilities/PDC.java @@ -9,11 +9,15 @@ import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataHolder; import org.bukkit.persistence.PersistentDataType; +import xyz.soukup.ecoCraftCore.EcoCraftCore; + +import static xyz.soukup.ecoCraftCore.EcoCraftCore.plugin; @SuppressWarnings({"unchecked", "rawtypes"}) public class PDC { - - + + + //TileState public static void set(TileState tileState, String key, Integer value){ @@ -65,7 +69,7 @@ public class PDC { return (String) get(itemStack, key, PersistentDataType.STRING); } - private static Object get(ItemStack itemStack, String key, PersistentDataType persistentDataType){ + public static Object get(ItemStack itemStack, String key, PersistentDataType persistentDataType){ ItemMeta itemMeta = itemStack.getItemMeta(); return getUniversal(itemMeta, key, persistentDataType); } @@ -80,15 +84,15 @@ public class PDC { //Univerzální set & get - private static Object getUniversal(PersistentDataHolder persistentDataHolder, String key, PersistentDataType persistentDataType){ + public static Object getUniversal(PersistentDataHolder persistentDataHolder, String key, PersistentDataType persistentDataType){ PersistentDataContainer persistentDataContainer = persistentDataHolder.getPersistentDataContainer(); - NamespacedKey namespacedKey = new NamespacedKey("ecc", key); + NamespacedKey namespacedKey = new NamespacedKey(plugin, key); return persistentDataContainer.get(namespacedKey, persistentDataType); } - private static void setUniversal(PersistentDataHolder persistentDataHolder, String key, Object value, PersistentDataType persistentDataType){ + public static void setUniversal(PersistentDataHolder persistentDataHolder, String key, Object value, PersistentDataType persistentDataType){ PersistentDataContainer persistentDataContainer = persistentDataHolder.getPersistentDataContainer(); - NamespacedKey namespacedKey = new NamespacedKey("ecc", key); + NamespacedKey namespacedKey = new NamespacedKey(plugin, key); persistentDataContainer.set(namespacedKey, persistentDataType, value); } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index af80369..3d9ced4 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,4 +9,6 @@ database: password: "ecc" database: "ecc" cache: - save-interval: 6000 \ No newline at end of file + save-interval: 6000 +islands: + spawn: null \ No newline at end of file diff --git a/src/main/resources/island_templates/flat_grass.slime b/src/main/resources/island_templates/flat_grass.slime new file mode 100644 index 0000000..7345ce4 Binary files /dev/null and b/src/main/resources/island_templates/flat_grass.slime differ diff --git a/src/main/resources/island_templates/flat_hell.slime b/src/main/resources/island_templates/flat_hell.slime new file mode 100644 index 0000000..b047926 Binary files /dev/null and b/src/main/resources/island_templates/flat_hell.slime differ diff --git a/src/main/resources/island_templates/flat_sand.slime b/src/main/resources/island_templates/flat_sand.slime new file mode 100644 index 0000000..7874e7e Binary files /dev/null and b/src/main/resources/island_templates/flat_sand.slime differ diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 36219f0..27f75fc 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -1,4 +1,6 @@ generic: + success: + creat ed: Vytvořeno. error: not-player: "Na tuto akci musíš být hráč" no-funds: @@ -10,6 +12,10 @@ generic: no-item: self: "Nemáš dostatek itemů" shop: "Obchod nemá dostatek itemů" +region: + error: + not-marked: "Musíš nejprve označit pozice" + not-exist: "Region neexistuje" shop: error: already-shop: "Tato cedule již je obchod" @@ -44,6 +50,12 @@ marker: primary: "První pozice označena (,,)" secondary: "Druhá pozice označena (,,)" menu: + island-selector: + title: "Výběr ostrovu" + my-islands: "Moje ostrovy" + shared-islands: "Ostrovy sdílené se mnou" + public-islands: "Veřejné Ostrovy" + all-islands: "Všechny ostrovy (Pouze OP)" shop: buy: "Koupit ks za $" sell: "Prodat ks za $" @@ -65,4 +77,11 @@ gui: title: "Obchod" buy: "Koupit ks za $" sell: "Prodat ks za $" +mine: + regenerating: "Regenerace dolů začala..." + regenerate-complete: "Regenerace dolů dokončena." + teleporting: "Teleportuješ se do dolů." + teleported-out: "Byl jsi teleportován z dolů kvůli regeneraci." + error: + no-world: "Důlní svět není načtený." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5868d64..2c14e67 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,3 +2,6 @@ name: EcoCraftCore version: '1.0-SNAPSHOT' main: xyz.soukup.ecoCraftCore.EcoCraftCore api-version: '1.21' +depend: + - packetevents +