diff --git a/API/src/main/java/fr/maxlego08/essentials/api/Configuration.java b/API/src/main/java/fr/maxlego08/essentials/api/Configuration.java index d706d81d..7179d255 100644 --- a/API/src/main/java/fr/maxlego08/essentials/api/Configuration.java +++ b/API/src/main/java/fr/maxlego08/essentials/api/Configuration.java @@ -266,4 +266,19 @@ public interface Configuration extends ConfigurationFile { * @return a map of options and their default values */ Map getDefaultOptionValues(); + + /** + * Retrieves the server name used for cross-server communication. + * This is used with BungeeCord/Velocity for cross-server teleportation. + * + * @return the server name, or "default" if not configured + */ + String getServerName(); + + /** + * Checks if cross-server teleportation is enabled. + * + * @return true if cross-server teleportation is enabled + */ + boolean isCrossServerTeleportEnabled(); } diff --git a/API/src/main/java/fr/maxlego08/essentials/api/cache/ExpiringCache.java b/API/src/main/java/fr/maxlego08/essentials/api/cache/ExpiringCache.java index 2ca6a338..c7b028ce 100644 --- a/API/src/main/java/fr/maxlego08/essentials/api/cache/ExpiringCache.java +++ b/API/src/main/java/fr/maxlego08/essentials/api/cache/ExpiringCache.java @@ -28,6 +28,9 @@ public ExpiringCache(long expiryDurationMillis) { * Retrieves the value associated with the specified key from the cache. If the key is not found, * or the entry has expired, the provided Loader is used to load the value, store it in the cache, * and then return it. + *

+ * Supports soft expiration: if the entry is soft-expired but not hard-expired, it returns the old value + * and refreshes the cache asynchronously. * * @param key the key whose associated value is to be returned * @param loader the loader used to generate the value if it is not present or expired in the cache @@ -35,14 +38,42 @@ public ExpiringCache(long expiryDurationMillis) { * was not found in the cache or if the entry expired */ public V get(K key, Loader loader) { - return cache.compute(key, (k, v) -> { - long currentTime = System.currentTimeMillis(); - if (v == null || v.expiryTime < currentTime) { - V newValue = loader.load(); - return new CacheEntry<>(newValue, currentTime + expiryDurationMillis); - } - return v; - }).value; + CacheEntry entry = cache.get(key); + long currentTime = System.currentTimeMillis(); + + // 1. Hard Expiration or Not Present -> Synchronous Load + if (entry == null || entry.expiryTime < currentTime) { + return cache.compute(key, (k, v) -> { + // Double-check inside lock + long now = System.currentTimeMillis(); + if (v == null || v.expiryTime < now) { + V newValue = loader.load(); + long expiry = now + expiryDurationMillis; + long softExpiry = now + (long) (expiryDurationMillis * 0.8); // Refresh at 80% of ttl + return new CacheEntry<>(newValue, expiry, softExpiry); + } + return v; + }).value; + } + + // 2. Soft Expiration -> Return Old Value & Asynchronous Refresh + if (entry.softExpiryTime < currentTime && entry.isUpdating.compareAndSet(false, true)) { + java.util.concurrent.CompletableFuture.runAsync(() -> { + try { + V newValue = loader.load(); + long now = System.currentTimeMillis(); + long expiry = now + expiryDurationMillis; + long softExpiry = now + (long) (expiryDurationMillis * 0.8); + cache.put(key, new CacheEntry<>(newValue, expiry, softExpiry)); + } catch (Exception e) { + e.printStackTrace(); + // Reset flag on failure so we can try again later + entry.isUpdating.set(false); + } + }); + } + + return entry.value; } /** @@ -75,6 +106,16 @@ public interface Loader { /** * A simple cache entry that holds the value and its expiry time. */ - private record CacheEntry(V value, long expiryTime) { + private static class CacheEntry { + final V value; + final long expiryTime; + final long softExpiryTime; + final java.util.concurrent.atomic.AtomicBoolean isUpdating = new java.util.concurrent.atomic.AtomicBoolean(false); + + CacheEntry(V value, long expiryTime, long softExpiryTime) { + this.value = value; + this.expiryTime = expiryTime; + this.softExpiryTime = softExpiryTime; + } } } diff --git a/API/src/main/java/fr/maxlego08/essentials/api/commands/Permission.java b/API/src/main/java/fr/maxlego08/essentials/api/commands/Permission.java index 7fef5e64..fc976c17 100644 --- a/API/src/main/java/fr/maxlego08/essentials/api/commands/Permission.java +++ b/API/src/main/java/fr/maxlego08/essentials/api/commands/Permission.java @@ -38,6 +38,9 @@ public enum Permission { ESSENTIALS_TPA_ACCEPT, ESSENTIALS_TPA_DENY, ESSENTIALS_TPA_CANCEL, + ESSENTIALS_TRADE_USE, + ESSENTIALS_TRADE_ACCEPT, + ESSENTIALS_TRADE_DENY, ESSENTIALS_BYPASS_COOLDOWN("Allows not to have a cooldown on all commands"), ESSENTIALS_MORE, ESSENTIALS_TP_WORLD, diff --git a/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeCancelEvent.java b/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeCancelEvent.java new file mode 100644 index 00000000..3bcbefbc --- /dev/null +++ b/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeCancelEvent.java @@ -0,0 +1,30 @@ +package fr.maxlego08.essentials.api.event.events.trade; + +import fr.maxlego08.essentials.api.event.EssentialsEvent; +import org.bukkit.entity.Player; + +public class TradeCancelEvent extends EssentialsEvent { + + private final Player player1; + private final Player player2; + private final Player canceller; + + public TradeCancelEvent(Player player1, Player player2, Player canceller) { + this.player1 = player1; + this.player2 = player2; + this.canceller = canceller; + } + + public Player getPlayer1() { + return player1; + } + + public Player getPlayer2() { + return player2; + } + + public Player getCanceller() { + return canceller; + } +} + diff --git a/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeCompleteEvent.java b/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeCompleteEvent.java new file mode 100644 index 00000000..ee5df706 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeCompleteEvent.java @@ -0,0 +1,24 @@ +package fr.maxlego08.essentials.api.event.events.trade; + +import fr.maxlego08.essentials.api.event.EssentialsEvent; +import org.bukkit.entity.Player; + +public class TradeCompleteEvent extends EssentialsEvent { + + private final Player player1; + private final Player player2; + + public TradeCompleteEvent(Player player1, Player player2) { + this.player1 = player1; + this.player2 = player2; + } + + public Player getPlayer1() { + return player1; + } + + public Player getPlayer2() { + return player2; + } +} + diff --git a/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeStartEvent.java b/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeStartEvent.java new file mode 100644 index 00000000..2dc5f368 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/essentials/api/event/events/trade/TradeStartEvent.java @@ -0,0 +1,24 @@ +package fr.maxlego08.essentials.api.event.events.trade; + +import fr.maxlego08.essentials.api.event.CancellableEssentialsEvent; +import org.bukkit.entity.Player; + +public class TradeStartEvent extends CancellableEssentialsEvent { + + private final Player player1; + private final Player player2; + + public TradeStartEvent(Player player1, Player player2) { + this.player1 = player1; + this.player2 = player2; + } + + public Player getPlayer1() { + return player1; + } + + public Player getPlayer2() { + return player2; + } +} + diff --git a/API/src/main/java/fr/maxlego08/essentials/api/messages/Message.java b/API/src/main/java/fr/maxlego08/essentials/api/messages/Message.java index 3ce51ff3..0f38dec7 100644 --- a/API/src/main/java/fr/maxlego08/essentials/api/messages/Message.java +++ b/API/src/main/java/fr/maxlego08/essentials/api/messages/Message.java @@ -44,6 +44,7 @@ public enum Message { COMMAND_NO_ARG("Impossible to find the command with its arguments."), COMMAND_RESTRICTED("You cannot use this command here."), COMMAND_SYNTAXE_HELP("&f%syntax% &7» &7%description%"), + DESCRIPTION_TRADE("Manage trade requests"), COMMAND_RELOAD("You have just reloaded the configuration files."), COMMAND_RELOAD_MODULE("You have just reloaded the configuration files of the module &f%module%."), diff --git a/API/src/main/java/fr/maxlego08/essentials/api/server/EssentialsServer.java b/API/src/main/java/fr/maxlego08/essentials/api/server/EssentialsServer.java index fe385aee..3d847230 100644 --- a/API/src/main/java/fr/maxlego08/essentials/api/server/EssentialsServer.java +++ b/API/src/main/java/fr/maxlego08/essentials/api/server/EssentialsServer.java @@ -2,6 +2,8 @@ import fr.maxlego08.essentials.api.commands.Permission; import fr.maxlego08.essentials.api.messages.Message; +import fr.maxlego08.essentials.api.teleport.CrossServerLocation; +import fr.maxlego08.essentials.api.teleport.TeleportType; import fr.maxlego08.essentials.api.user.Option; import fr.maxlego08.essentials.api.user.PrivateMessage; import fr.maxlego08.essentials.api.user.User; @@ -142,4 +144,47 @@ public interface EssentialsServer { * @param message The message to send. */ void pub(Player player, String message); + + /** + * Sends a player to another server via BungeeCord/Velocity. + * + * @param player The player to send. + * @param serverName The target server name. + */ + void sendToServer(Player player, String serverName); + + /** + * Requests a cross-server teleport. The player will be sent to the target server + * and then teleported to the destination location. + * + * @param player The player to teleport. + * @param teleportType The type of teleportation. + * @param destination The destination location including server name. + */ + void crossServerTeleport(Player player, TeleportType teleportType, CrossServerLocation destination); + + /** + * Requests a cross-server teleport to another player. + * + * @param player The player requesting the teleport. + * @param teleportType The type of teleportation (TPA or TPA_HERE). + * @param targetPlayerName The name of the target player. + * @param targetServer The server where the target player is. + */ + void crossServerTeleportToPlayer(Player player, TeleportType teleportType, String targetPlayerName, String targetServer); + + /** + * Gets the current server name from BungeeCord configuration. + * + * @return The server name or null if not configured. + */ + String getServerName(); + + /** + * Finds which server a player is on. + * + * @param playerName The player name to find. + * @return The server name or null if not found. + */ + String findPlayerServer(String playerName); } \ No newline at end of file diff --git a/API/src/main/java/fr/maxlego08/essentials/api/server/messages/CrossServerTeleport.java b/API/src/main/java/fr/maxlego08/essentials/api/server/messages/CrossServerTeleport.java new file mode 100644 index 00000000..4a3f0923 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/essentials/api/server/messages/CrossServerTeleport.java @@ -0,0 +1,65 @@ +package fr.maxlego08.essentials.api.server.messages; + +import fr.maxlego08.essentials.api.teleport.CrossServerLocation; +import fr.maxlego08.essentials.api.teleport.TeleportType; + +import java.util.UUID; + +/** + * Redis message for cross-server teleportation requests. + */ +public class CrossServerTeleport { + + private final UUID playerUuid; + private final String playerName; + private final TeleportType teleportType; + private final CrossServerLocation destination; + private final String targetName; // For TPA - the target player name + private final long timestamp; + + public CrossServerTeleport(UUID playerUuid, String playerName, TeleportType teleportType, CrossServerLocation destination) { + this(playerUuid, playerName, teleportType, destination, null); + } + + public CrossServerTeleport(UUID playerUuid, String playerName, TeleportType teleportType, CrossServerLocation destination, String targetName) { + this.playerUuid = playerUuid; + this.playerName = playerName; + this.teleportType = teleportType; + this.destination = destination; + this.targetName = targetName; + this.timestamp = System.currentTimeMillis(); + } + + public UUID getPlayerUuid() { + return playerUuid; + } + + public String getPlayerName() { + return playerName; + } + + public TeleportType getTeleportType() { + return teleportType; + } + + public CrossServerLocation getDestination() { + return destination; + } + + public String getTargetName() { + return targetName; + } + + public long getTimestamp() { + return timestamp; + } + + /** + * Check if this request is still valid (not expired). + * Default timeout is 30 seconds. + */ + public boolean isValid() { + return System.currentTimeMillis() - timestamp < 30000; + } +} + diff --git a/API/src/main/java/fr/maxlego08/essentials/api/teleport/CrossServerLocation.java b/API/src/main/java/fr/maxlego08/essentials/api/teleport/CrossServerLocation.java new file mode 100644 index 00000000..a91132b3 --- /dev/null +++ b/API/src/main/java/fr/maxlego08/essentials/api/teleport/CrossServerLocation.java @@ -0,0 +1,88 @@ +package fr.maxlego08.essentials.api.teleport; + +import fr.maxlego08.essentials.api.utils.SafeLocation; + +/** + * Represents a location that can span across multiple servers. + * Used for cross-server teleportation via Redis/BungeeCord. + */ +public class CrossServerLocation { + + private final String serverName; + private final String world; + private final double x; + private final double y; + private final double z; + private final float yaw; + private final float pitch; + + public CrossServerLocation(String serverName, String world, double x, double y, double z, float yaw, float pitch) { + this.serverName = serverName; + this.world = world; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + } + + public CrossServerLocation(String serverName, SafeLocation location) { + this(serverName, location.getWorld(), location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + } + + public String getServerName() { + return serverName; + } + + public String getWorld() { + return world; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public float getYaw() { + return yaw; + } + + public float getPitch() { + return pitch; + } + + public SafeLocation toSafeLocation() { + return new SafeLocation(world, x, y, z, yaw, pitch); + } + + /** + * Checks if this location is on the same server. + * + * @param currentServer The current server name + * @return true if the location is on the same server + */ + public boolean isSameServer(String currentServer) { + return serverName == null || serverName.isEmpty() || serverName.equalsIgnoreCase(currentServer); + } + + @Override + public String toString() { + return "CrossServerLocation{" + + "serverName='" + serverName + '\'' + + ", world='" + world + '\'' + + ", x=" + x + + ", y=" + y + + ", z=" + z + + ", yaw=" + yaw + + ", pitch=" + pitch + + '}'; + } +} + diff --git a/API/src/main/java/fr/maxlego08/essentials/api/teleport/TeleportType.java b/API/src/main/java/fr/maxlego08/essentials/api/teleport/TeleportType.java new file mode 100644 index 00000000..66404e9c --- /dev/null +++ b/API/src/main/java/fr/maxlego08/essentials/api/teleport/TeleportType.java @@ -0,0 +1,16 @@ +package fr.maxlego08.essentials.api.teleport; + +/** + * Types of cross-server teleportation. + */ +public enum TeleportType { + + WARP, + SPAWN, + HOME, + TPA, + TPA_HERE, + BACK, + PLAYER +} + diff --git a/API/src/main/java/fr/maxlego08/essentials/api/vote/VoteSiteConfiguration.java b/API/src/main/java/fr/maxlego08/essentials/api/vote/VoteSiteConfiguration.java index f6e8ab46..a43fa5a5 100644 --- a/API/src/main/java/fr/maxlego08/essentials/api/vote/VoteSiteConfiguration.java +++ b/API/src/main/java/fr/maxlego08/essentials/api/vote/VoteSiteConfiguration.java @@ -2,5 +2,5 @@ import fr.maxlego08.essentials.api.modules.Loadable; -public record VoteSiteConfiguration(String name, int seconds) implements Loadable { +public record VoteSiteConfiguration(String name, String url, long seconds) implements Loadable { } diff --git a/Hooks/Redis/src/main/java/fr/maxlego08/essentials/hooks/redis/RedisServer.java b/Hooks/Redis/src/main/java/fr/maxlego08/essentials/hooks/redis/RedisServer.java index 8149db9e..8252882f 100644 --- a/Hooks/Redis/src/main/java/fr/maxlego08/essentials/hooks/redis/RedisServer.java +++ b/Hooks/Redis/src/main/java/fr/maxlego08/essentials/hooks/redis/RedisServer.java @@ -10,11 +10,14 @@ import fr.maxlego08.essentials.api.server.messages.ChatClear; import fr.maxlego08.essentials.api.server.messages.ChatToggle; import fr.maxlego08.essentials.api.server.messages.ClearCooldown; +import fr.maxlego08.essentials.api.server.messages.CrossServerTeleport; import fr.maxlego08.essentials.api.server.messages.KickMessage; import fr.maxlego08.essentials.api.server.messages.ServerMessage; import fr.maxlego08.essentials.api.server.messages.ServerPrivateMessage; import fr.maxlego08.essentials.api.server.messages.UpdateCooldown; import fr.maxlego08.essentials.api.storage.IStorage; +import fr.maxlego08.essentials.api.teleport.CrossServerLocation; +import fr.maxlego08.essentials.api.teleport.TeleportType; import fr.maxlego08.essentials.api.user.Option; import fr.maxlego08.essentials.api.user.PrivateMessage; import fr.maxlego08.essentials.api.user.User; @@ -23,6 +26,7 @@ import fr.maxlego08.essentials.hooks.redis.listener.ChatClearListener; import fr.maxlego08.essentials.hooks.redis.listener.ChatToggleListener; import fr.maxlego08.essentials.hooks.redis.listener.ClearCooldownListener; +import fr.maxlego08.essentials.hooks.redis.listener.CrossServerTeleportListener; import fr.maxlego08.essentials.hooks.redis.listener.KickListener; import fr.maxlego08.essentials.hooks.redis.listener.MessageListener; import fr.maxlego08.essentials.hooks.redis.listener.PrivateMessageListener; @@ -41,10 +45,15 @@ import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.Protocol; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -56,7 +65,9 @@ public class RedisServer implements EssentialsServer, Listener { private final EssentialsPlugin plugin; private final EssentialsUtils utils; private final String playersKey = "essentials:playerlist"; + private final String playerServerKey = "essentials:playerserver:"; private final ExpiringCache onlineCache = new ExpiringCache<>(1000 * 30); + private final ExpiringCache playerServerCache = new ExpiringCache<>(1000 * 10); private JedisPool jedisPool; public RedisServer(EssentialsPlugin plugin) { @@ -100,6 +111,7 @@ private void registerListener() { this.redisSubscriberRunnable.registerListener(ServerPrivateMessage.class, new PrivateMessageListener(this.plugin)); this.redisSubscriberRunnable.registerListener(ClearCooldown.class, new ClearCooldownListener(this.utils)); this.redisSubscriberRunnable.registerListener(UpdateCooldown.class, new UpdateCooldownListener(this.utils)); + this.redisSubscriberRunnable.registerListener(CrossServerTeleport.class, new CrossServerTeleportListener(this.plugin)); } @Override @@ -252,16 +264,28 @@ public PlayerCache getPlayerCache() { @EventHandler public void onJoin(PlayerJoinEvent event) { - String playerName = event.getPlayer().getName(); + Player player = event.getPlayer(); + String playerName = player.getName(); + String serverName = getServerName(); + this.playerCache.addPlayer(playerName); - execute(jedis -> jedis.sadd(this.playersKey, playerName)); + execute(jedis -> { + jedis.sadd(this.playersKey, playerName); + jedis.setex(this.playerServerKey + playerName.toLowerCase(), 300, serverName); + }); + + // Handle pending cross-server teleports + CrossServerTeleportListener.onPlayerJoin(this.plugin, player); } @EventHandler public void onQuit(PlayerQuitEvent event) { String playerName = event.getPlayer().getName(); this.playerCache.removePlayer(playerName); - execute(jedis -> jedis.srem(this.playersKey, playerName)); + execute(jedis -> { + jedis.srem(this.playersKey, playerName); + jedis.del(this.playerServerKey + playerName.toLowerCase()); + }); } private void execute(Consumer consumer) { @@ -281,4 +305,110 @@ private void execute(Consumer consumer, boolean async) { if (async) this.plugin.getScheduler().runAsync(wrappedTask -> runnable.run()); else runnable.run(); } + + @Override + public void sendToServer(Player player, String serverName) { + try { + ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(byteArray); + out.writeUTF("Connect"); + out.writeUTF(serverName); + player.sendPluginMessage(this.plugin, "BungeeCord", byteArray.toByteArray()); + } catch (IOException exception) { + this.plugin.getLogger().severe("Failed to send player to server: " + exception.getMessage()); + } + } + + @Override + public void crossServerTeleport(Player player, TeleportType teleportType, CrossServerLocation destination) { + String currentServer = getServerName(); + + if (destination.isSameServer(currentServer)) { + // Same server, teleport locally + User user = this.plugin.getUser(player.getUniqueId()); + if (user != null) { + user.teleport(destination.toSafeLocation().getLocation()); + } + return; + } + + // Different server - send teleport request via Redis then transfer player + CrossServerTeleport teleportRequest = new CrossServerTeleport( + player.getUniqueId(), + player.getName(), + teleportType, + destination + ); + + sendMessage(teleportRequest); + + // Send player to target server + this.utils.message(player, Message.TELEPORT_CROSS_SERVER_CONNECTING, "%server%", destination.getServerName()); + + plugin.getScheduler().runLater(() -> sendToServer(player, destination.getServerName()), 10L); + } + + @Override + public void crossServerTeleportToPlayer(Player player, TeleportType teleportType, String targetPlayerName, String targetServer) { + if (targetServer == null) { + targetServer = findPlayerServer(targetPlayerName); + } + + if (targetServer == null) { + this.utils.message(player, Message.TELEPORT_CROSS_SERVER_PLAYER_NOT_FOUND, "%player%", targetPlayerName); + return; + } + + String currentServer = getServerName(); + if (targetServer.equalsIgnoreCase(currentServer)) { + // Player is on same server, handle locally + Player targetPlayer = Bukkit.getPlayer(targetPlayerName); + if (targetPlayer != null) { + User user = this.plugin.getUser(player.getUniqueId()); + if (user != null) { + user.teleport(targetPlayer.getLocation()); + } + } + return; + } + + // Cross-server TPA - send request and transfer + CrossServerTeleport teleportRequest = new CrossServerTeleport( + player.getUniqueId(), + player.getName(), + teleportType, + null, + targetPlayerName + ); + + sendMessage(teleportRequest); + + this.utils.message(player, Message.TELEPORT_CROSS_SERVER_CONNECTING, "%server%", targetServer); + + String finalTargetServer = targetServer; + plugin.getScheduler().runLater(() -> sendToServer(player, finalTargetServer), 10L); + } + + @Override + public String getServerName() { + return this.plugin.getConfiguration().getServerName(); + } + + @Override + public String findPlayerServer(String playerName) { + // First check local + Player player = Bukkit.getPlayer(playerName); + if (player != null) { + return getServerName(); + } + + // Check Redis cache + return this.playerServerCache.get(playerName.toLowerCase(), () -> { + try (Jedis jedis = jedisPool.getResource()) { + return jedis.get(this.playerServerKey + playerName.toLowerCase()); + } catch (Exception e) { + return null; + } + }); + } } diff --git a/Hooks/Redis/src/main/java/fr/maxlego08/essentials/hooks/redis/listener/CrossServerTeleportListener.java b/Hooks/Redis/src/main/java/fr/maxlego08/essentials/hooks/redis/listener/CrossServerTeleportListener.java new file mode 100644 index 00000000..dbd2861f --- /dev/null +++ b/Hooks/Redis/src/main/java/fr/maxlego08/essentials/hooks/redis/listener/CrossServerTeleportListener.java @@ -0,0 +1,101 @@ +package fr.maxlego08.essentials.hooks.redis.listener; + +import fr.maxlego08.essentials.api.EssentialsPlugin; +import fr.maxlego08.essentials.api.server.messages.CrossServerTeleport; +import fr.maxlego08.essentials.api.storage.IStorage; +import fr.maxlego08.essentials.api.teleport.CrossServerLocation; +import fr.maxlego08.essentials.api.user.User; +import fr.maxlego08.essentials.hooks.redis.RedisListener; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Redis listener for handling cross-server teleport requests. + * When a player connects to this server after a cross-server teleport request, + * they will be teleported to the destination. + */ +public class CrossServerTeleportListener extends RedisListener { + + private final EssentialsPlugin plugin; + // Store pending teleports for players who are connecting + private static final Map pendingTeleports = new ConcurrentHashMap<>(); + + public CrossServerTeleportListener(EssentialsPlugin plugin) { + this.plugin = plugin; + } + + @Override + protected void onMessage(CrossServerTeleport message) { + if (!message.isValid()) { + return; + } + + UUID playerUuid = message.getPlayerUuid(); + Player player = Bukkit.getPlayer(playerUuid); + + if (player != null && player.isOnline()) { + // Player is already on this server, teleport them + performTeleport(player, message); + } else { + // Player is not on this server yet, store for when they connect + pendingTeleports.put(playerUuid, message); + + // Clean up after 30 seconds if player doesn't connect + plugin.getScheduler().runLater(() -> pendingTeleports.remove(playerUuid), 30 * 20L); + } + } + + /** + * Called when a player joins the server. Check if they have a pending teleport. + */ + public static void onPlayerJoin(EssentialsPlugin plugin, Player player) { + CrossServerTeleport pending = pendingTeleports.remove(player.getUniqueId()); + if (pending != null && pending.isValid()) { + // Delay teleport slightly to ensure player is fully loaded + plugin.getScheduler().runLater(() -> performTeleport(player, pending), 10L); + } + } + + private static void performTeleport(Player player, CrossServerTeleport message) { + CrossServerLocation destination = message.getDestination(); + if (destination == null) return; + + World world = Bukkit.getWorld(destination.getWorld()); + if (world == null) { + player.sendMessage("§cWorld not found: " + destination.getWorld()); + return; + } + + Location location = new Location(world, destination.getX(), destination.getY(), destination.getZ(), + destination.getYaw(), destination.getPitch()); + + player.teleportAsync(location).thenAccept(success -> { + if (success) { + player.sendMessage("§aYou have been teleported!"); + } else { + player.sendMessage("§cTeleportation failed!"); + } + }); + } + + /** + * Check if a player has a pending cross-server teleport. + */ + public static boolean hasPendingTeleport(UUID playerUuid) { + return pendingTeleports.containsKey(playerUuid); + } + + /** + * Clear a pending teleport for a player. + */ + public static void clearPendingTeleport(UUID playerUuid) { + pendingTeleports.remove(playerUuid); + } +} + diff --git a/src/main/java/fr/maxlego08/essentials/MainConfiguration.java b/src/main/java/fr/maxlego08/essentials/MainConfiguration.java index 6dc6ef9e..b57c36e9 100644 --- a/src/main/java/fr/maxlego08/essentials/MainConfiguration.java +++ b/src/main/java/fr/maxlego08/essentials/MainConfiguration.java @@ -71,6 +71,8 @@ public class MainConfiguration extends YamlLoader implements Configuration { private List blacklistUuids; private List flyTaskAnnounce; private String placeholderEmpty; + private String serverName = "default"; + private boolean crossServerTeleportEnabled; public MainConfiguration(ZEssentialsPlugin plugin) { this.plugin = plugin; @@ -93,10 +95,30 @@ public List getCommandCooldown() { @Override public Optional getCooldown(Permissible permissible, String command) { - return this.commandCooldowns.stream().filter(commandCooldown -> commandCooldown.command().equalsIgnoreCase(command)).map(commandCooldown -> { - List> permissions = commandCooldown.permissions() == null ? new ArrayList<>() : commandCooldown.permissions(); - return permissions.stream().filter(e -> permissible.hasPermission((String) e.get("permission"))).mapToInt(e -> ((Number) e.get("cooldown")).intValue()).min().orElse(commandCooldown.cooldown()); - }).findFirst(); + // Optimized: Replace nested streams with for-loops + for (int i = 0, size = this.commandCooldowns.size(); i < size; i++) { + CommandCooldown commandCooldown = this.commandCooldowns.get(i); + if (commandCooldown.command().equalsIgnoreCase(command)) { + List> permissions = commandCooldown.permissions(); + if (permissions == null || permissions.isEmpty()) { + return Optional.of(commandCooldown.cooldown()); + } + + int minCooldown = commandCooldown.cooldown(); + for (int j = 0, permSize = permissions.size(); j < permSize; j++) { + Map perm = permissions.get(j); + String permission = (String) perm.get("permission"); + if (permissible.hasPermission(permission)) { + int cooldown = ((Number) perm.get("cooldown")).intValue(); + if (cooldown < minCooldown) { + minCooldown = cooldown; + } + } + } + return Optional.of(minCooldown); + } + } + return Optional.empty(); } @Override @@ -350,4 +372,13 @@ public List getCommandRestrictions() { return this.commandRestrictions; } + @Override + public String getServerName() { + return this.serverName; + } + + @Override + public boolean isCrossServerTeleportEnabled() { + return this.crossServerTeleportEnabled; + } } diff --git a/src/main/java/fr/maxlego08/essentials/ZEssentialsPlugin.java b/src/main/java/fr/maxlego08/essentials/ZEssentialsPlugin.java index 29dfdcf8..c9c9b591 100644 --- a/src/main/java/fr/maxlego08/essentials/ZEssentialsPlugin.java +++ b/src/main/java/fr/maxlego08/essentials/ZEssentialsPlugin.java @@ -26,6 +26,7 @@ import fr.maxlego08.essentials.api.sanction.SanctionManager; import fr.maxlego08.essentials.api.scoreboard.ScoreboardManager; import fr.maxlego08.essentials.api.server.EssentialsServer; +import fr.maxlego08.essentials.api.server.ServerType; import fr.maxlego08.essentials.api.steps.StepManager; import fr.maxlego08.essentials.api.storage.Persist; import fr.maxlego08.essentials.api.storage.ServerStorage; @@ -228,14 +229,19 @@ public void onEnable() { this.getLogger().info("Create " + this.commandManager.countCommands() + " commands."); - // Essentials Server - /*if (this.configuration.getServerType() == ServerType.REDIS) { - this.essentialsServer = new RedisServer(this); - this.getLogger().info("Using Redis server."); - }*/ + // Essentials Server - Load Redis server if configured + if (this.configuration.getServerType() == ServerType.REDIS) { + this.createRedisServer().ifPresent(server -> { + this.essentialsServer = server; + this.getLogger().info("Using Redis server for cross-server communication."); + }); + } this.essentialsServer.onEnable(); + // Register BungeeCord messaging channel for cross-server teleportation + this.getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord"); + // Storage this.storageManager = new ZStorageManager(this); this.registerListener(this.storageManager); @@ -702,6 +708,25 @@ public Optional createInstance(String className) { return Optional.empty(); } + /** + * Creates a Redis server instance using reflection. + * This allows loading the Redis module without a direct dependency. + */ + private Optional createRedisServer() { + try { + Class clazz = Class.forName("fr.maxlego08.essentials.hooks.redis.RedisServer"); + Constructor constructor = clazz.getConstructor(EssentialsPlugin.class); + return Optional.of((EssentialsServer) constructor.newInstance(this)); + } catch (ClassNotFoundException exception) { + this.getLogger().severe("Redis module not found! Make sure the Redis hook is included."); + this.getLogger().severe("Falling back to Paper/Spigot server."); + } catch (Exception exception) { + this.getLogger().severe("Failed to create Redis server instance: " + exception.getMessage()); + exception.printStackTrace(); + } + return Optional.empty(); + } + @Override public VoteManager getVoteManager() { return this.moduleManager.getModule(VoteModule.class); diff --git a/src/main/java/fr/maxlego08/essentials/commands/CommandLoader.java b/src/main/java/fr/maxlego08/essentials/commands/CommandLoader.java index e3ba4f0e..4906a37e 100644 --- a/src/main/java/fr/maxlego08/essentials/commands/CommandLoader.java +++ b/src/main/java/fr/maxlego08/essentials/commands/CommandLoader.java @@ -120,6 +120,7 @@ import fr.maxlego08.essentials.commands.commands.utils.blocks.CommandSmithingTable; import fr.maxlego08.essentials.commands.commands.utils.blocks.CommandStoneCutter; import fr.maxlego08.essentials.commands.commands.utils.experience.CommandExperience; +import fr.maxlego08.essentials.commands.commands.trade.CommandTrade; import fr.maxlego08.essentials.commands.commands.vault.CommandVault; import fr.maxlego08.essentials.commands.commands.vote.CommandVote; import fr.maxlego08.essentials.commands.commands.vote.CommandVoteParty; @@ -291,6 +292,7 @@ public void loadCommands(CommandManager commandManager) { register("voteparty", CommandVoteParty.class, "vp"); register("vote", CommandVote.class); + register("trade", CommandTrade.class, "exchange"); register("vault", CommandVault.class, "sac", "bag", "b", "coffre", "chest"); register("player-worldedit", CommandWorldEdit.class, "pwe", "ess-worldedit", "eworldedit", "ew"); diff --git a/src/main/java/fr/maxlego08/essentials/commands/ZCommandManager.java b/src/main/java/fr/maxlego08/essentials/commands/ZCommandManager.java index ac83c46b..053ca334 100644 --- a/src/main/java/fr/maxlego08/essentials/commands/ZCommandManager.java +++ b/src/main/java/fr/maxlego08/essentials/commands/ZCommandManager.java @@ -166,7 +166,9 @@ public List processTab(CommandSender sender, EssentialsCommand command, } else if (type.equals(CommandResultType.SUCCESS)) { var list = command.toTab(this.plugin, sender, args); - return list == null ? List.of() : list.stream().limit(100).toList(); + if (list == null || list.isEmpty()) return List.of(); + // Limit to 100 without stream + return list.size() <= 100 ? list : list.subList(0, 100); } return List.of(); diff --git a/src/main/java/fr/maxlego08/essentials/commands/commands/economy/CommandEconomyResetAll.java b/src/main/java/fr/maxlego08/essentials/commands/commands/economy/CommandEconomyResetAll.java index 9966a088..efe0f566 100644 --- a/src/main/java/fr/maxlego08/essentials/commands/commands/economy/CommandEconomyResetAll.java +++ b/src/main/java/fr/maxlego08/essentials/commands/commands/economy/CommandEconomyResetAll.java @@ -19,7 +19,8 @@ public class CommandEconomyResetAll extends VCommand { private static final long CONFIRMATION_DURATION = TimeUnit.SECONDS.toMillis(30); - private static final Map CONFIRMATIONS = new HashMap<>(); + // Thread-safe confirmation map + private static final Map CONFIRMATIONS = new java.util.concurrent.ConcurrentHashMap<>(); public CommandEconomyResetAll(EssentialsPlugin plugin) { super(plugin); diff --git a/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTrade.java b/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTrade.java new file mode 100644 index 00000000..dbec270b --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTrade.java @@ -0,0 +1,45 @@ +package fr.maxlego08.essentials.commands.commands.trade; + +import fr.maxlego08.essentials.api.EssentialsPlugin; +import fr.maxlego08.essentials.api.commands.CommandResultType; +import fr.maxlego08.essentials.api.commands.Permission; +import fr.maxlego08.essentials.api.messages.Message; +import fr.maxlego08.essentials.module.modules.trade.TradeModule; +import fr.maxlego08.essentials.zutils.utils.commands.VCommand; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class CommandTrade extends VCommand { + + public CommandTrade(EssentialsPlugin plugin) { + super(plugin); + this.setModule(TradeModule.class); + this.setPermission(Permission.ESSENTIALS_TRADE_USE); + this.setDescription(Message.DESCRIPTION_TRADE); + + this.addSubCommand(new CommandTradeAccept(plugin)); + this.addSubCommand(new CommandTradeDeny(plugin)); + } + + @Override + protected CommandResultType perform(EssentialsPlugin plugin) { + if (args.length == 0) { + syntaxMessage(); + return CommandResultType.SYNTAX_ERROR; + } + + String targetName = args[0]; + Player target = Bukkit.getPlayer(targetName); + + if (target == null) { + message(sender, Message.PLAYER_NOT_FOUND, "%player%", targetName); + return CommandResultType.DEFAULT; + } + + TradeModule module = plugin.getModuleManager().getModule(TradeModule.class); + module.getTradeManager().sendRequest(player, target); + + return CommandResultType.SUCCESS; + } +} + diff --git a/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTradeAccept.java b/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTradeAccept.java new file mode 100644 index 00000000..6b2095b7 --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTradeAccept.java @@ -0,0 +1,55 @@ +package fr.maxlego08.essentials.commands.commands.trade; + +import fr.maxlego08.essentials.api.EssentialsPlugin; +import fr.maxlego08.essentials.api.commands.CommandResultType; +import fr.maxlego08.essentials.api.commands.Permission; +import fr.maxlego08.essentials.api.messages.Message; +import fr.maxlego08.essentials.module.modules.trade.TradeManager; +import fr.maxlego08.essentials.module.modules.trade.TradeModule; +import fr.maxlego08.essentials.zutils.utils.commands.VCommand; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; + +public class CommandTradeAccept extends VCommand { + + public CommandTradeAccept(EssentialsPlugin plugin) { + super(plugin); + this.addSubCommand("accept"); + this.setPermission(Permission.ESSENTIALS_TRADE_ACCEPT); + this.setDescription(Message.DESCRIPTION_TRADE); + } + + @Override + protected CommandResultType perform(EssentialsPlugin plugin) { + TradeModule module = plugin.getModuleManager().getModule(TradeModule.class); + TradeManager manager = module.getTradeManager(); + + if (args.length == 0) { + Map requests = manager.getRequests(); + UUID senderUUID = requests.get(player.getUniqueId()); + if (senderUUID != null) { + Player senderPlayer = Bukkit.getPlayer(senderUUID); + if (senderPlayer != null) { + manager.acceptRequest(senderPlayer, player); + return CommandResultType.SUCCESS; + } + } + message(sender, Message.COMMAND_NO_ARG); + return CommandResultType.SYNTAX_ERROR; + } + + String targetName = args[0]; + Player target = Bukkit.getPlayer(targetName); + if (target == null) { + message(sender, Message.PLAYER_NOT_FOUND, "%player%", targetName); + return CommandResultType.DEFAULT; + } + + manager.acceptRequest(target, player); + return CommandResultType.SUCCESS; + } +} + diff --git a/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTradeDeny.java b/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTradeDeny.java new file mode 100644 index 00000000..fca498fa --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/commands/commands/trade/CommandTradeDeny.java @@ -0,0 +1,55 @@ +package fr.maxlego08.essentials.commands.commands.trade; + +import fr.maxlego08.essentials.api.EssentialsPlugin; +import fr.maxlego08.essentials.api.commands.CommandResultType; +import fr.maxlego08.essentials.api.commands.Permission; +import fr.maxlego08.essentials.api.messages.Message; +import fr.maxlego08.essentials.module.modules.trade.TradeManager; +import fr.maxlego08.essentials.module.modules.trade.TradeModule; +import fr.maxlego08.essentials.zutils.utils.commands.VCommand; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; + +public class CommandTradeDeny extends VCommand { + + public CommandTradeDeny(EssentialsPlugin plugin) { + super(plugin); + this.addSubCommand("deny"); + this.setPermission(Permission.ESSENTIALS_TRADE_DENY); + this.setDescription(Message.DESCRIPTION_TRADE); + } + + @Override + protected CommandResultType perform(EssentialsPlugin plugin) { + TradeModule module = plugin.getModuleManager().getModule(TradeModule.class); + TradeManager manager = module.getTradeManager(); + + if (args.length == 0) { + Map requests = manager.getRequests(); + UUID senderUUID = requests.get(player.getUniqueId()); + if (senderUUID != null) { + Player senderPlayer = Bukkit.getPlayer(senderUUID); + if (senderPlayer != null) { + manager.denyRequest(senderPlayer, player); + return CommandResultType.SUCCESS; + } + } + message(sender, Message.COMMAND_NO_ARG); + return CommandResultType.SYNTAX_ERROR; + } + + String targetName = args[0]; + Player target = Bukkit.getPlayer(targetName); + if (target == null) { + message(sender, Message.PLAYER_NOT_FOUND, "%player%", targetName); + return CommandResultType.DEFAULT; + } + + manager.denyRequest(target, player); + return CommandResultType.SUCCESS; + } +} + diff --git a/src/main/java/fr/maxlego08/essentials/listener/PlayerListener.java b/src/main/java/fr/maxlego08/essentials/listener/PlayerListener.java index d0eee64d..5472bd90 100644 --- a/src/main/java/fr/maxlego08/essentials/listener/PlayerListener.java +++ b/src/main/java/fr/maxlego08/essentials/listener/PlayerListener.java @@ -40,6 +40,7 @@ import org.bukkit.potion.PotionEffectType; import java.util.Optional; +import java.util.UUID; public class PlayerListener extends ZUtils implements Listener { @@ -54,7 +55,7 @@ private User getUser(Entity player) { return this.plugin.getStorageManager().getStorage().getUser(player.getUniqueId()); } - private void cancelGoldEvent(Player player, Cancellable event) { + private void cancelGodEvent(Player player, Cancellable event) { User user = getUser(player); if (user != null && user.getOption(Option.GOD)) { event.setCancelled(true); @@ -64,7 +65,7 @@ private void cancelGoldEvent(Player player, Cancellable event) { @EventHandler(priority = EventPriority.HIGHEST) public void onDamage(EntityDamageEvent event) { if (event.getEntity() instanceof Player player) { - cancelGoldEvent(player, event); + cancelGodEvent(player, event); var user = getUser(player); if (user == null) return; @@ -78,7 +79,7 @@ public void onDamage(EntityDamageEvent event) { @EventHandler(priority = EventPriority.HIGHEST) public void onFood(FoodLevelChangeEvent event) { if (event.getEntity() instanceof Player player) { - cancelGoldEvent(player, event); + cancelGodEvent(player, event); } } @@ -121,7 +122,14 @@ public void onCommand(PlayerCommandPreprocessEvent event) { Configuration configuration = this.plugin.getConfiguration(); Player player = event.getPlayer(); - String label = event.getMessage().substring(1).split(" ")[0]; + // Safe command extraction with bounds checking + String message = event.getMessage(); + if (message.length() <= 1) return; // Empty command, skip + + String[] parts = message.substring(1).split(" "); + if (parts.length == 0) return; // No command parts, skip + + String label = parts[0]; for (var restriction : configuration.getCommandRestrictions()) { if (restriction.commands().contains(label)) { String bypass = restriction.bypassPermission(); @@ -190,7 +198,12 @@ public void onJoin(PlayerJoinEvent event) { if (user != null) user.startCurrentSessionPlayTime(); if (user != null && user.isFirstJoin() && ConfigStorage.spawnLocation != null && ConfigStorage.spawnLocation.isValid()) { - this.plugin.getScheduler().teleportAsync(player, ConfigStorage.spawnLocation.getLocation()); + // Wait for the player to be fully loaded in the chunk loader for Folia + this.plugin.getScheduler().runAtLocationLater(player.getLocation(), () -> { + if (player.isOnline()) { + this.plugin.getScheduler().teleportAsync(player, ConfigStorage.spawnLocation.getLocation()); + } + }, 2); } if (user != null && user.getOption(Option.VANISH)) { @@ -212,6 +225,8 @@ public void onJoin(PlayerJoinEvent event) { this.plugin.getScheduler().runAtLocationLater(player.getLocation(), () -> { + if (!player.isOnline()) return; + if (hasPermission(player, Permission.ESSENTIALS_FLY_SAFELOGIN) && shouldFlyBasedOnLocation(player.getLocation())) { player.setAllowFlight(true); player.setFlying(true); @@ -230,7 +245,12 @@ public void onQuit(PlayerQuitEvent event) { if (user == null) return; long sessionPlayTime = (System.currentTimeMillis() - user.getCurrentSessionPlayTime()) / 1000; long playtime = user.getPlayTime(); - this.plugin.getStorageManager().getStorage().insertPlayTime(user.getUniqueId(), sessionPlayTime, playtime, user.getAddress()); + String address = user.getAddress(); + UUID uuid = user.getUniqueId(); + + this.plugin.getScheduler().runAsync(wrappedTask -> { + this.plugin.getStorageManager().getStorage().insertPlayTime(uuid, sessionPlayTime, playtime, address); + }); } @EventHandler diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/SanctionModule.java b/src/main/java/fr/maxlego08/essentials/module/modules/SanctionModule.java index f0578e93..0c3b6cac 100644 --- a/src/main/java/fr/maxlego08/essentials/module/modules/SanctionModule.java +++ b/src/main/java/fr/maxlego08/essentials/module/modules/SanctionModule.java @@ -418,7 +418,10 @@ public void freeze(CommandSender sender, UUID uuid, String userName) { player.setAllowFlight(true); player.setFlying(true); player.setFlySpeed(0f); - this.plugin.getScheduler().teleportAsync(user.getPlayer(), user.getPlayer().getLocation().add(0, 0.1, 0)); + // Null check for player safety + if (user.getPlayer() != null) { + this.plugin.getScheduler().teleportAsync(user.getPlayer(), user.getPlayer().getLocation().add(0, 0.1, 0)); + } } else { player.setAllowFlight(false); player.setFlying(false); diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/TeleportationModule.java b/src/main/java/fr/maxlego08/essentials/module/modules/TeleportationModule.java index 3de0d122..ddd9f017 100644 --- a/src/main/java/fr/maxlego08/essentials/module/modules/TeleportationModule.java +++ b/src/main/java/fr/maxlego08/essentials/module/modules/TeleportationModule.java @@ -106,11 +106,25 @@ public boolean isOpenConfirmInventoryForTpaHere() { } public int getTeleportDelay(Player player) { - return this.teleportDelayPermissions.stream().filter(teleportPermission -> player.hasPermission(teleportPermission.permission())).mapToInt(TeleportPermission::delay).min().orElse(this.teleportDelay); + int minDelay = this.teleportDelay; + for (int i = 0, size = this.teleportDelayPermissions.size(); i < size; i++) { + TeleportPermission perm = this.teleportDelayPermissions.get(i); + if (player.hasPermission(perm.permission()) && perm.delay() < minDelay) { + minDelay = perm.delay(); + } + } + return minDelay; } public int getTeleportProtectionDelay(Player player) { - return this.teleportProtections.stream().filter(teleportPermission -> player.hasPermission(teleportPermission.permission())).mapToInt(TeleportPermission::delay).min().orElse(this.teleportProtection); + int minDelay = this.teleportProtection; + for (int i = 0, size = this.teleportProtections.size(); i < size; i++) { + TeleportPermission perm = this.teleportProtections.get(i); + if (player.hasPermission(perm.permission()) && perm.delay() < minDelay) { + minDelay = perm.delay(); + } + } + return minDelay; } public void openConfirmInventory(Player player) { @@ -124,6 +138,7 @@ public void openConfirmHereInventory(Player player) { public void randomTeleport(Player player, World world) { // Check for world override String worldName = world.getName(); + if (rtpWorldOverrides.containsKey(worldName)) { String overrideWorld = rtpWorldOverrides.get(worldName); World targetWorld = plugin.getServer().getWorld(overrideWorld); @@ -153,38 +168,29 @@ public void randomTeleport(Player player, World world, int centerX, int centerZ, } private void performRandomTeleport(Player player, World world, int centerX, int centerZ, int rangeX, int rangeZ) { - this.debug("Starting random teleport for player " + player.getName()); message(player, Message.TELEPORT_RANDOM_START); getRandomSurfaceLocation(world, centerX, centerZ, rangeX, rangeZ, this.maxRtpAttempts).thenAccept(randomLocation -> { - this.debug("Random location found: " + randomLocation); if (randomLocation != null) { User user = this.getUser(player); user.teleport(randomLocation, Message.TELEPORT_MESSAGE_RANDOM, Message.TELEPORT_SUCCESS_RANDOM); } else { - this.debug("Failed to find random location"); message(player, Message.COMMAND_RANDOM_TP_ERROR); } }); } private CompletableFuture getRandomSurfaceLocation(World world, int centerX, int centerZ, int rangeX, int rangeZ, int attempts) { - this.debug("Starting random surface location search for world " + world.getName()); CompletableFuture future = new CompletableFuture<>(); if (attempts > 0) { - randomLocation(world, centerX, centerZ, rangeX, rangeZ).thenAccept(location -> { - this.debug("Random location generated: " + location); if (isValidLocation(location)) { future.complete(location); } else { - this.debug("Random location not valid"); getRandomSurfaceLocation(world, centerX, centerZ, rangeX, rangeZ, attempts - 1).thenAccept(future::complete); } }); - } else { - this.debug("Failed to find random surface location, using default location"); future.complete(null); } @@ -192,7 +198,6 @@ private CompletableFuture getRandomSurfaceLocation(World world, int ce } private CompletableFuture randomLocation(World world, int centerX, int centerZ, int rangeX, int rangeZ) { - this.debug("Generating random location for world " + world.getName()); CompletableFuture future = new CompletableFuture<>(); int x = centerX + (int) (Math.random() * (2 * rangeX + 1)) - rangeX; @@ -202,7 +207,6 @@ private CompletableFuture randomLocation(World world, int centerX, int this.plugin.getScheduler().runAtLocation(new Location(world, x, 0, z), wrappedTask -> { int y = findSafeY(world, x, z); Location location = new Location(world, x + 0.5, y, z + 0.5, 360 * random.nextFloat() - 180, 0); - this.debug("Final location determined: " + location); future.complete(location); }); }); @@ -215,23 +219,79 @@ private int findSafeY(World world, int x, int z) { return getNetherYAt(new Location(world, x, 0, z)); } - // Start from top and work down to find first solid block + int seaLevel = world.getSeaLevel(); int maxY = world.getMaxHeight() - 1; int minY = world.getMinHeight(); - for (int y = maxY; y >= minY; y--) { + this.debug("Finding safe Y for coordinates (" + x + ", " + z + ") - SeaLevel: " + seaLevel + ", MinY: " + minY + ", MaxY: " + maxY); + + // Force chunk generation first + try { + world.getChunkAt(x >> 4, z >> 4); + } catch (Exception e) { + this.plugin.getLogger().warning("Failed to load chunk at " + (x >> 4) + ", " + (z >> 4) + ": " + e.getMessage()); + } + + // For void/empty worlds, try to find any solid surface + if (seaLevel < 0) { + this.debug("Detected void world (sea level < 0), using special algorithm"); + + // Try from bedrock level up + for (int y = minY + 1; y <= maxY - 2; y++) { + Material blockType = world.getBlockAt(x, y, z).getType(); + Material aboveType = world.getBlockAt(x, y + 1, z).getType(); + Material above2Type = world.getBlockAt(x, y + 2, z).getType(); + + if (blockType.isSolid() && + blockType != Material.WATER && blockType != Material.LAVA && + blockType != Material.BEDROCK && + aboveType.isAir() && above2Type.isAir()) { + this.debug("Found void world surface at Y=" + (y + 1) + " (solid: " + blockType + ")"); + return y + 1; + } + } + + // If no surface found in void world, create a safe spot at Y=70 + this.debug("No surface in void world, using Y=70"); + return 70; + } + + // Normal world - try from sea level up + for (int y = Math.max(seaLevel, 1); y <= maxY - 2; y++) { Material blockType = world.getBlockAt(x, y, z).getType(); + Material aboveType = world.getBlockAt(x, y + 1, z).getType(); + Material above2Type = world.getBlockAt(x, y + 2, z).getType(); + // Skip water and lava if (blockType == Material.WATER || blockType == Material.LAVA) { continue; } - if (blockType.isSolid()) { - return y + 1; // Return the block above the solid block + + // Found solid ground with air above + if (blockType.isSolid() && aboveType.isAir() && above2Type.isAir()) { + this.debug("Found surface at Y=" + (y + 1) + " (solid: " + blockType + ", above: " + aboveType + ")"); + return y + 1; + } + } + + // Fallback: Work down from max height + for (int y = maxY - 2; y >= Math.max(minY + 1, 1); y--) { + Material blockType = world.getBlockAt(x, y, z).getType(); + Material aboveType = world.getBlockAt(x, y + 1, z).getType(); + Material above2Type = world.getBlockAt(x, y + 2, z).getType(); + + if (blockType.isSolid() && + blockType != Material.WATER && blockType != Material.LAVA && + aboveType.isAir() && above2Type.isAir()) { + this.debug("Found fallback surface at Y=" + (y + 1) + " (solid: " + blockType + ")"); + return y + 1; } } - // If no solid block found, return sea level - return world.getSeaLevel(); + // Ultimate fallback - use a safe Y level + int safeY = Math.max(seaLevel + 1, 70); + this.debug("No safe Y found, using safe fallback: " + safeY); + return safeY; } private boolean isValidLocation(Location location) { @@ -253,19 +313,21 @@ private boolean isValidLocation(Location location) { Material atType = at.getBlock().getType(); Material aboveType = above.getBlock().getType(); - // Make sure we're not spawning in water or lava - boolean isValid = belowType.isSolid() + // Special handling for void worlds (when all blocks are air) + boolean isVoidWorld = location.getWorld().getSeaLevel() < 0; + if (isVoidWorld && atType.isAir() && aboveType.isAir()) { + // In void worlds, accept locations with air below too, but place a block + // Place a stone block below the teleport location for safety + below.getBlock().setType(Material.STONE); + return true; + } + + // Normal validation: solid block below, air at player position and above + return belowType.isSolid() && belowType != Material.WATER && belowType != Material.LAVA && !atType.isSolid() && atType.isAir() && aboveType.isAir(); - - this.debug("Location validation: " + location + " -> " + isValid); - this.debug(" Below: " + below.getBlock().getType() + " (solid: " + below.getBlock().getType().isSolid() + ")"); - this.debug(" At: " + at.getBlock().getType() + " (air: " + at.getBlock().getType().isAir() + ")"); - this.debug(" Above: " + above.getBlock().getType() + " (air: " + above.getBlock().getType().isAir() + ")"); - - return isValid; } private int getNetherYAt(final Location location) { @@ -283,9 +345,8 @@ private boolean isBlockUnsafe(World world, int x, int y, int z) { } private void debug(String message) { - if (this.enableRandomTeleportSearchLogMessage) { - this.plugin.getLogger().info(message); - } + // Debug logging disabled for performance + // Enable "enable-random-teleport-search-log-message: true" in config for debugging } // Queue System Methods diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/VoteModule.java b/src/main/java/fr/maxlego08/essentials/module/modules/VoteModule.java index d7efb183..9e7ba142 100644 --- a/src/main/java/fr/maxlego08/essentials/module/modules/VoteModule.java +++ b/src/main/java/fr/maxlego08/essentials/module/modules/VoteModule.java @@ -96,6 +96,30 @@ public void loadConfiguration() { var rewardActions = typedMapAccessor.getStringList("commands"); this.rewardsOnVote.add(new VoteReward(min, max, rewardActions)); } + + this.placeholderAvailable = configuration.getString("placeholders.available", "&aAvailable"); + this.placeholderCooldown = configuration.getString("placeholders.cooldown", "&cCooldown"); + + this.enableVoteParty = configuration.getBoolean("vote-party.enable"); + this.votePartyObjective = configuration.getLong("vote-party.objective"); + this.enableVotePartyOpenVoteInventory = configuration.getBoolean("vote-party.open-inventory"); + this.enableOfflineVoteMessage = configuration.getBoolean("enable-offline-vote-message"); + + this.sites = new ArrayList<>(); + for (Map map : configuration.getMapList("vote-sites")) { + TypedMapAccessor typedMapAccessor = new TypedMapAccessor((Map) map); + String name = typedMapAccessor.getString("name"); + String url = typedMapAccessor.getString("url"); + long delay = typedMapAccessor.getLong("delay"); + this.sites.add(new VoteSiteConfiguration(name, url, delay)); + } + + this.resetConfiguration = new VoteResetConfiguration( + configuration.getInt("reset-votes.day"), + configuration.getInt("reset-votes.hour"), + configuration.getInt("reset-votes.minute"), + configuration.getInt("reset-votes.second") + ); } @Override @@ -188,13 +212,17 @@ private void updateDatabaseFromCacheForPlayer(UUID uniqueId) { if (voteDTO != null) { this.plugin.getScheduler().runAsync(wrappedTask -> { - var dto = storage.getVote(uniqueId); - long vote = voteDTO.vote() + dto.vote(); - User user = this.plugin.getUser(uniqueId); - if (user != null) { - user.setVote(vote); - } else { - storage.setVote(uniqueId, vote, this.enableOfflineVoteMessage ? voteDTO.vote_offline() + dto.vote_offline() : 0); + try { + var dto = storage.getVote(uniqueId); + long vote = voteDTO.vote() + dto.vote(); + User user = this.plugin.getUser(uniqueId); + if (user != null) { + user.setVote(vote); + } else { + storage.setVote(uniqueId, vote, this.enableOfflineVoteMessage ? voteDTO.vote_offline() + dto.vote_offline() : 0); + } + } catch (Exception exception) { + exception.printStackTrace(); } }); } @@ -312,21 +340,59 @@ public String getPlaceholderAvailable() { return placeholderAvailable; } + @NonLoadable + private ScheduledExecutorService resetScheduler; + + @Override + public void onDisable() { + if (this.resetScheduler != null && !this.resetScheduler.isShutdown()) { + this.resetScheduler.shutdown(); + } + super.onDisable(); + } + @Override public void startResetTask() { if (!isEnable()) return; + + if (this.resetScheduler != null && !this.resetScheduler.isShutdown()) { + this.resetScheduler.shutdown(); + } LocalDateTime now = LocalDateTime.now(); - LocalDateTime nextRun = now.withDayOfMonth(range(this.resetConfiguration.day(), 1, 31)).withHour(range(this.resetConfiguration.hour(), 0, 23)).withMinute(range(this.resetConfiguration.minute(), 0, 59)).withSecond(range(this.resetConfiguration.second(), 0, 59)); + LocalDateTime nextRun = now.withHour(range(this.resetConfiguration.hour(), 0, 23)) + .withMinute(range(this.resetConfiguration.minute(), 0, 59)) + .withSecond(range(this.resetConfiguration.second(), 0, 59)); + + // Handle month day adjustment safely + int targetDay = range(this.resetConfiguration.day(), 1, 31); + try { + nextRun = nextRun.withDayOfMonth(Math.min(targetDay, nextRun.toLocalDate().lengthOfMonth())); + } catch (Exception e) { + nextRun = nextRun.withDayOfMonth(nextRun.toLocalDate().lengthOfMonth()); + } if (!now.isBefore(nextRun)) { nextRun = nextRun.plusMonths(1); + // Adjust day again for the next month + try { + nextRun = nextRun.withDayOfMonth(Math.min(targetDay, nextRun.toLocalDate().lengthOfMonth())); + } catch (Exception e) { + nextRun = nextRun.withDayOfMonth(nextRun.toLocalDate().lengthOfMonth()); + } } + long initialDelay = ChronoUnit.MILLIS.between(now, nextRun); - ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - scheduler.scheduleAtFixedRate(this::resetVotes, initialDelay, TimeUnit.DAYS.toMillis(30), TimeUnit.MILLISECONDS); + this.resetScheduler = Executors.newScheduledThreadPool(1); + // Schedule only once, then reschedule inside the task for correct monthly calculation + this.resetScheduler.schedule(this::resetVotesAndReschedule, initialDelay, TimeUnit.MILLISECONDS); + } + + private void resetVotesAndReschedule() { + this.resetVotes(); + this.startResetTask(); // Reschedule for next month } @Override diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/trade/TradeManager.java b/src/main/java/fr/maxlego08/essentials/module/modules/trade/TradeManager.java new file mode 100644 index 00000000..17a3ef3a --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/module/modules/trade/TradeManager.java @@ -0,0 +1,42 @@ +package fr.maxlego08.essentials.module.modules.trade; + +import fr.maxlego08.essentials.ZEssentialsPlugin; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class TradeManager { + + private final ZEssentialsPlugin plugin; + private final TradeModule tradeModule; + private final Map activeTrades = new HashMap<>(); + + public TradeManager(ZEssentialsPlugin plugin, TradeModule tradeModule) { + this.plugin = plugin; + this.tradeModule = tradeModule; + } + + public void cancelAllTrades() { + for (TradeRequest request : activeTrades.values()) { + if (request != null) { + Player player1 = request.getPlayer1(); + Player player2 = request.getPlayer2(); + if (player1 != null && player1.isOnline()) { + player1.closeInventory(); + } + if (player2 != null && player2.isOnline()) { + player2.closeInventory(); + } + } + } + activeTrades.clear(); + } + + public Map getActiveTrades() { + return activeTrades; + } +} + + diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/trade/TradeModule.java b/src/main/java/fr/maxlego08/essentials/module/modules/trade/TradeModule.java new file mode 100644 index 00000000..e5dd267e --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/module/modules/trade/TradeModule.java @@ -0,0 +1,189 @@ +package fr.maxlego08.essentials.module.modules.trade; + +import fr.maxlego08.essentials.ZEssentialsPlugin; +import fr.maxlego08.essentials.module.ZModule; + +import java.util.List; +import java.util.ArrayList; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import fr.maxlego08.essentials.api.cache.SimpleCache; + +public class TradeModule extends ZModule { + + private TradeManager tradeManager; + + private long requestTimeout; + private double maxDistance; + private List worlds; + private List ownSlots = new ArrayList<>(); + private List partnerSlots = new ArrayList<>(); + private final SimpleCache itemCache = new SimpleCache<>(); + + public TradeModule(ZEssentialsPlugin plugin) { + super(plugin, "trade"); + this.tradeManager = new TradeManager(plugin, this); + org.bukkit.Bukkit.getPluginManager().registerEvents(new fr.maxlego08.essentials.module.modules.trade.listeners.TradeInventoryListener(this), plugin); + } + + @Override + public void loadConfiguration() { + super.loadConfiguration(); + var config = getConfiguration(); + this.requestTimeout = config.getLong("request-timeout", 60); + this.maxDistance = config.getDouble("max-distance", 10.0); + this.worlds = config.getStringList("worlds"); + + this.ownSlots = parseSlots(config.getStringList("own-slots")); + this.partnerSlots = parseSlots(config.getStringList("partner-slots")); + this.itemCache.clear(); + } + + private List parseSlots(List slotStrings) { + List slots = new ArrayList<>(); + for (String s : slotStrings) { + try { + if (s.contains("-")) { + String[] parts = s.split("-"); + int start = Integer.parseInt(parts[0]); + int end = Integer.parseInt(parts[1]); + for (int i = start; i <= end; i++) slots.add(i); + } else { + slots.add(Integer.parseInt(s)); + } + } catch (NumberFormatException ignored) {} + } + return slots; + } + + public List getOwnSlots() { + return ownSlots; + } + + public List getPartnerSlots() { + return partnerSlots; + } + + public void sendMessage(Player player, String key, String... replacements) { + String message = getConfiguration().getString("messages." + key); + if (message == null) return; + + for (int i = 0; i < replacements.length; i += 2) { + if (i + 1 < replacements.length) { + message = message.replace(replacements[i], replacements[i + 1]); + } + } + + this.plugin.getComponentMessage().sendMessage(player, message); + } + + public ItemStack getItem(String path, Player player, String... replacements) { + if (replacements.length == 0) { + ItemStack cached = itemCache.get(path, () -> loadRawItem(path)); + if (cached != null) { + ItemStack clone = cached.clone(); + updateItemMeta(clone, player); + return clone; + } + } + + ItemStack item = loadRawItem(path); + updateItemMeta(item, player, replacements); + return item; + } + + private ItemStack loadRawItem(String path) { + var config = getConfiguration(); + var section = config.getConfigurationSection(path); + if (section == null) return new ItemStack(org.bukkit.Material.AIR); + + String materialName = section.getString("material", "STONE"); + org.bukkit.Material material = org.bukkit.Material.matchMaterial(materialName); + if (material == null) material = org.bukkit.Material.STONE; + + ItemStack item = new ItemStack(material); + var meta = item.getItemMeta(); + if (meta != null) { + if (section.contains("custom-model-data")) { + meta.setCustomModelData(section.getInt("custom-model-data")); + } + + String name = section.getString("name"); + if (name != null) { + meta.setDisplayName(name); + } + + List lore = section.getStringList("lore"); + if (!lore.isEmpty()) { + meta.setLore(lore); + } + + item.setItemMeta(meta); + } + return item; + } + + private void updateItemMeta(ItemStack item, Player player, String... replacements) { + var meta = item.getItemMeta(); + if (meta == null) return; + + if (meta.hasDisplayName()) { + String name = meta.getDisplayName(); + for (int i = 0; i < replacements.length; i += 2) { + if (i + 1 < replacements.length) name = name.replace(replacements[i], replacements[i + 1]); + } + if (this.plugin.getComponentMessage() instanceof fr.maxlego08.essentials.zutils.utils.paper.PaperComponent paperComponent) { + paperComponent.updateDisplayName(meta, name, player); + } + } + + if (meta.hasLore()) { + List lore = meta.getLore(); + if (lore != null && !lore.isEmpty()) { + List newLore = new ArrayList<>(); + for (String line : lore) { + for (int i = 0; i < replacements.length; i += 2) { + if (i + 1 < replacements.length) line = line.replace(replacements[i], replacements[i + 1]); + } + newLore.add(line); + } + if (this.plugin.getComponentMessage() instanceof fr.maxlego08.essentials.zutils.utils.paper.PaperComponent paperComponent) { + paperComponent.updateLore(meta, newLore, player); + } + } + } + item.setItemMeta(meta); + } + + public int getOwnConfirmSlot() { + return getConfiguration().getInt("own.confirm-item.slot", 0); + } + + public int getPartnerConfirmSlot() { + return getConfiguration().getInt("partner.confirm-item.slot", 8); + } + + public long getRequestTimeout() { + return requestTimeout; + } + + public double getMaxDistance() { + return maxDistance; + } + + public List getWorlds() { + return worlds; + } + + public TradeManager getTradeManager() { + return tradeManager; + } + + public void onDisable() { + if (this.tradeManager != null) { + this.tradeManager.cancelAllTrades(); + } + } +} + diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/trade/enums/TradeState.java b/src/main/java/fr/maxlego08/essentials/module/modules/trade/enums/TradeState.java new file mode 100644 index 00000000..757c47d4 --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/module/modules/trade/enums/TradeState.java @@ -0,0 +1,9 @@ +package fr.maxlego08.essentials.module.modules.trade.enums; + +public enum TradeState { + WAITING, + READY, + COUNTDOWN, + COMPLETED +} + diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/trade/inventory/TradeInventoryHolder.java b/src/main/java/fr/maxlego08/essentials/module/modules/trade/inventory/TradeInventoryHolder.java new file mode 100644 index 00000000..65533f18 --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/module/modules/trade/inventory/TradeInventoryHolder.java @@ -0,0 +1,104 @@ +package fr.maxlego08.essentials.module.modules.trade.inventory; + +import fr.maxlego08.essentials.module.modules.trade.TradeModule; +import fr.maxlego08.essentials.module.modules.trade.model.TradePlayer; +import fr.maxlego08.essentials.module.modules.trade.model.TradeSession; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import java.util.List; + +public class TradeInventoryHolder implements InventoryHolder { + + private final Player viewer; + private final TradeSession session; + private final TradeModule module; + private final Inventory inventory; + + public TradeInventoryHolder(Player viewer, TradeSession session, TradeModule module) { + this.viewer = viewer; + this.session = session; + this.module = module; + this.inventory = Bukkit.createInventory(this, 54, "Trade with " + session.getOtherTradePlayer(viewer).getPlayer().getName()); + setupInventory(); + } + + public void open() { + viewer.openInventory(inventory); + } + + private void setupInventory() { + var config = module.getConfiguration(); + var decorations = config.getConfigurationSection("decorations"); + if (decorations != null) { + for (String key : decorations.getKeys(false)) { + String path = "decorations." + key; + ItemStack item = module.getItem(path, viewer); + List slots = config.getIntegerList(path + ".slot"); + for (int slot : slots) { + inventory.setItem(slot, item); + } + } + } + + updateButtons(); + updateItems(); + } + + public void updateItems() { + TradePlayer me = session.getTradePlayer(viewer); + TradePlayer other = session.getOtherTradePlayer(viewer); + + List mySlots = module.getOwnSlots(); + List myItems = me.getItems(); + for (int i = 0; i < mySlots.size(); i++) { + int slot = mySlots.get(i); + if (i < myItems.size()) { + inventory.setItem(slot, myItems.get(i)); + } else { + inventory.setItem(slot, null); + } + } + + List otherSlots = module.getPartnerSlots(); + List otherItems = other.getItems(); + for (int i = 0; i < otherSlots.size(); i++) { + int slot = otherSlots.get(i); + if (i < otherItems.size()) { + inventory.setItem(slot, otherItems.get(i)); + } else { + inventory.setItem(slot, null); + } + } + } + + public void updateButtons() { + TradePlayer me = session.getTradePlayer(viewer); + String path = "own.confirm-item." + (me.isReady() ? "cancel" : "accept"); + ItemStack readyBtn = module.getItem(path, viewer); + inventory.setItem(module.getOwnConfirmSlot(), readyBtn); + + TradePlayer other = session.getOtherTradePlayer(viewer); + String otherPath = "partner.confirm-item." + (other.isReady() ? "cancel" : "accept"); + ItemStack otherBtn = module.getItem(otherPath, viewer, "%partner-name%", other.getPlayer().getName()); + inventory.setItem(module.getPartnerConfirmSlot(), otherBtn); + } + + @Override + public Inventory getInventory() { + return inventory; + } + + public TradeSession getSession() { + return session; + } + + public Player getViewer() { + return viewer; + } +} + diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/trade/listeners/TradeInventoryListener.java b/src/main/java/fr/maxlego08/essentials/module/modules/trade/listeners/TradeInventoryListener.java new file mode 100644 index 00000000..3e3f15c7 --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/module/modules/trade/listeners/TradeInventoryListener.java @@ -0,0 +1,28 @@ +package fr.maxlego08.essentials.module.modules.trade.listeners; + +import fr.maxlego08.essentials.module.modules.trade.TradeModule; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; + +public class TradeInventoryListener implements Listener { + + private final TradeModule tradeModule; + + public TradeInventoryListener(TradeModule tradeModule) { + this.tradeModule = tradeModule; + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + // Trade inventory click handling + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + // Trade inventory close handling + } +} + + diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/trade/model/TradePlayer.java b/src/main/java/fr/maxlego08/essentials/module/modules/trade/model/TradePlayer.java new file mode 100644 index 00000000..ca5a3ca1 --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/module/modules/trade/model/TradePlayer.java @@ -0,0 +1,61 @@ +package fr.maxlego08.essentials.module.modules.trade.model; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class TradePlayer { + + private final Player player; + private final List items = new ArrayList<>(); + private double money = 0; + private boolean isReady = false; + private boolean hasConfirmed = false; + + public TradePlayer(Player player) { + this.player = player; + } + + public Player getPlayer() { + return player; + } + + public UUID getUniqueId() { + return player.getUniqueId(); + } + + public List getItems() { + return items; + } + + public double getMoney() { + return money; + } + + public void setMoney(double money) { + this.money = money; + } + + public void addMoney(double amount) { + this.money += amount; + } + + public boolean isReady() { + return isReady; + } + + public void setReady(boolean ready) { + isReady = ready; + } + + public boolean hasConfirmed() { + return hasConfirmed; + } + + public void setConfirmed(boolean confirmed) { + this.hasConfirmed = confirmed; + } +} + diff --git a/src/main/java/fr/maxlego08/essentials/module/modules/trade/model/TradeSession.java b/src/main/java/fr/maxlego08/essentials/module/modules/trade/model/TradeSession.java new file mode 100644 index 00000000..be8ea559 --- /dev/null +++ b/src/main/java/fr/maxlego08/essentials/module/modules/trade/model/TradeSession.java @@ -0,0 +1,54 @@ +package fr.maxlego08.essentials.module.modules.trade.model; + +import fr.maxlego08.essentials.module.modules.trade.enums.TradeState; +import org.bukkit.entity.Player; + +public class TradeSession { + + private final TradePlayer player1; + private final TradePlayer player2; + private TradeState state = TradeState.WAITING; + private int countdownTask = -1; + + public TradeSession(Player p1, Player p2) { + this.player1 = new TradePlayer(p1); + this.player2 = new TradePlayer(p2); + } + + public TradePlayer getTradePlayer1() { + return player1; + } + + public TradePlayer getTradePlayer2() { + return player2; + } + + public TradePlayer getTradePlayer(Player player) { + if (player1.getPlayer().equals(player)) return player1; + if (player2.getPlayer().equals(player)) return player2; + return null; + } + + public TradePlayer getOtherTradePlayer(Player player) { + if (player1.getPlayer().equals(player)) return player2; + if (player2.getPlayer().equals(player)) return player1; + return null; + } + + public TradeState getState() { + return state; + } + + public void setState(TradeState state) { + this.state = state; + } + + public void setCountdownTask(int taskId) { + this.countdownTask = taskId; + } + + public int getCountdownTask() { + return countdownTask; + } +} + diff --git a/src/main/java/fr/maxlego08/essentials/placeholders/LocalPlaceholder.java b/src/main/java/fr/maxlego08/essentials/placeholders/LocalPlaceholder.java index 7f9a9997..6f489b17 100644 --- a/src/main/java/fr/maxlego08/essentials/placeholders/LocalPlaceholder.java +++ b/src/main/java/fr/maxlego08/essentials/placeholders/LocalPlaceholder.java @@ -9,10 +9,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class LocalPlaceholder implements Placeholder { @@ -20,23 +18,22 @@ public class LocalPlaceholder implements Placeholder { private final List autoPlaceholders = new ArrayList<>(); private final EssentialsPlugin plugin; private final String prefix = "zessentials"; + private final String realPrefix; public LocalPlaceholder(EssentialsPlugin plugin) { this.plugin = plugin; + this.realPrefix = this.prefix + "_"; } public String setPlaceholders(Player player, String placeholder) { - if (placeholder == null || !placeholder.contains("%")) { return placeholder; } - final String realPrefix = this.prefix + "_"; - Matcher matcher = this.pattern.matcher(placeholder); while (matcher.find()) { String stringPlaceholder = matcher.group(0); - String regex = matcher.group(1).replace(realPrefix, ""); + String regex = matcher.group(1).replace(this.realPrefix, ""); String replace = this.onRequest(player, regex); if (replace != null) { placeholder = placeholder.replace(stringPlaceholder, replace); @@ -47,20 +44,27 @@ public String setPlaceholders(Player player, String placeholder) { } public List setPlaceholders(Player player, List lore) { - return lore == null ? null : lore.stream().map(e -> e = setPlaceholders(player, e)).collect(Collectors.toList()); + if (lore == null) return null; + + List result = new ArrayList<>(lore.size()); + for (int i = 0, size = lore.size(); i < size; i++) { + result.add(setPlaceholders(player, lore.get(i))); + } + return result; } @Override public String onRequest(Player player, String string) { - if (string == null || player == null) return null; - Optional optional = this.autoPlaceholders.stream().filter(autoPlaceholder -> autoPlaceholder.startsWith(string)).findFirst(); - if (optional.isPresent()) { - - AutoPlaceholder autoPlaceholder = optional.get(); - String value = string.replace(autoPlaceholder.getStartWith(), ""); - return autoPlaceholder.accept(player, value); + // Optimized: Use for-loop instead of stream for hot path + int size = this.autoPlaceholders.size(); + for (int i = 0; i < size; i++) { + AutoPlaceholder autoPlaceholder = this.autoPlaceholders.get(i); + if (autoPlaceholder.startsWith(string)) { + String value = string.replace(autoPlaceholder.getStartWith(), ""); + return autoPlaceholder.accept(player, value); + } } return null; diff --git a/src/main/java/fr/maxlego08/essentials/server/PaperServer.java b/src/main/java/fr/maxlego08/essentials/server/PaperServer.java index 757301bc..a800d029 100644 --- a/src/main/java/fr/maxlego08/essentials/server/PaperServer.java +++ b/src/main/java/fr/maxlego08/essentials/server/PaperServer.java @@ -5,6 +5,8 @@ import fr.maxlego08.essentials.api.messages.Message; import fr.maxlego08.essentials.api.server.EssentialsServer; import fr.maxlego08.essentials.api.storage.IStorage; +import fr.maxlego08.essentials.api.teleport.CrossServerLocation; +import fr.maxlego08.essentials.api.teleport.TeleportType; import fr.maxlego08.essentials.api.user.Option; import fr.maxlego08.essentials.api.user.PrivateMessage; import fr.maxlego08.essentials.api.user.User; @@ -17,6 +19,9 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -136,4 +141,50 @@ public void deleteCooldown(UUID uniqueId, String cooldownName) { public void updateCooldown(UUID uniqueId, String cooldownName, long expiredAt) { this.plugin.getUtils().updateCooldown(uniqueId, cooldownName, expiredAt); } + + @Override + public void sendToServer(Player player, String serverName) { + try { + ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(byteArray); + out.writeUTF("Connect"); + out.writeUTF(serverName); + player.sendPluginMessage(this.plugin, "BungeeCord", byteArray.toByteArray()); + } catch (IOException exception) { + this.plugin.getLogger().severe("Failed to send player to server: " + exception.getMessage()); + } + } + + @Override + public void crossServerTeleport(Player player, TeleportType teleportType, CrossServerLocation destination) { + // For non-Redis servers, cross-server teleport is not supported + // Just teleport locally if same server + String currentServer = getServerName(); + if (destination.isSameServer(currentServer)) { + User user = this.plugin.getUser(player.getUniqueId()); + if (user != null) { + user.teleport(destination.toSafeLocation().getLocation()); + } + } else { + message(player, Message.TELEPORT_CROSS_SERVER_NOT_SUPPORTED); + } + } + + @Override + public void crossServerTeleportToPlayer(Player player, TeleportType teleportType, String targetPlayerName, String targetServer) { + // For non-Redis servers, cross-server teleport is not supported + message(player, Message.TELEPORT_CROSS_SERVER_NOT_SUPPORTED); + } + + @Override + public String getServerName() { + return this.plugin.getConfiguration().getServerName(); + } + + @Override + public String findPlayerServer(String playerName) { + // For non-Redis servers, only check local + Player player = Bukkit.getPlayer(playerName); + return player != null ? getServerName() : null; + } } diff --git a/src/main/java/fr/maxlego08/essentials/server/SpigotServer.java b/src/main/java/fr/maxlego08/essentials/server/SpigotServer.java index 9c0e4d4a..ee83e02f 100644 --- a/src/main/java/fr/maxlego08/essentials/server/SpigotServer.java +++ b/src/main/java/fr/maxlego08/essentials/server/SpigotServer.java @@ -5,6 +5,8 @@ import fr.maxlego08.essentials.api.messages.Message; import fr.maxlego08.essentials.api.server.EssentialsServer; import fr.maxlego08.essentials.api.storage.IStorage; +import fr.maxlego08.essentials.api.teleport.CrossServerLocation; +import fr.maxlego08.essentials.api.teleport.TeleportType; import fr.maxlego08.essentials.api.user.Option; import fr.maxlego08.essentials.api.user.PrivateMessage; import fr.maxlego08.essentials.api.user.User; @@ -14,6 +16,9 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -110,4 +115,38 @@ public void deleteCooldown(UUID uniqueId, String cooldownName) { public void updateCooldown(UUID uniqueId, String cooldownName, long expiredAt) { this.plugin.getUtils().deleteCooldown(uniqueId, cooldownName); } + + @Override + public void sendToServer(Player player, String serverName) { + try { + ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(byteArray); + out.writeUTF("Connect"); + out.writeUTF(serverName); + player.sendPluginMessage(this.plugin, "BungeeCord", byteArray.toByteArray()); + } catch (IOException exception) { + this.plugin.getLogger().severe("Failed to send player to server: " + exception.getMessage()); + } + } + + @Override + public void crossServerTeleport(Player player, TeleportType teleportType, CrossServerLocation destination) { + // For non-Redis servers, cross-server teleport is not supported + } + + @Override + public void crossServerTeleportToPlayer(Player player, TeleportType teleportType, String targetPlayerName, String targetServer) { + // For non-Redis servers, cross-server teleport is not supported + } + + @Override + public String getServerName() { + return this.plugin.getConfiguration().getServerName(); + } + + @Override + public String findPlayerServer(String playerName) { + Player player = Bukkit.getPlayer(playerName); + return player != null ? getServerName() : null; + } } diff --git a/src/main/java/fr/maxlego08/essentials/storage/database/repositeries/VaultItemRepository.java b/src/main/java/fr/maxlego08/essentials/storage/database/repositeries/VaultItemRepository.java index 76908f8c..64ae3b29 100644 --- a/src/main/java/fr/maxlego08/essentials/storage/database/repositeries/VaultItemRepository.java +++ b/src/main/java/fr/maxlego08/essentials/storage/database/repositeries/VaultItemRepository.java @@ -48,23 +48,26 @@ public void updateQuantity(UUID uniqueId, int vaultId, int slot, long quantity) private void startTask(CacheKey key) { this.plugin.getScheduler().runLaterAsync(() -> { - - long currentTime = System.currentTimeMillis(); - var value = this.caches.get(key); - if (value == null) { - return; - } - - if (currentTime - value.getCreatedAt() >= 200) { - this.caches.remove(key); - this.update(table -> { - table.bigInt("quantity", value.quantity); - table.where("unique_id", key.uniqueId); - table.where("vault_id", key.vaultId); - table.where("slot", key.slot); - }); - } else { - this.startTask(key); + try { + long currentTime = System.currentTimeMillis(); + var value = this.caches.get(key); + if (value == null) { + return; + } + + if (currentTime - value.getCreatedAt() >= 200) { + this.caches.remove(key); + this.update(table -> { + table.bigInt("quantity", value.quantity); + table.where("unique_id", key.uniqueId); + table.where("vault_id", key.vaultId); + table.where("slot", key.slot); + }); + } else { + this.startTask(key); + } + } catch (Exception e) { + this.plugin.getLogger().warning("Error in vault item cache task: " + e.getMessage()); } }, 4); } diff --git a/src/main/java/fr/maxlego08/essentials/storage/storages/SqlStorage.java b/src/main/java/fr/maxlego08/essentials/storage/storages/SqlStorage.java index 37ec78f8..12101ef7 100644 --- a/src/main/java/fr/maxlego08/essentials/storage/storages/SqlStorage.java +++ b/src/main/java/fr/maxlego08/essentials/storage/storages/SqlStorage.java @@ -122,6 +122,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -132,8 +133,9 @@ public class SqlStorage extends StorageHelper implements IStorage { private final TypeSafeCache cache = new TypeSafeCache(); private final DatabaseConnection connection; private final Repositories repositories; - private final Map economyUpdateQueue = new HashMap<>(); - private final Set existingUUIDs = new HashSet<>(); + // Thread-safe maps for async operations + private final Map economyUpdateQueue = new ConcurrentHashMap<>(); + private final Set existingUUIDs = ConcurrentHashMap.newKeySet(); public SqlStorage(EssentialsPlugin plugin, StorageType storageType) { super(plugin); diff --git a/src/main/java/fr/maxlego08/essentials/user/ZTeleportHereRequest.java b/src/main/java/fr/maxlego08/essentials/user/ZTeleportHereRequest.java index 3f5a142f..dc2c8141 100644 --- a/src/main/java/fr/maxlego08/essentials/user/ZTeleportHereRequest.java +++ b/src/main/java/fr/maxlego08/essentials/user/ZTeleportHereRequest.java @@ -50,9 +50,21 @@ public boolean isValid() { @Override public void accept() { + // Check if both players are online + if (!this.fromUser.isOnline() || !this.toUser.isOnline()) { + this.isTeleport = true; + return; + } + message(this.fromUser, Message.COMMAND_TPA_HERE_ACCEPT_SENDER, this.toUser); message(this.toUser, Message.COMMAND_TPA_HERE_ACCEPT_RECEIVER, this.fromUser); + // Validate player objects + if (this.fromUser.getPlayer() == null || this.toUser.getPlayer() == null) { + this.isTeleport = true; + return; + } + TeleportationModule teleportationModule = this.plugin.getModuleManager().getModule(TeleportationModule.class); AtomicInteger atomicInteger = new AtomicInteger(teleportationModule.getTeleportDelay(toUser.getPlayer())); @@ -66,27 +78,33 @@ public void accept() { PlatformScheduler serverImplementation = this.plugin.getScheduler(); serverImplementation.runAtLocationTimer(this.fromUser.getPlayer().getLocation(), wrappedTask -> { - if (!same(playerLocation, toUser.getPlayer().getLocation())) { - - message(this.toUser, Message.TELEPORT_MOVE); + // Check if players are still online + if (!this.toUser.isOnline() || !this.fromUser.isOnline()) { wrappedTask.cancel(); this.fromUser.removeTeleportRequest(this.toUser); return; } - int currentSecond = atomicInteger.getAndDecrement(); + // Validate player objects + if (this.fromUser.getPlayer() == null || this.toUser.getPlayer() == null) { + wrappedTask.cancel(); + this.fromUser.removeTeleportRequest(this.toUser); + return; + } - if (!this.toUser.isOnline() || !this.fromUser.isOnline()) { + if (!same(playerLocation, toUser.getPlayer().getLocation())) { + message(this.toUser, Message.TELEPORT_MOVE); wrappedTask.cancel(); + this.fromUser.removeTeleportRequest(this.toUser); return; } - if (currentSecond <= 0) { + int currentSecond = atomicInteger.getAndDecrement(); + if (currentSecond <= 0) { wrappedTask.cancel(); this.teleport(teleportationModule); } else { - message(this.toUser, Message.TELEPORT_MESSAGE, "%seconds%", currentSecond); } @@ -94,6 +112,17 @@ public void accept() { } private void teleport(TeleportationModule teleportationModule) { + // Validate both players are still online and have valid player objects + if (!this.toUser.isOnline() || !this.fromUser.isOnline()) { + this.isTeleport = true; + return; + } + + if (this.fromUser.getPlayer() == null || this.toUser.getPlayer() == null) { + this.isTeleport = true; + return; + } + Location playerLocation = fromUser.getPlayer().getLocation(); Location location = toUser.getPlayer().isFlying() ? playerLocation : teleportationModule.isTeleportSafety() ? toSafeLocation(playerLocation) : playerLocation; diff --git a/src/main/java/fr/maxlego08/essentials/user/ZTeleportRequest.java b/src/main/java/fr/maxlego08/essentials/user/ZTeleportRequest.java index 019b7f76..84ed3231 100644 --- a/src/main/java/fr/maxlego08/essentials/user/ZTeleportRequest.java +++ b/src/main/java/fr/maxlego08/essentials/user/ZTeleportRequest.java @@ -50,9 +50,21 @@ public boolean isValid() { @Override public void accept() { + // Check if both players are online + if (!this.fromUser.isOnline() || !this.toUser.isOnline()) { + this.isTeleport = true; + return; + } + message(this.fromUser, Message.COMMAND_TPA_ACCEPT_SENDER, this.toUser); message(this.toUser, Message.COMMAND_TPA_ACCEPT_RECEIVER, this.fromUser); + // Validate player objects + if (this.fromUser.getPlayer() == null || this.toUser.getPlayer() == null) { + this.isTeleport = true; + return; + } + TeleportationModule teleportationModule = this.plugin.getModuleManager().getModule(TeleportationModule.class); AtomicInteger atomicInteger = new AtomicInteger(teleportationModule.getTeleportDelay(fromUser.getPlayer())); @@ -66,27 +78,33 @@ public void accept() { PlatformScheduler serverImplementation = this.plugin.getScheduler(); serverImplementation.runAtLocationTimer(this.toUser.getPlayer().getLocation(), wrappedTask -> { - if (!same(playerLocation, fromUser.getPlayer().getLocation())) { - - message(this.fromUser, Message.TELEPORT_MOVE); + // Check if players are still online + if (!this.toUser.isOnline() || !this.fromUser.isOnline()) { wrappedTask.cancel(); this.fromUser.removeTeleportRequest(this.toUser); return; } - int currentSecond = atomicInteger.getAndDecrement(); + // Validate player objects + if (this.fromUser.getPlayer() == null || this.toUser.getPlayer() == null) { + wrappedTask.cancel(); + this.fromUser.removeTeleportRequest(this.toUser); + return; + } - if (!this.toUser.isOnline() || !this.fromUser.isOnline()) { + if (!same(playerLocation, fromUser.getPlayer().getLocation())) { + message(this.fromUser, Message.TELEPORT_MOVE); wrappedTask.cancel(); + this.fromUser.removeTeleportRequest(this.toUser); return; } - if (currentSecond <= 0) { + int currentSecond = atomicInteger.getAndDecrement(); + if (currentSecond <= 0) { wrappedTask.cancel(); this.teleport(teleportationModule); } else { - message(this.fromUser, Message.TELEPORT_MESSAGE, "%seconds%", currentSecond); } @@ -94,6 +112,17 @@ public void accept() { } private void teleport(TeleportationModule teleportationModule) { + // Validate both players are still online and have valid player objects + if (!this.toUser.isOnline() || !this.fromUser.isOnline()) { + this.isTeleport = true; + return; + } + + if (this.fromUser.getPlayer() == null || this.toUser.getPlayer() == null) { + this.isTeleport = true; + return; + } + Location playerLocation = toUser.getPlayer().getLocation(); Location location = fromUser.getPlayer().isFlying() ? playerLocation : teleportationModule.isTeleportSafety() ? toSafeLocation(playerLocation) : playerLocation; diff --git a/src/main/java/fr/maxlego08/essentials/zutils/utils/AttributeUtils.java b/src/main/java/fr/maxlego08/essentials/zutils/utils/AttributeUtils.java index a7132223..a403e39d 100644 --- a/src/main/java/fr/maxlego08/essentials/zutils/utils/AttributeUtils.java +++ b/src/main/java/fr/maxlego08/essentials/zutils/utils/AttributeUtils.java @@ -4,12 +4,13 @@ import org.bukkit.Registry; import org.bukkit.attribute.Attribute; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class AttributeUtils { - private static final Map attributeCache = new HashMap<>(); + // Thread-safe cache for attribute lookups + private static final Map attributeCache = new ConcurrentHashMap<>(); public static Attribute getAttribute(String name) { return attributeCache.computeIfAbsent(name, key -> { diff --git a/src/main/java/fr/maxlego08/essentials/zutils/utils/YamlLoader.java b/src/main/java/fr/maxlego08/essentials/zutils/utils/YamlLoader.java index 6f8cfcd6..e61899ac 100644 --- a/src/main/java/fr/maxlego08/essentials/zutils/utils/YamlLoader.java +++ b/src/main/java/fr/maxlego08/essentials/zutils/utils/YamlLoader.java @@ -62,6 +62,20 @@ protected void loadYamlConfirmation(EssentialsPlugin plugin, YamlConfiguration c } field.set(this, configuration.getStringList(configKey)); + } else if (field.getType().equals(Map.class) && isStringStringMap(field)) { + // Handle Map fields with optimized loading + ConfigurationSection section = configuration.getConfigurationSection(configKey); + if (section != null) { + Map map = section.getKeys(false).stream() + .collect(HashMap::new, + (m, key) -> { + String value = section.getString(key); + if (value != null) m.put(key, value); + }, + HashMap::putAll); + field.set(this, map); + } + continue; } else { ConfigurationSection configurationSection = configuration.getConfigurationSection(configKey); if (configurationSection == null) continue; @@ -79,4 +93,15 @@ private List loadObjects(Logger logger, Class fieldArgClass, List constructor = fieldArgClass.getConstructors()[0]; return maps.stream().map(map -> createInstanceFromMap(logger, constructor, map)).collect(Collectors.toList()); } + + private boolean isStringStringMap(Field field) { + Type genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType paramType) { + Type[] typeArgs = paramType.getActualTypeArguments(); + return typeArgs.length == 2 && + typeArgs[0].equals(String.class) && + typeArgs[1].equals(String.class); + } + return false; + } } diff --git a/src/main/java/fr/maxlego08/essentials/zutils/utils/commands/VCommand.java b/src/main/java/fr/maxlego08/essentials/zutils/utils/commands/VCommand.java index cbfba535..2f0c4902 100644 --- a/src/main/java/fr/maxlego08/essentials/zutils/utils/commands/VCommand.java +++ b/src/main/java/fr/maxlego08/essentials/zutils/utils/commands/VCommand.java @@ -27,7 +27,8 @@ public abstract class VCommand extends Arguments implements EssentialsCommand { - private static final Map>> uuidRequestQueue = new HashMap<>(); + // Thread-safe queue for UUID requests + private static final Map>> uuidRequestQueue = new java.util.concurrent.ConcurrentHashMap<>(); protected final EssentialsPlugin plugin; protected final List cooldowns = Arrays.asList( "1m", // 60 seconds diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 41638c7a..82972ee7 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -30,6 +30,13 @@ storage-type: SQLITE # REDIS - Allows connecting several servers (currently in development) server-type: PAPER +# The name of this server, used for cross-server teleportation with BungeeCord/Velocity +# This should match your BungeeCord/Velocity server name +server-name: "default" + +# Enable cross-server teleportation features (requires Redis and BungeeCord/Velocity) +cross-server-teleport-enabled: false + # Configuration of your database, it is recommended to use the database to store your data. # JSON does not support everything database-configuration: diff --git a/src/main/resources/modules/spawn/config.yml b/src/main/resources/modules/spawn/config.yml index b3cdb749..9437de04 100644 --- a/src/main/resources/modules/spawn/config.yml +++ b/src/main/resources/modules/spawn/config.yml @@ -37,4 +37,9 @@ respawn-at-home: false respawn-at-bed: true # Allows teleporting the player to spawn when logged in -teleport-at-spawn-on-join: false \ No newline at end of file +teleport-at-spawn-on-join: false + +# Allows teleporting the player to their last location when they log in +# This will teleport the player to where they were when they last logged out +# Note: This takes priority over teleport-at-spawn-on-join when enabled +teleport-to-last-location-on-join: false \ No newline at end of file diff --git a/src/main/resources/modules/trade/config.yml b/src/main/resources/modules/trade/config.yml new file mode 100644 index 00000000..cc5a4e9a --- /dev/null +++ b/src/main/resources/modules/trade/config.yml @@ -0,0 +1,170 @@ +enable: true +request-timeout: 60 +max-distance: 10.0 +worlds: + - world + - world_nether + - world_the_end + +# DOCUMENTATION: https://docs.artillex-studios.com/axtrade.html +# ITEM BUILDER: https://docs.artillex-studios.com/item-builder.html + +# ----- SETTINGS ----- +title: "&0Trading with: %player%" +# a gui can have 1-6 rows +rows: 6 + +# ----- SLOTS ----- +# the slots where the items can be placed +# make sure to not put decorative items or currency items in the these slots +# the own-slots and partner-slots must have an equal amount of slots +own-slots: + - 9-12 + - 18-21 + - 27-30 + - 36-39 + - 45-48 + +partner-slots: + - 14-17 + - 23-26 + - 32-35 + - 41-44 + - 50-53 + +# ----- ITEMS ----- +# items on your side +own: + confirm-item: + slot: 0 + # you can use these placeholders: + # %own-name%" + # %partner-name% + accept: + material: "RED_CONCRETE" + # if you want, you can add head textures, like this: + #material: "PLAYER_HEAD" + #texture: "%own-head%" + name: "�ffdd&lACCEPT TRADE" + lore: + - "" + - " &7- &fAre you happy with the trade?" + - "" + - "�ffdd&l> �ffddClick &8- �ffddConfirm Trade" + cancel: + material: "LIME_CONCRETE" + name: "�ffdd&lCANCEL CONFIRMATION" + lore: + - "" + - " &7- &fDo you want to change something?" + - "" + - "�ffdd&l> �ffddClick &8- �ffddCancel Confirmation" + + # you can define as many currencies as you want, but make sure to copy them to the 'partner' section too! + currency1: + slot: 2 + # you need Vault installed for this + currency: "Vault" + material: "GOLD_NUGGET" + name: "�ffdd&lMONEY" + # you can use these placeholders: + # %amount% (the amount the player set) + # %tax-amount% (amount after tax) + # %tax-percent% (the % of tax on the currency) + # %tax-fee% (amount taken because of tax) + lore: + - "&7Your offer" + - "" + - " &7- &fAmount: �ffdd%amount%$" + - "" + - "�ffdd&l> �ffddClick &8- �ffddChange Amount" + + currency2: + slot: 3 + currency: "Experience" + material: "EXPERIENCE_BOTTLE" + name: "�ffdd&lEXPERIENCE" + lore: + - "&7Your offer" + - "" + - " &7- &fAmount: �ffdd%amount% EXP" + - "" + - "�ffdd&l> �ffddClick &8- �ffddChange Amount" + +# items on your trade partner's side +partner: + confirm-item: + slot: 8 + # you can also use these placeholders: + # %own-name%" + # %partner-name% + accept: + material: "RED_CONCRETE" + # if you want, you can add head textures, like this: + #material: "PLAYER_HEAD" + #texture: "%partner-head%" + name: "�ffdd&lWAITING FOR OTHER PLAYER" + lore: + - "" + - " &7- &f%partner-name% has not yet confirmed the trade!" + - "" + cancel: + material: "LIME_CONCRETE" + name: "�ffdd&lWAITING" + lore: + - "" + - " &7- &f%partner-name% has confirmed the trade!" + + currency1: + slot: 6 + currency: "Vault" + material: "GOLD_NUGGET" + name: "�ffdd&lMONEY" + # you can use these placeholders: + # %amount% (the amount the player set) + # %tax-amount% (amount after tax) + # %tax-percent% (the % of tax on the currency) + # %tax-fee% (amount taken because of tax) + lore: + - "&7%partner-name%'s offer" + - "" + - " &7- &fAmount: �ffdd%amount%$" + - "" + - "�ffdd&l> �ffddClick &8- �ffddChange Amount" + + currency2: + slot: 5 + currency: "Experience" + material: "EXPERIENCE_BOTTLE" + name: "�ffdd&lEXPERIENCE" + lore: + - "&7%partner-name%'s offer" + - "" + - " &7- &fAmount: �ffdd%amount% EXP" + - "" + - "�ffdd&l> �ffddClick &8- �ffddChange Amount" + +decorations: + separator: + slot: [4, 13, 22, 31, 40, 49] + material: "LIGHT_BLUE_STAINED_GLASS_PANE" + name: " " + +# Messages +messages: + request-sent: "&aTrade request sent to %player%." + request-received: "&a%player% sent you a trade request. Type /trade accept %player% to accept." + request-accepted: "&aTrade request accepted." + request-denied: "&cTrade request denied." + trade-cancelled: "&cTrade cancelled." + trade-completed: "&aTrade completed successfully." + player-not-found: "&cPlayer not found." + player-too-far: "&cPlayer is too far away." + inventory-full: "&cYour inventory is full. Items dropped on ground." + already-trading: "&cYou are already trading." + target-already-trading: "&c%player% is already trading." + no-request: "&cYou don't have a trade request from %player%." + yourself: "&cYou cannot trade with yourself." + trade-ready: "&aYou marked yourself as ready." + trade-not-ready: "&cYou are no longer ready." + command-no-arg: "&cYou must specify a player."