From 4d60395b0d61e294e8c38a28a08d0c4172a0feaf Mon Sep 17 00:00:00 2001 From: mozidevv Date: Sat, 1 Aug 2026 14:36:09 +0300 Subject: [PATCH 1/3] Add admin command, /rtp cancel and /rtp - /lexonrtp reload: reload config and messages without restart - /rtp cancel: cancel active solo teleport countdown - /rtp : admin teleport of other players (lexonrtp.admin) - Console support for admin teleports - New messages: rtp-cancelled, rtp-cancel-none, player-not-found, reloaded, lexonrtp-help (en + tr) --- README.md | 4 ++ src/main/java/com/lexon/rtp/LexonRTP.java | 10 ++++ .../com/lexon/rtp/command/BaseRtpCommand.java | 8 ++- .../lexon/rtp/command/LexonRtpCommand.java | 49 ++++++++++++++++ .../com/lexon/rtp/command/RtpCommand.java | 57 +++++++++++++++++++ .../com/lexon/rtp/config/MessageManager.java | 11 ++++ src/main/resources/messages.yml | 7 +++ src/main/resources/messages_en.yml | 7 +++ src/main/resources/plugin.yml | 7 +++ 9 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/lexon/rtp/command/LexonRtpCommand.java diff --git a/README.md b/README.md index 611c76f..5e53049 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,19 @@ High performance Random Teleport plugin with 1v1 matchmaking, Redis cross-server |---|---| | `/rtp` | Opens solo teleport menu | | `/rtp ` | Direct teleport to specified world | +| `/rtp cancel` | Cancel an active solo teleport countdown | +| `/rtp ` | Admin: teleport another player (requires `lexonrtp.admin`) | | `/rtpqueue` | Opens 1v1 matchmaking menu | | `/rtpqueue ` | Join queue for specified world | | `/rtpqueue leave` | Leave the queue | +| `/lexonrtp reload` | Admin: reload config & messages without restart (requires `lexonrtp.admin`) | ## Permissions | Permission | Default | Description | |---|---|---| | `lexonrtp.use` | `true` | Use RTP commands | +| `lexonrtp.admin` | `op` | Reload plugin and RTP other players | | `lexonrtp.cooldown.bypass` | `op` | Bypass cooldown | ## Installation diff --git a/src/main/java/com/lexon/rtp/LexonRTP.java b/src/main/java/com/lexon/rtp/LexonRTP.java index 70894a3..6622273 100644 --- a/src/main/java/com/lexon/rtp/LexonRTP.java +++ b/src/main/java/com/lexon/rtp/LexonRTP.java @@ -1,5 +1,6 @@ package com.lexon.rtp; +import com.lexon.rtp.command.LexonRtpCommand; import com.lexon.rtp.command.RtpCommand; import com.lexon.rtp.command.RtpQueueCommand; import com.lexon.rtp.config.ConfigManager; @@ -42,6 +43,7 @@ public void onEnable() { this.queueManager.start(); register("rtp", new RtpCommand(this)); register("rtpqueue", new RtpQueueCommand(this)); + register("lexonrtp", new LexonRtpCommand(this)); getServer().getPluginManager().registerEvents(new MenuListener(this), this); getServer().getPluginManager().registerEvents(new CrossServerListener(this), this); getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord"); @@ -100,4 +102,12 @@ public QueueManager queue() { public RtpService rtpService() { return rtpService; } + + public void reloadPlugin() { + configManager.load(); + messageManager.load(); + this.locationFinder = new LocationFinder(this); + queueManager.start(); + getLogger().info("LexonRTP reloaded."); + } } diff --git a/src/main/java/com/lexon/rtp/command/BaseRtpCommand.java b/src/main/java/com/lexon/rtp/command/BaseRtpCommand.java index 6996ae7..31b4437 100644 --- a/src/main/java/com/lexon/rtp/command/BaseRtpCommand.java +++ b/src/main/java/com/lexon/rtp/command/BaseRtpCommand.java @@ -19,8 +19,7 @@ protected BaseRtpCommand(LexonRTP plugin) { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!(sender instanceof Player player)) { - plugin.messages().send(sender, "players-only"); - return true; + return executeConsole(sender, args); } if (!player.hasPermission("lexonrtp.use")) { plugin.messages().send(player, "no-permission"); @@ -31,6 +30,11 @@ public boolean onCommand(CommandSender sender, Command command, String label, St protected abstract boolean execute(Player player, String[] args); + protected boolean executeConsole(CommandSender sender, String[] args) { + plugin.messages().send(sender, "players-only"); + return true; + } + protected List worldKeys(String prefix) { List matches = new ArrayList<>(); for (String key : plugin.config().getWorlds().keySet()) { diff --git a/src/main/java/com/lexon/rtp/command/LexonRtpCommand.java b/src/main/java/com/lexon/rtp/command/LexonRtpCommand.java new file mode 100644 index 0000000..41a9a90 --- /dev/null +++ b/src/main/java/com/lexon/rtp/command/LexonRtpCommand.java @@ -0,0 +1,49 @@ +package com.lexon.rtp.command; + +import com.lexon.rtp.LexonRTP; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; + +import java.util.List; + +public final class LexonRtpCommand implements TabExecutor { + private final LexonRTP plugin; + + public LexonRtpCommand(LexonRTP plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("lexonrtp.admin")) { + plugin.messages().send(sender, "no-permission"); + return true; + } + if (args.length == 0) { + plugin.messages().sendList(sender, "lexonrtp-help"); + return true; + } + if (args[0].equalsIgnoreCase("reload")) { + plugin.reloadPlugin(); + plugin.messages().send(sender, "reloaded"); + return true; + } + plugin.messages().sendList(sender, "lexonrtp-help"); + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (!sender.hasPermission("lexonrtp.admin")) { + return List.of(); + } + if (args.length == 1) { + String prefix = args[0].toLowerCase(); + if ("reload".startsWith(prefix)) { + return List.of("reload"); + } + } + return List.of(); + } +} diff --git a/src/main/java/com/lexon/rtp/command/RtpCommand.java b/src/main/java/com/lexon/rtp/command/RtpCommand.java index bdad3b3..66783c7 100644 --- a/src/main/java/com/lexon/rtp/command/RtpCommand.java +++ b/src/main/java/com/lexon/rtp/command/RtpCommand.java @@ -2,8 +2,13 @@ import com.lexon.rtp.LexonRTP; import com.lexon.rtp.gui.RtpMenu; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.util.List; + public final class RtpCommand extends BaseRtpCommand { public RtpCommand(LexonRTP plugin) { super(plugin); @@ -15,7 +20,59 @@ protected boolean execute(Player player, String[] args) { new RtpMenu(plugin).open(player); return true; } + if (args.length == 1 && args[0].equalsIgnoreCase("cancel")) { + if (plugin.queue().cancelSoloCountdown(player.getUniqueId())) { + plugin.messages().send(player, "rtp-cancelled"); + } else { + plugin.messages().send(player, "rtp-cancel-none"); + } + return true; + } + if (args.length == 2) { + if (!player.hasPermission("lexonrtp.admin")) { + plugin.messages().send(player, "no-permission"); + return true; + } + Player target = Bukkit.getPlayer(args[0]); + if (target == null) { + plugin.messages().send(player, "player-not-found"); + return true; + } + plugin.rtpService().request(target, args[1].toLowerCase(), false); + return true; + } plugin.rtpService().request(player, args[0].toLowerCase(), false); return true; } + + @Override + protected boolean executeConsole(CommandSender sender, String[] args) { + if (args.length >= 2 && sender.hasPermission("lexonrtp.admin")) { + Player target = Bukkit.getPlayer(args[0]); + if (target == null) { + plugin.messages().send(sender, "player-not-found"); + return true; + } + plugin.rtpService().request(target, args[1].toLowerCase(), false); + return true; + } + plugin.messages().send(sender, "players-only"); + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + String prefix = args[0].toLowerCase(); + List matches = worldKeys(prefix); + if ("cancel".startsWith(prefix)) { + matches.add("cancel"); + } + return matches; + } + if (args.length == 2 && sender.hasPermission("lexonrtp.admin")) { + return worldKeys(args[1].toLowerCase()); + } + return List.of(); + } } diff --git a/src/main/java/com/lexon/rtp/config/MessageManager.java b/src/main/java/com/lexon/rtp/config/MessageManager.java index 9225394..50cbebc 100644 --- a/src/main/java/com/lexon/rtp/config/MessageManager.java +++ b/src/main/java/com/lexon/rtp/config/MessageManager.java @@ -81,4 +81,15 @@ public void send(CommandSender target, String path, String... replacements) { target.sendMessage(message); } } + + public void sendList(CommandSender target, String path, String... replacements) { + if (target == null) { + return; + } + for (String line : getList(path, replacements)) { + if (!line.isBlank()) { + target.sendMessage(line); + } + } + } } diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 87e6718..5907d2a 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -34,3 +34,10 @@ searching: "%prefix%&7ɢᴜᴠᴇɴʟɪ ʟᴏᴋᴀsʏᴏɴ ᴀʀᴀɴɪʏᴏʀ. teleport-success: "%prefix%&aɪsɪɴʟᴀɴᴅɪɴ!" teleport-failed: "%prefix%&cɢᴜᴠᴇɴʟɪ ʙɪʀ ʏᴇʀ ʙᴜʟᴜɴᴀᴍᴀᴅɪ, ʟᴜᴛғᴇɴ ᴛᴇᴋʀᴀʀ ᴅᴇɴᴇ." moved-cancelled: "%prefix%&cʜᴀʀᴇᴋᴇᴛ ᴇᴛᴛɪɢɪɴ ɪᴄɪɴ ɪsɪɴʟᴀɴᴍᴀ ɪᴘᴛᴀʟ ᴇᴅɪʟᴅɪ." +rtp-cancelled: "%prefix%&aɪsɪɴʟᴀɴᴍᴀ ɪᴘᴛᴀʟ ᴇᴅɪʟᴅɪ." +rtp-cancel-none: "%prefix%&cɪᴘᴛᴀʟ ᴇᴅɪʟᴇᴄᴇᴋ ʙɪʀ ɪsɪɴʟᴀɴᴍᴀ ʏᴏᴋ." +player-not-found: "%prefix%&cᴏʏᴜɴᴄᴜ ʙᴜʟᴜɴᴀᴍᴀᴅɪ ᴠᴇʏᴀ ᴏғʟɪɴᴇ." +reloaded: "%prefix%&aʏᴀᴘɪʟᴀɴᴅɪʀᴍᴀ ᴠᴇ ᴍᴇsᴀᴊʟᴀʀ ʏᴇɴɪʟᴇɴᴅɪ." +lexonrtp-help: + - "%prefix%&8» &aʟᴇxᴏɴʀᴛᴘ &7ᴋᴏᴍᴜᴛʟᴀʀɪ" + - "&a/lexonrtp reload &8» &7ᴋᴏɴғɪɢ ᴠᴇ ᴍᴇsᴀᴊʟᴀʀɪ ʏᴇɴɪʟᴇ" diff --git a/src/main/resources/messages_en.yml b/src/main/resources/messages_en.yml index bdac277..e997877 100644 --- a/src/main/resources/messages_en.yml +++ b/src/main/resources/messages_en.yml @@ -34,3 +34,10 @@ searching: "%prefix%&7sᴇᴀʀᴄʜɪɴɢ ғᴏʀ ᴀ sᴀғᴇ ʟᴏᴄᴀᴛ teleport-success: "%prefix%&aᴛᴇʟᴇᴘᴏʀᴛᴇᴅ!" teleport-failed: "%prefix%&cᴄᴏᴜʟᴅ ɴᴏᴛ ғɪɴᴅ ᴀ sᴀғᴇ ʟᴏᴄᴀᴛɪᴏɴ, ᴘʟᴇᴀsᴇ ᴛʀʏ ᴀɢᴀɪɴ." moved-cancelled: "%prefix%&cᴛᴇʟᴇᴘᴏʀᴛ ᴄᴀɴᴄᴇʟʟᴇᴅ ʙᴇᴄᴀᴜsᴇ ʏᴏᴜ ᴍᴏᴠᴇᴅ." +rtp-cancelled: "%prefix%&aᴛᴇʟᴇᴘᴏʀᴛ ᴄᴀɴᴄᴇʟʟᴇᴅ." +rtp-cancel-none: "%prefix%&cʏᴏᴜ ʜᴀᴠᴇ ɴᴏ ᴀᴄᴛɪᴠᴇ ᴛᴇʟᴇᴘᴏʀᴛ ᴛᴏ ᴄᴀɴᴄᴇʟ." +player-not-found: "%prefix%&cᴘʟᴀʏᴇʀ ɴᴏᴛ ғᴏᴜɴᴅ ᴏʀ ᴏғғʟɪɴᴇ." +reloaded: "%prefix%&aᴄᴏɴғɪɢᴜʀᴀᴛɪᴏɴ ᴀɴᴅ ᴍᴇssᴀɢᴇs ʀᴇʟᴏᴀᴅᴇᴅ." +lexonrtp-help: + - "%prefix%&8» &aʟᴇxᴏɴʀᴛᴘ &7ᴄᴏᴍᴍᴀɴᴅs" + - "&a/lexonrtp reload &8» &7ʀᴇʟᴏᴀᴅ ᴄᴏɴғɪɢ & ᴍᴇssᴀɢᴇs" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 3f1d993..a6e409f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -15,11 +15,18 @@ commands: description: Join or leave the 1v1 matchmaking queue. usage: /rtpqueue [overworld|nether|end|leave] aliases: [rtpq, queue] + lexonrtp: + description: Admin commands (reload, help). + usage: /lexonrtp reload + aliases: [lrtp] permissions: lexonrtp.use: description: Permission to use the RTP command. default: true + lexonrtp.admin: + description: Reload the plugin and teleport other players. + default: op lexonrtp.cooldown.bypass: description: Permission to bypass cooldown. default: op From eaaed2c276e4cdabe91e67a5ea3537d3184ff66e Mon Sep 17 00:00:00 2001 From: mozidevv Date: Sat, 1 Aug 2026 18:55:43 +0300 Subject: [PATCH 2/3] Fix review findings: cancel, locale, admin feedback - /rtp cancel now resets the on-screen title and works during the async location search phase (queue.remove stops the countdown chain) - All world key toLowerCase calls use Locale.ROOT (Turkish locale no longer breaks keys like ISLAND) - Admin/console sender now receives feedback: mirrored request outcomes plus admin-rtp-success/admin-rtp-fail on teleport result - Use getPlayerExact for admin targets, avoid duplicate 'cancel' tab suggestion, reject >2 args with usage, clearer console messages --- src/main/java/com/lexon/rtp/RtpService.java | 29 +++++++++- .../com/lexon/rtp/command/BaseRtpCommand.java | 3 +- .../lexon/rtp/command/LexonRtpCommand.java | 3 +- .../com/lexon/rtp/command/RtpCommand.java | 38 ++++++++---- .../lexon/rtp/command/RtpQueueCommand.java | 5 +- .../com/lexon/rtp/config/ConfigManager.java | 5 +- .../com/lexon/rtp/queue/QueueManager.java | 58 ++++++++++++++----- .../java/com/lexon/rtp/queue/RtpRequest.java | 11 ++++ src/main/resources/messages.yml | 3 + src/main/resources/messages_en.yml | 3 + 10 files changed, 125 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/lexon/rtp/RtpService.java b/src/main/java/com/lexon/rtp/RtpService.java index f2cafe3..1be281b 100644 --- a/src/main/java/com/lexon/rtp/RtpService.java +++ b/src/main/java/com/lexon/rtp/RtpService.java @@ -4,6 +4,7 @@ import com.google.common.io.ByteStreams; import com.lexon.rtp.config.WorldSettings; import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import java.util.List; @@ -16,23 +17,31 @@ public RtpService(LexonRTP plugin) { } public void request(Player player, String worldKey, boolean matchmaking) { + request(player, worldKey, matchmaking, null); + } + + public void request(Player player, String worldKey, boolean matchmaking, CommandSender requester) { WorldSettings target = plugin.config().getWorld(worldKey); if (target == null) { plugin.messages().send(player, "invalid-world"); + feedback(requester, player, "invalid-world"); return; } if (!target.isEnabled()) { plugin.messages().send(player, "world-disabled"); + feedback(requester, player, "world-disabled"); return; } String targetServer = target.getTargetServer(); if (targetServer != null && !targetServer.isEmpty()) { if (!plugin.redis().isConnected()) { plugin.messages().send(player, "cross-server-unavailable"); + feedback(requester, player, "cross-server-unavailable"); return; } plugin.redis().savePendingRtp(player.getUniqueId(), worldKey, matchmaking); plugin.messages().send(player, "rtp-queued"); + feedback(requester, player, "rtp-queued"); ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("Connect"); out.writeUTF(targetServer); @@ -41,29 +50,37 @@ public void request(Player player, String worldKey, boolean matchmaking) { } if (Bukkit.getWorld(target.getWorldName()) == null) { plugin.messages().send(player, "world-not-found"); + feedback(requester, player, "world-not-found"); return; } if (matchmaking && !plugin.config().isQueueEnabled()) { plugin.messages().send(player, "queue-disabled"); + feedback(requester, player, "queue-disabled"); return; } if (plugin.queue().isQueued(player.getUniqueId())) { sendQueueAlready(player); + feedback(requester, player, "queue-already", + "%queue%", String.valueOf(plugin.queue().positionOf(player.getUniqueId()))); return; } if (!player.hasPermission("lexonrtp.cooldown.bypass")) { long remaining = plugin.redis().getRemaining(player.getUniqueId()); if (remaining > 0) { plugin.messages().send(player, "cooldown-active", "%time%", String.valueOf(remaining)); + feedback(requester, player, "cooldown-active", "%time%", String.valueOf(remaining)); return; } } - if (!plugin.queue().enqueue(player, target, matchmaking)) { + if (!plugin.queue().enqueue(player, target, matchmaking, requester)) { sendQueueAlready(player); + feedback(requester, player, "queue-already", + "%queue%", String.valueOf(plugin.queue().positionOf(player.getUniqueId()))); return; } if (!matchmaking) { plugin.messages().send(player, "rtp-queued"); + feedback(requester, player, "rtp-queued"); return; } int count = plugin.queue().matchSize(target.getKey()); @@ -72,11 +89,21 @@ public void request(Player player, String worldKey, boolean matchmaking) { "%count%", String.valueOf(count), "%required%", String.valueOf(required), "%queue%", String.valueOf(plugin.queue().positionOf(player.getUniqueId()))); + feedback(requester, player, "queue-joined", + "%count%", String.valueOf(count), + "%required%", String.valueOf(required), + "%queue%", String.valueOf(plugin.queue().positionOf(player.getUniqueId()))); if (count < required && plugin.config().isAnnounceWaiting()) { broadcastWaiting(player, count, required); } } + private void feedback(CommandSender requester, Player player, String path, String... replacements) { + if (requester != null && requester != player) { + plugin.messages().send(requester, path, replacements); + } + } + private void sendQueueAlready(Player player) { plugin.messages().send(player, "queue-already", "%queue%", String.valueOf(plugin.queue().positionOf(player.getUniqueId()))); diff --git a/src/main/java/com/lexon/rtp/command/BaseRtpCommand.java b/src/main/java/com/lexon/rtp/command/BaseRtpCommand.java index 31b4437..133f8f8 100644 --- a/src/main/java/com/lexon/rtp/command/BaseRtpCommand.java +++ b/src/main/java/com/lexon/rtp/command/BaseRtpCommand.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; public abstract class BaseRtpCommand implements TabExecutor { protected final LexonRTP plugin; @@ -47,6 +48,6 @@ protected List worldKeys(String prefix) { @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - return args.length == 1 ? worldKeys(args[0].toLowerCase()) : List.of(); + return args.length == 1 ? worldKeys(args[0].toLowerCase(Locale.ROOT)) : List.of(); } } diff --git a/src/main/java/com/lexon/rtp/command/LexonRtpCommand.java b/src/main/java/com/lexon/rtp/command/LexonRtpCommand.java index 41a9a90..dc58302 100644 --- a/src/main/java/com/lexon/rtp/command/LexonRtpCommand.java +++ b/src/main/java/com/lexon/rtp/command/LexonRtpCommand.java @@ -6,6 +6,7 @@ import org.bukkit.command.TabExecutor; import java.util.List; +import java.util.Locale; public final class LexonRtpCommand implements TabExecutor { private final LexonRTP plugin; @@ -39,7 +40,7 @@ public List onTabComplete(CommandSender sender, Command command, String return List.of(); } if (args.length == 1) { - String prefix = args[0].toLowerCase(); + String prefix = args[0].toLowerCase(Locale.ROOT); if ("reload".startsWith(prefix)) { return List.of("reload"); } diff --git a/src/main/java/com/lexon/rtp/command/RtpCommand.java b/src/main/java/com/lexon/rtp/command/RtpCommand.java index 66783c7..dc535b1 100644 --- a/src/main/java/com/lexon/rtp/command/RtpCommand.java +++ b/src/main/java/com/lexon/rtp/command/RtpCommand.java @@ -8,6 +8,7 @@ import org.bukkit.entity.Player; import java.util.List; +import java.util.Locale; public final class RtpCommand extends BaseRtpCommand { public RtpCommand(LexonRTP plugin) { @@ -21,39 +22,56 @@ protected boolean execute(Player player, String[] args) { return true; } if (args.length == 1 && args[0].equalsIgnoreCase("cancel")) { - if (plugin.queue().cancelSoloCountdown(player.getUniqueId())) { + if (plugin.queue().cancelSolo(player.getUniqueId())) { + player.resetTitle(); plugin.messages().send(player, "rtp-cancelled"); } else { plugin.messages().send(player, "rtp-cancel-none"); } return true; } + if (args.length > 2) { + plugin.messages().send(player, "rtp-usage"); + return true; + } if (args.length == 2) { if (!player.hasPermission("lexonrtp.admin")) { plugin.messages().send(player, "no-permission"); return true; } - Player target = Bukkit.getPlayer(args[0]); + Player target = Bukkit.getPlayerExact(args[0]); if (target == null) { plugin.messages().send(player, "player-not-found"); return true; } - plugin.rtpService().request(target, args[1].toLowerCase(), false); + plugin.rtpService().request(target, args[1].toLowerCase(Locale.ROOT), false, player); return true; } - plugin.rtpService().request(player, args[0].toLowerCase(), false); + plugin.rtpService().request(player, args[0].toLowerCase(Locale.ROOT), false); return true; } @Override protected boolean executeConsole(CommandSender sender, String[] args) { - if (args.length >= 2 && sender.hasPermission("lexonrtp.admin")) { - Player target = Bukkit.getPlayer(args[0]); + if (args.length == 0 || (args.length == 1 && args[0].equalsIgnoreCase("cancel"))) { + plugin.messages().send(sender, "rtp-usage"); + return true; + } + if (args.length > 2) { + plugin.messages().send(sender, "rtp-usage"); + return true; + } + if (args.length == 2) { + if (!sender.hasPermission("lexonrtp.admin")) { + plugin.messages().send(sender, "no-permission"); + return true; + } + Player target = Bukkit.getPlayerExact(args[0]); if (target == null) { plugin.messages().send(sender, "player-not-found"); return true; } - plugin.rtpService().request(target, args[1].toLowerCase(), false); + plugin.rtpService().request(target, args[1].toLowerCase(Locale.ROOT), false, sender); return true; } plugin.messages().send(sender, "players-only"); @@ -63,15 +81,15 @@ protected boolean executeConsole(CommandSender sender, String[] args) { @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { if (args.length == 1) { - String prefix = args[0].toLowerCase(); + String prefix = args[0].toLowerCase(Locale.ROOT); List matches = worldKeys(prefix); - if ("cancel".startsWith(prefix)) { + if (!matches.contains("cancel") && "cancel".startsWith(prefix)) { matches.add("cancel"); } return matches; } if (args.length == 2 && sender.hasPermission("lexonrtp.admin")) { - return worldKeys(args[1].toLowerCase()); + return worldKeys(args[1].toLowerCase(Locale.ROOT)); } return List.of(); } diff --git a/src/main/java/com/lexon/rtp/command/RtpQueueCommand.java b/src/main/java/com/lexon/rtp/command/RtpQueueCommand.java index 5a5d70b..ddc9b8d 100644 --- a/src/main/java/com/lexon/rtp/command/RtpQueueCommand.java +++ b/src/main/java/com/lexon/rtp/command/RtpQueueCommand.java @@ -7,6 +7,7 @@ import org.bukkit.entity.Player; import java.util.List; +import java.util.Locale; public final class RtpQueueCommand extends BaseRtpCommand { public RtpQueueCommand(LexonRTP plugin) { @@ -28,14 +29,14 @@ protected boolean execute(Player player, String[] args) { } return true; } - plugin.rtpService().request(player, args[0].toLowerCase(), true); + plugin.rtpService().request(player, args[0].toLowerCase(Locale.ROOT), true); return true; } @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { if (args.length == 1) { - String prefix = args[0].toLowerCase(); + String prefix = args[0].toLowerCase(Locale.ROOT); List matches = worldKeys(prefix); if ("leave".startsWith(prefix)) { matches.add("leave"); diff --git a/src/main/java/com/lexon/rtp/config/ConfigManager.java b/src/main/java/com/lexon/rtp/config/ConfigManager.java index 2a3d26a..d161f1a 100644 --- a/src/main/java/com/lexon/rtp/config/ConfigManager.java +++ b/src/main/java/com/lexon/rtp/config/ConfigManager.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -69,7 +70,7 @@ public void load() { if (s == null) { continue; } - worlds.put(key.toLowerCase(), new WorldSettings( + worlds.put(key.toLowerCase(Locale.ROOT), new WorldSettings( key, s.getString("world-name", key), s.getBoolean("enabled", true), @@ -84,7 +85,7 @@ public void load() { } public WorldSettings getWorld(String key) { - return key == null ? null : worlds.get(key.toLowerCase()); + return key == null ? null : worlds.get(key.toLowerCase(Locale.ROOT)); } public Map getWorlds() { diff --git a/src/main/java/com/lexon/rtp/queue/QueueManager.java b/src/main/java/com/lexon/rtp/queue/QueueManager.java index afeab2c..cfb7fd5 100644 --- a/src/main/java/com/lexon/rtp/queue/QueueManager.java +++ b/src/main/java/com/lexon/rtp/queue/QueueManager.java @@ -6,10 +6,12 @@ import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -52,7 +54,7 @@ public boolean isQueued(UUID uuid) { } public int matchSize(String worldKey) { - QueueState state = matchQueues.get(worldKey.toLowerCase()); + QueueState state = matchQueues.get(worldKey.toLowerCase(Locale.ROOT)); return state == null ? 0 : state.requests.size(); } @@ -75,13 +77,13 @@ public int positionOf(UUID uuid) { return 0; } - public boolean enqueue(Player player, WorldSettings target, boolean matchmaking) { + public boolean enqueue(Player player, WorldSettings target, boolean matchmaking, CommandSender requester) { if (!inQueue.add(player.getUniqueId())) { return false; } - RtpRequest request = new RtpRequest(player.getUniqueId(), target); + RtpRequest request = new RtpRequest(player.getUniqueId(), target, requester); if (matchmaking) { - String key = target.getKey().toLowerCase(); + String key = target.getKey().toLowerCase(Locale.ROOT); QueueState state = matchQueues.computeIfAbsent(key, k -> new QueueState()); state.requests.add(request); playerQueue.put(player.getUniqueId(), key); @@ -121,6 +123,18 @@ public boolean cancelSoloCountdown(UUID uuid) { return cancelled; } + public boolean cancelSolo(UUID uuid) { + if (!inQueue.contains(uuid)) { + return false; + } + String queueKey = playerQueue.get(uuid); + if (queueKey == null || !queueKey.isEmpty()) { + return false; + } + remove(uuid); + return true; + } + private void finish(UUID uuid) { inQueue.remove(uuid); playerQueue.remove(uuid); @@ -181,10 +195,10 @@ private void processSoloRequest(RtpRequest request) { scheduler.entity(player, () -> plugin.messages().send(player, "searching")); plugin.locationFinder().find(target).whenComplete((location, error) -> { if (location == null) { - handleFailure(request.getPlayerId(), target); + handleFailure(request.getPlayerId(), target, request.getRequester()); return; } - startCountdown(request.getPlayerId(), location, target, seconds, false); + startCountdown(request.getPlayerId(), location, target, seconds, false, request.getRequester()); }); } @@ -200,17 +214,20 @@ private void processGroup(List group, int spacing) { plugin.locationFinder().findGroup(target, group.size(), spacing).whenComplete((locations, error) -> { if (locations == null || locations.size() < group.size()) { for (RtpRequest request : group) { - handleFailure(request.getPlayerId(), target); + handleFailure(request.getPlayerId(), target, request.getRequester()); } return; } for (int i = 0; i < group.size(); i++) { - startCountdown(group.get(i).getPlayerId(), locations.get(i), target, seconds, true); + RtpRequest request = group.get(i); + startCountdown(request.getPlayerId(), locations.get(i), target, seconds, true, + request.getRequester()); } }); } - private void startCountdown(UUID uuid, Location location, WorldSettings target, int seconds, boolean matchmaking) { + private void startCountdown(UUID uuid, Location location, WorldSettings target, int seconds, + boolean matchmaking, CommandSender requester) { Player player = Bukkit.getPlayer(uuid); if (player == null || !player.isOnline() || !inQueue.contains(uuid)) { finish(uuid); @@ -220,10 +237,11 @@ private void startCountdown(UUID uuid, Location location, WorldSettings target, if (cancellable) { soloCountdowns.add(uuid); } - scheduler.entity(player, () -> tickCountdown(uuid, location, target, seconds, cancellable)); + scheduler.entity(player, () -> tickCountdown(uuid, location, target, seconds, cancellable, requester)); } - private void tickCountdown(UUID uuid, Location location, WorldSettings target, int remaining, boolean cancellable) { + private void tickCountdown(UUID uuid, Location location, WorldSettings target, int remaining, + boolean cancellable, CommandSender requester) { Player player = Bukkit.getPlayer(uuid); if (player == null || !player.isOnline()) { finish(uuid); @@ -234,17 +252,17 @@ private void tickCountdown(UUID uuid, Location location, WorldSettings target, i } if (remaining <= 0) { soloCountdowns.remove(uuid); - teleport(uuid, location, target); + teleport(uuid, location, target, requester); return; } String title = plugin.messages().get("countdown-title", "%seconds%", String.valueOf(remaining)); String subtitle = plugin.messages().get("countdown-subtitle", "%seconds%", String.valueOf(remaining)); player.sendTitle(title, subtitle, 0, 25, 5); scheduler.entityLater(player, - () -> tickCountdown(uuid, location, target, remaining - 1, cancellable), 20L); + () -> tickCountdown(uuid, location, target, remaining - 1, cancellable, requester), 20L); } - private void teleport(UUID uuid, Location location, WorldSettings target) { + private void teleport(UUID uuid, Location location, WorldSettings target, CommandSender requester) { Player player = Bukkit.getPlayer(uuid); if (player == null || !player.isOnline()) { finish(uuid); @@ -254,15 +272,20 @@ private void teleport(UUID uuid, Location location, WorldSettings target) { if (Boolean.TRUE.equals(success)) { player.resetTitle(); plugin.messages().send(player, "teleport-success"); + if (requester != null && requester != player) { + plugin.messages().send(requester, "admin-rtp-success", + "%player%", player.getName(), + "%world%", target.getWorldName()); + } plugin.redis().setCooldown(uuid, plugin.config().getCooldownSeconds()); finish(uuid); } else { - handleFailure(uuid, target); + handleFailure(uuid, target, requester); } })); } - private void handleFailure(UUID uuid, WorldSettings target) { + private void handleFailure(UUID uuid, WorldSettings target, CommandSender requester) { Player player = Bukkit.getPlayer(uuid); if (player == null) { finish(uuid); @@ -271,6 +294,9 @@ private void handleFailure(UUID uuid, WorldSettings target) { scheduler.entity(player, () -> { player.resetTitle(); plugin.messages().send(player, "teleport-failed"); + if (requester != null && requester != player) { + plugin.messages().send(requester, "admin-rtp-fail", "%player%", player.getName()); + } if (plugin.config().isCooldownOnFail()) { plugin.redis().setCooldown(uuid, plugin.config().getCooldownSeconds()); } diff --git a/src/main/java/com/lexon/rtp/queue/RtpRequest.java b/src/main/java/com/lexon/rtp/queue/RtpRequest.java index 95812e0..5d378f0 100644 --- a/src/main/java/com/lexon/rtp/queue/RtpRequest.java +++ b/src/main/java/com/lexon/rtp/queue/RtpRequest.java @@ -1,16 +1,23 @@ package com.lexon.rtp.queue; import com.lexon.rtp.config.WorldSettings; +import org.bukkit.command.CommandSender; import java.util.UUID; public final class RtpRequest { private final UUID playerId; private final WorldSettings target; + private final CommandSender requester; public RtpRequest(UUID playerId, WorldSettings target) { + this(playerId, target, null); + } + + public RtpRequest(UUID playerId, WorldSettings target, CommandSender requester) { this.playerId = playerId; this.target = target; + this.requester = requester; } public UUID getPlayerId() { @@ -20,4 +27,8 @@ public UUID getPlayerId() { public WorldSettings getTarget() { return target; } + + public CommandSender getRequester() { + return requester; + } } diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 5907d2a..a568db7 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -36,8 +36,11 @@ teleport-failed: "%prefix%&cɢᴜᴠᴇɴʟɪ ʙɪʀ ʏᴇʀ ʙᴜʟᴜɴᴀᴍ moved-cancelled: "%prefix%&cʜᴀʀᴇᴋᴇᴛ ᴇᴛᴛɪɢɪɴ ɪᴄɪɴ ɪsɪɴʟᴀɴᴍᴀ ɪᴘᴛᴀʟ ᴇᴅɪʟᴅɪ." rtp-cancelled: "%prefix%&aɪsɪɴʟᴀɴᴍᴀ ɪᴘᴛᴀʟ ᴇᴅɪʟᴅɪ." rtp-cancel-none: "%prefix%&cɪᴘᴛᴀʟ ᴇᴅɪʟᴇᴄᴇᴋ ʙɪʀ ɪsɪɴʟᴀɴᴍᴀ ʏᴏᴋ." +rtp-usage: "%prefix%&cᴋᴜʟʟᴀɴɪᴍ&8: &7/ʀᴛᴘ [ᴄᴀɴᴄᴇʟ|ᴅᴜɴʏᴀ|<ᴏʏᴜɴᴄᴜ> <ᴅᴜɴʏᴀ>]" player-not-found: "%prefix%&cᴏʏᴜɴᴄᴜ ʙᴜʟᴜɴᴀᴍᴀᴅɪ ᴠᴇʏᴀ ᴏғʟɪɴᴇ." reloaded: "%prefix%&aʏᴀᴘɪʟᴀɴᴅɪʀᴍᴀ ᴠᴇ ᴍᴇsᴀᴊʟᴀʀ ʏᴇɴɪʟᴇɴᴅɪ." +admin-rtp-success: "%prefix%&a%player% &f%world% &7ᴅᴜɴʏᴀsɪɴᴀ ɪsɪɴʟᴀɴᴅɪ." +admin-rtp-fail: "%prefix%&c%player% ɪᴄɪɴ ɪsɪɴʟᴀɴᴍᴀ ʙᴀsᴀʀɪsɪᴢ." lexonrtp-help: - "%prefix%&8» &aʟᴇxᴏɴʀᴛᴘ &7ᴋᴏᴍᴜᴛʟᴀʀɪ" - "&a/lexonrtp reload &8» &7ᴋᴏɴғɪɢ ᴠᴇ ᴍᴇsᴀᴊʟᴀʀɪ ʏᴇɴɪʟᴇ" diff --git a/src/main/resources/messages_en.yml b/src/main/resources/messages_en.yml index e997877..d52c43d 100644 --- a/src/main/resources/messages_en.yml +++ b/src/main/resources/messages_en.yml @@ -36,8 +36,11 @@ teleport-failed: "%prefix%&cᴄᴏᴜʟᴅ ɴᴏᴛ ғɪɴᴅ ᴀ sᴀғᴇ ʟ moved-cancelled: "%prefix%&cᴛᴇʟᴇᴘᴏʀᴛ ᴄᴀɴᴄᴇʟʟᴇᴅ ʙᴇᴄᴀᴜsᴇ ʏᴏᴜ ᴍᴏᴠᴇᴅ." rtp-cancelled: "%prefix%&aᴛᴇʟᴇᴘᴏʀᴛ ᴄᴀɴᴄᴇʟʟᴇᴅ." rtp-cancel-none: "%prefix%&cʏᴏᴜ ʜᴀᴠᴇ ɴᴏ ᴀᴄᴛɪᴠᴇ ᴛᴇʟᴇᴘᴏʀᴛ ᴛᴏ ᴄᴀɴᴄᴇʟ." +rtp-usage: "%prefix%&cᴜsᴀɢᴇ&8: &7/ʀᴛᴘ [ᴄᴀɴᴄᴇʟ|ᴡᴏʀʟᴅ|<ᴘʟᴀʏᴇʀ> <ᴡᴏʀʟᴅ>]" player-not-found: "%prefix%&cᴘʟᴀʏᴇʀ ɴᴏᴛ ғᴏᴜɴᴅ ᴏʀ ᴏғғʟɪɴᴇ." reloaded: "%prefix%&aᴄᴏɴғɪɢᴜʀᴀᴛɪᴏɴ ᴀɴᴅ ᴍᴇssᴀɢᴇs ʀᴇʟᴏᴀᴅᴇᴅ." +admin-rtp-success: "%prefix%&a%player% &7ᴛᴇʟᴇᴘᴏʀᴛᴇᴅ ᴛᴏ &f%world%&7." +admin-rtp-fail: "%prefix%&cᴛᴇʟᴇᴘᴏʀᴛ ғᴀɪʟᴇᴅ ғᴏʀ &f%player%&c." lexonrtp-help: - "%prefix%&8» &aʟᴇxᴏɴʀᴛᴘ &7ᴄᴏᴍᴍᴀɴᴅs" - "&a/lexonrtp reload &8» &7ʀᴇʟᴏᴀᴅ ᴄᴏɴғɪɢ & ᴍᴇssᴀɢᴇs" From 48fbb870ecae6961fa8daf2aeba850114fe57843 Mon Sep 17 00:00:00 2001 From: mozidevv Date: Sat, 1 Aug 2026 19:24:44 +0300 Subject: [PATCH 3/3] Fix cancel race, title reset, redis reload and thread-safety --- src/main/java/com/lexon/rtp/LexonRTP.java | 1 + .../com/lexon/rtp/command/RtpCommand.java | 3 +- .../lexon/rtp/command/RtpQueueCommand.java | 2 +- .../com/lexon/rtp/config/ConfigManager.java | 33 +++++++++-------- .../com/lexon/rtp/config/MessageManager.java | 4 +-- .../com/lexon/rtp/listener/MenuListener.java | 11 +++++- .../com/lexon/rtp/queue/QueueManager.java | 35 +++++++++++++------ .../com/lexon/rtp/redis/RedisManager.java | 24 ++++++++++++- .../com/lexon/rtp/storage/SQLiteStorage.java | 8 ++--- .../java/com/lexon/rtp/util/Scheduler.java | 13 +++++-- src/main/java/com/lexon/rtp/util/Text.java | 6 ++++ 11 files changed, 101 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/lexon/rtp/LexonRTP.java b/src/main/java/com/lexon/rtp/LexonRTP.java index 6622273..208bd96 100644 --- a/src/main/java/com/lexon/rtp/LexonRTP.java +++ b/src/main/java/com/lexon/rtp/LexonRTP.java @@ -108,6 +108,7 @@ public void reloadPlugin() { messageManager.load(); this.locationFinder = new LocationFinder(this); queueManager.start(); + redisManager.reload(); getLogger().info("LexonRTP reloaded."); } } diff --git a/src/main/java/com/lexon/rtp/command/RtpCommand.java b/src/main/java/com/lexon/rtp/command/RtpCommand.java index dc535b1..ae8f04b 100644 --- a/src/main/java/com/lexon/rtp/command/RtpCommand.java +++ b/src/main/java/com/lexon/rtp/command/RtpCommand.java @@ -2,6 +2,7 @@ import com.lexon.rtp.LexonRTP; import com.lexon.rtp.gui.RtpMenu; +import com.lexon.rtp.util.Text; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -23,7 +24,7 @@ protected boolean execute(Player player, String[] args) { } if (args.length == 1 && args[0].equalsIgnoreCase("cancel")) { if (plugin.queue().cancelSolo(player.getUniqueId())) { - player.resetTitle(); + Text.resetTitle(player); plugin.messages().send(player, "rtp-cancelled"); } else { plugin.messages().send(player, "rtp-cancel-none"); diff --git a/src/main/java/com/lexon/rtp/command/RtpQueueCommand.java b/src/main/java/com/lexon/rtp/command/RtpQueueCommand.java index ddc9b8d..43ade9b 100644 --- a/src/main/java/com/lexon/rtp/command/RtpQueueCommand.java +++ b/src/main/java/com/lexon/rtp/command/RtpQueueCommand.java @@ -38,7 +38,7 @@ public List onTabComplete(CommandSender sender, Command command, String if (args.length == 1) { String prefix = args[0].toLowerCase(Locale.ROOT); List matches = worldKeys(prefix); - if ("leave".startsWith(prefix)) { + if (!matches.contains("leave") && "leave".startsWith(prefix)) { matches.add("leave"); } return matches; diff --git a/src/main/java/com/lexon/rtp/config/ConfigManager.java b/src/main/java/com/lexon/rtp/config/ConfigManager.java index d161f1a..16209e8 100644 --- a/src/main/java/com/lexon/rtp/config/ConfigManager.java +++ b/src/main/java/com/lexon/rtp/config/ConfigManager.java @@ -4,7 +4,6 @@ import org.bukkit.Material; import org.bukkit.configuration.ConfigurationSection; -import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.Locale; @@ -14,20 +13,20 @@ public final class ConfigManager { private final LexonRTP plugin; - private String language; - private long cooldownSeconds; - private boolean cooldownOnFail; - private int rtpCountdown; - private boolean queueEnabled; - private int playersPerCycle; - private long cycleIntervalTicks; - private int requiredPlayers; - private int matchCountdown; - private int matchSpacing; - private boolean announceWaiting; - private int maxAttempts; - private int netherScanTop; - private int netherScanBottom; + private volatile String language; + private volatile long cooldownSeconds; + private volatile boolean cooldownOnFail; + private volatile int rtpCountdown; + private volatile boolean queueEnabled; + private volatile int playersPerCycle; + private volatile long cycleIntervalTicks; + private volatile int requiredPlayers; + private volatile int matchCountdown; + private volatile int matchSpacing; + private volatile boolean announceWaiting; + private volatile int maxAttempts; + private volatile int netherScanTop; + private volatile int netherScanBottom; private final Set unsafeBlocks = EnumSet.noneOf(Material.class); private final Map worlds = new LinkedHashMap<>(); @@ -89,7 +88,7 @@ public WorldSettings getWorld(String key) { } public Map getWorlds() { - return Collections.unmodifiableMap(worlds); + return Map.copyOf(worlds); } public String getLanguage() { @@ -149,6 +148,6 @@ public int getNetherScanBottom() { } public Set getUnsafeBlocks() { - return Collections.unmodifiableSet(unsafeBlocks); + return Set.copyOf(unsafeBlocks); } } diff --git a/src/main/java/com/lexon/rtp/config/MessageManager.java b/src/main/java/com/lexon/rtp/config/MessageManager.java index 50cbebc..6648207 100644 --- a/src/main/java/com/lexon/rtp/config/MessageManager.java +++ b/src/main/java/com/lexon/rtp/config/MessageManager.java @@ -16,8 +16,8 @@ public final class MessageManager { private final LexonRTP plugin; - private FileConfiguration messages; - private String prefix = ""; + private volatile FileConfiguration messages; + private volatile String prefix = ""; public MessageManager(LexonRTP plugin) { this.plugin = plugin; diff --git a/src/main/java/com/lexon/rtp/listener/MenuListener.java b/src/main/java/com/lexon/rtp/listener/MenuListener.java index 0b19858..80e6acb 100644 --- a/src/main/java/com/lexon/rtp/listener/MenuListener.java +++ b/src/main/java/com/lexon/rtp/listener/MenuListener.java @@ -4,11 +4,13 @@ import com.lexon.rtp.gui.GuiMenu; import com.lexon.rtp.gui.QueueMenu; import com.lexon.rtp.gui.RtpMenu; +import com.lexon.rtp.util.Text; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.Inventory; @@ -46,6 +48,13 @@ private void handleClick(InventoryClickEvent event, GuiMenu menu, boolean matchm } } + @EventHandler + public void onDrag(InventoryDragEvent event) { + if (event.getInventory().getHolder() instanceof GuiMenu) { + event.setCancelled(true); + } + } + @EventHandler public void onQuit(PlayerQuitEvent event) { plugin.queue().remove(event.getPlayer().getUniqueId()); @@ -64,7 +73,7 @@ public void onMove(PlayerMoveEvent event) { return; } if (plugin.queue().cancelSoloCountdown(event.getPlayer().getUniqueId())) { - event.getPlayer().resetTitle(); + Text.resetTitle(event.getPlayer()); plugin.messages().send(event.getPlayer(), "moved-cancelled"); } } diff --git a/src/main/java/com/lexon/rtp/queue/QueueManager.java b/src/main/java/com/lexon/rtp/queue/QueueManager.java index cfb7fd5..d8a22a1 100644 --- a/src/main/java/com/lexon/rtp/queue/QueueManager.java +++ b/src/main/java/com/lexon/rtp/queue/QueueManager.java @@ -3,6 +3,7 @@ import com.lexon.rtp.LexonRTP; import com.lexon.rtp.config.WorldSettings; import com.lexon.rtp.util.Scheduler; +import com.lexon.rtp.util.Text; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -26,6 +27,7 @@ public final class QueueManager { private final Map playerQueue = new ConcurrentHashMap<>(); private final Set inQueue = ConcurrentHashMap.newKeySet(); private final Set soloCountdowns = ConcurrentHashMap.newKeySet(); + private final Map countdownEpoch = new ConcurrentHashMap<>(); private ScheduledTask task; public QueueManager(LexonRTP plugin) { @@ -97,11 +99,13 @@ public boolean enqueue(Player player, WorldSettings target, boolean matchmaking, public void remove(UUID uuid) { if (!inQueue.remove(uuid)) { soloCountdowns.remove(uuid); + countdownEpoch.remove(uuid); return; } String queueKey = playerQueue.remove(uuid); if (queueKey == null) { soloCountdowns.remove(uuid); + countdownEpoch.remove(uuid); return; } if (queueKey.isEmpty()) { @@ -113,6 +117,7 @@ public void remove(UUID uuid) { } } soloCountdowns.remove(uuid); + countdownEpoch.remove(uuid); } public boolean cancelSoloCountdown(UUID uuid) { @@ -139,6 +144,11 @@ private void finish(UUID uuid) { inQueue.remove(uuid); playerQueue.remove(uuid); soloCountdowns.remove(uuid); + countdownEpoch.remove(uuid); + } + + private long startEpoch(UUID uuid) { + return countdownEpoch.compute(uuid, (k, v) -> v == null ? 1L : v + 1L); } private void processCycle() { @@ -228,25 +238,30 @@ private void processGroup(List group, int spacing) { private void startCountdown(UUID uuid, Location location, WorldSettings target, int seconds, boolean matchmaking, CommandSender requester) { + boolean cancellable = !matchmaking && seconds > 0; + if (cancellable) { + soloCountdowns.add(uuid); + } + long epoch = startEpoch(uuid); Player player = Bukkit.getPlayer(uuid); if (player == null || !player.isOnline() || !inQueue.contains(uuid)) { finish(uuid); return; } - boolean cancellable = !matchmaking && seconds > 0; - if (cancellable) { - soloCountdowns.add(uuid); - } - scheduler.entity(player, () -> tickCountdown(uuid, location, target, seconds, cancellable, requester)); + scheduler.entity(player, + () -> tickCountdown(uuid, location, target, seconds, cancellable, requester, epoch)); } private void tickCountdown(UUID uuid, Location location, WorldSettings target, int remaining, - boolean cancellable, CommandSender requester) { + boolean cancellable, CommandSender requester, long epoch) { Player player = Bukkit.getPlayer(uuid); if (player == null || !player.isOnline()) { finish(uuid); return; } + if (countdownEpoch.getOrDefault(uuid, -1L) != epoch) { + return; + } if (cancellable && !soloCountdowns.contains(uuid)) { return; } @@ -259,7 +274,7 @@ private void tickCountdown(UUID uuid, Location location, WorldSettings target, i String subtitle = plugin.messages().get("countdown-subtitle", "%seconds%", String.valueOf(remaining)); player.sendTitle(title, subtitle, 0, 25, 5); scheduler.entityLater(player, - () -> tickCountdown(uuid, location, target, remaining - 1, cancellable, requester), 20L); + () -> tickCountdown(uuid, location, target, remaining - 1, cancellable, requester, epoch), 20L); } private void teleport(UUID uuid, Location location, WorldSettings target, CommandSender requester) { @@ -270,7 +285,7 @@ private void teleport(UUID uuid, Location location, WorldSettings target, Comman } player.teleportAsync(location).whenComplete((success, error) -> scheduler.entity(player, () -> { if (Boolean.TRUE.equals(success)) { - player.resetTitle(); + Text.resetTitle(player); plugin.messages().send(player, "teleport-success"); if (requester != null && requester != player) { plugin.messages().send(requester, "admin-rtp-success", @@ -287,12 +302,12 @@ private void teleport(UUID uuid, Location location, WorldSettings target, Comman private void handleFailure(UUID uuid, WorldSettings target, CommandSender requester) { Player player = Bukkit.getPlayer(uuid); - if (player == null) { + if (player == null || !inQueue.contains(uuid)) { finish(uuid); return; } scheduler.entity(player, () -> { - player.resetTitle(); + Text.resetTitle(player); plugin.messages().send(player, "teleport-failed"); if (requester != null && requester != player) { plugin.messages().send(requester, "admin-rtp-fail", "%player%", player.getName()); diff --git a/src/main/java/com/lexon/rtp/redis/RedisManager.java b/src/main/java/com/lexon/rtp/redis/RedisManager.java index d5bde21..5d4b69d 100644 --- a/src/main/java/com/lexon/rtp/redis/RedisManager.java +++ b/src/main/java/com/lexon/rtp/redis/RedisManager.java @@ -2,6 +2,7 @@ import com.lexon.rtp.LexonRTP; import com.lexon.rtp.storage.SQLiteStorage; +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; @@ -17,6 +18,7 @@ public final class RedisManager { private final SQLiteStorage sqlite; private String keyPrefix = "lexonrtp:"; private JedisPool pool; + private ScheduledTask reconnectTask; private volatile boolean enabled; private volatile boolean connected; @@ -71,7 +73,13 @@ public void startReconnect() { if (!enabled) { return; } - plugin.scheduler().globalTimer(this::ensureConnected, RECONNECT_TICKS, RECONNECT_TICKS); + if (reconnectTask != null) { + try { + reconnectTask.cancel(); + } catch (Throwable ignored) { + } + } + reconnectTask = plugin.scheduler().globalTimer(this::ensureConnected, RECONNECT_TICKS, RECONNECT_TICKS); } private void ensureConnected() { @@ -180,13 +188,27 @@ public void removePendingRtp(UUID uuid) { } public void close() { + if (reconnectTask != null) { + try { + reconnectTask.cancel(); + } catch (Throwable ignored) { + } + reconnectTask = null; + } if (pool != null) { try { pool.close(); } catch (Exception ignored) { } + pool = null; } sqlite.close(); connected = false; } + + public void reload() { + close(); + connect(); + startReconnect(); + } } diff --git a/src/main/java/com/lexon/rtp/storage/SQLiteStorage.java b/src/main/java/com/lexon/rtp/storage/SQLiteStorage.java index c33d823..8a345da 100644 --- a/src/main/java/com/lexon/rtp/storage/SQLiteStorage.java +++ b/src/main/java/com/lexon/rtp/storage/SQLiteStorage.java @@ -13,10 +13,10 @@ public final class SQLiteStorage { private final LexonRTP plugin; - private Connection connection; - private PreparedStatement select; - private PreparedStatement insert; - private PreparedStatement delete; + private volatile Connection connection; + private volatile PreparedStatement select; + private volatile PreparedStatement insert; + private volatile PreparedStatement delete; public SQLiteStorage(LexonRTP plugin) { this.plugin = plugin; diff --git a/src/main/java/com/lexon/rtp/util/Scheduler.java b/src/main/java/com/lexon/rtp/util/Scheduler.java index 83f6beb..bef6480 100644 --- a/src/main/java/com/lexon/rtp/util/Scheduler.java +++ b/src/main/java/com/lexon/rtp/util/Scheduler.java @@ -43,11 +43,20 @@ public void region(Location location, Runnable task) { } public void entity(Entity entity, Runnable task) { - entity.getScheduler().run(plugin, t -> task.run(), null); + try { + entity.getScheduler().run(plugin, t -> task.run(), null); + } catch (IllegalStateException ex) { + Bukkit.getGlobalRegionScheduler().run(plugin, t -> task.run()); + } } public void entityLater(Entity entity, Runnable task, long delayTicks) { - entity.getScheduler().runDelayed(plugin, t -> task.run(), null, Math.max(1L, delayTicks)); + long delay = Math.max(1L, delayTicks); + try { + entity.getScheduler().runDelayed(plugin, t -> task.run(), null, delay); + } catch (IllegalStateException ex) { + Bukkit.getGlobalRegionScheduler().runDelayed(plugin, t -> task.run(), delay); + } } public void shutdown() { diff --git a/src/main/java/com/lexon/rtp/util/Text.java b/src/main/java/com/lexon/rtp/util/Text.java index 813fce9..c236f39 100644 --- a/src/main/java/com/lexon/rtp/util/Text.java +++ b/src/main/java/com/lexon/rtp/util/Text.java @@ -1,11 +1,17 @@ package com.lexon.rtp.util; import net.md_5.bungee.api.ChatColor; +import org.bukkit.entity.Player; public final class Text { private Text() { } + public static void resetTitle(Player player) { + player.resetTitle(); + player.sendTitle("", "", 0, 0, 0); + } + public static String color(String input) { if (input == null || input.isEmpty()) { return "";