From 7a4bb7ab3832e6275b409df6c3eb0c186df288aa Mon Sep 17 00:00:00 2001 From: Lured Date: Tue, 3 Jun 2025 09:09:54 -0400 Subject: [PATCH 1/7] Add chat filtering, duplicate message detection, stats command, and fix damage cap --- .../blbilink/blbilogin/load/LoadFunction.java | 2 + .../modules/commands/StatsCommand.java | 16 +++++ .../blbilogin/modules/events/ChatFilter.java | 65 +++++++++++++++++++ .../antiillegal/listeners/AttackListener.java | 7 +- src/main/resources/plugin.yml | 2 + 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/blbilink/blbilogin/modules/commands/StatsCommand.java create mode 100644 src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java diff --git a/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java b/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java index 2403607..3b7dda1 100644 --- a/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java +++ b/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java @@ -38,6 +38,7 @@ private void loadCommands(){ Objects.requireNonNull(plugin.getCommand("kill")).setExecutor(new KillCommand()); Objects.requireNonNull(plugin.getCommand("worldstats")).setExecutor(new WorldStatsCommand()); Objects.requireNonNull(plugin.getCommand("info")).setExecutor(new InfoCommand()); + Objects.requireNonNull(plugin.getCommand("stats")).setExecutor(new StatsCommand()); Objects.requireNonNull(plugin.getCommand("dupe")).setExecutor(new DupeCommand()); Objects.requireNonNull(plugin.getCommand("tpa")).setExecutor(new TpaCommand()); Objects.requireNonNull(plugin.getCommand("tphere")).setExecutor(new TphereCommand()); @@ -55,6 +56,7 @@ private void loadListeners(){ Bukkit.getPluginManager().registerEvents(new com.blbilink.blbilogin.modules.dupe.ChestBoatDupeListener(0), plugin); Bukkit.getPluginManager().registerEvents(new com.blbilink.blbilogin.modules.events.SpamProtection(), plugin); Bukkit.getPluginManager().registerEvents(new com.blbilink.blbilogin.modules.events.WitherSkullExplodeFix(), plugin); + Bukkit.getPluginManager().registerEvents(new com.blbilink.blbilogin.modules.events.ChatFilter(), plugin); } private void loadSqlite(){ diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/StatsCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/StatsCommand.java new file mode 100644 index 0000000..681fda4 --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/StatsCommand.java @@ -0,0 +1,16 @@ +package com.blbilink.blbilogin.modules.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +/** + * Alias for {@link WorldStatsCommand} providing the /stats command. + */ +public class StatsCommand extends WorldStatsCommand implements CommandExecutor { + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + return super.onCommand(sender, command, label, args); + } +} diff --git a/src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java b/src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java new file mode 100644 index 0000000..0666df7 --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java @@ -0,0 +1,65 @@ +package com.blbilink.blbilogin.modules.events; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ChatFilter implements Listener { + private static final String[] BLOCKED_TLDS = {".com", ".org", ".cc", ".net"}; + private final Map lastMessages = new HashMap<>(); + + @EventHandler + public void onChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + String message = event.getMessage(); + String lower = message.toLowerCase(); + + // Block links containing specific TLDs + for (String tld : BLOCKED_TLDS) { + if (lower.contains(tld)) { + event.setCancelled(true); + player.sendMessage("Links are not allowed in chat."); + return; + } + } + + // Duplicate message detection with basic similarity check + String sanitized = lower.replaceAll("[^a-z0-9]", ""); + String previous = lastMessages.get(player.getUniqueId()); + if (previous != null && isSimilar(sanitized, previous)) { + event.setCancelled(true); + player.sendMessage("Please avoid sending duplicate messages."); + return; + } + + lastMessages.put(player.getUniqueId(), sanitized); + } + + private boolean isSimilar(String a, String b) { + if (a.equals(b)) return true; + return levenshteinDistance(a, b) <= 2; + } + + private int levenshteinDistance(String a, String b) { + int[] costs = new int[b.length() + 1]; + for (int j = 0; j < costs.length; j++) { + costs[j] = j; + } + for (int i = 1; i <= a.length(); i++) { + costs[0] = i; + int nw = i - 1; + for (int j = 1; j <= b.length(); j++) { + int cj = Math.min(1 + Math.min(costs[j], costs[j - 1]), + a.charAt(i - 1) == b.charAt(j - 1) ? nw : nw + 1); + nw = costs[j]; + costs[j] = cj; + } + } + return costs[b.length()]; + } +} diff --git a/src/main/java/me/txmc/core/antiillegal/listeners/AttackListener.java b/src/main/java/me/txmc/core/antiillegal/listeners/AttackListener.java index c693381..dca48c5 100644 --- a/src/main/java/me/txmc/core/antiillegal/listeners/AttackListener.java +++ b/src/main/java/me/txmc/core/antiillegal/listeners/AttackListener.java @@ -14,8 +14,11 @@ public class AttackListener implements Listener { public void onAttack(EntityDamageByEntityEvent event) { if (!(event.getDamager() instanceof Player)) return; if (event.getDamage() > 30D) { - event.setCancelled(true); - ((Player) event.getDamager()).damage(event.getDamage()); + // Limit damage to prevent 32k weapons from one-shotting players. + // Cancelling the event and damaging the attacker could lead to + // inconsistent behaviour when other plugins listen to the same + // event. Simply cap the damage to a safe value instead. + event.setDamage(30D); } } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 71fffe5..dba583f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -31,6 +31,8 @@ commands: description: "Show world statistics" info: description: "Alias of worldstats" + stats: + description: "Alias of worldstats" dupe: description: "Explains the chest boat dupe" usage: /dupe From ddf2ea919ba7246e7d7ee64d6320d2cf93f6b2df Mon Sep 17 00:00:00 2001 From: Lured Date: Tue, 3 Jun 2025 11:42:41 -0400 Subject: [PATCH 2/7] Extend chat filter --- .../blbilink/blbilogin/modules/events/ChatFilter.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java b/src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java index 0666df7..1da2d5b 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java +++ b/src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java @@ -11,6 +11,8 @@ public class ChatFilter implements Listener { private static final String[] BLOCKED_TLDS = {".com", ".org", ".cc", ".net"}; + private static final String[] BLOCKED_WORDS = {"jonarchy"}; + private final Map lastMessages = new HashMap<>(); @EventHandler @@ -28,6 +30,15 @@ public void onChat(AsyncPlayerChatEvent event) { } } + // Block specific offensive words + for (String word : BLOCKED_WORDS) { + if (lower.contains(word)) { + event.setCancelled(true); + player.sendMessage("That word is not allowed in chat."); + return; + } + } + // Duplicate message detection with basic similarity check String sanitized = lower.replaceAll("[^a-z0-9]", ""); String previous = lastMessages.get(player.getUniqueId()); From 8aaeaa2fe699f237202d2cd94d28d033bd6defee Mon Sep 17 00:00:00 2001 From: Lured Date: Tue, 3 Jun 2025 11:42:46 -0400 Subject: [PATCH 3/7] Add teleport and player info commands --- .../com/blbilink/blbilogin/BlbiLogin.java | 2 + .../blbilink/blbilogin/load/LoadFunction.java | 6 + .../blbilink/blbilogin/modules/Sqlite.java | 142 ++++++++++++++++++ .../modules/commands/BlacklistCommand.java | 32 ++++ .../modules/commands/InfoCommand.java | 48 +++++- .../modules/commands/JoinDateCommand.java | 29 ++++ .../modules/commands/ListPlayersCommand.java | 33 ++++ .../blbilogin/modules/commands/Login.java | 5 + .../modules/commands/PlayTimeCommand.java | 26 ++++ .../blbilogin/modules/commands/Register.java | 5 + .../modules/commands/TpOfflineCommand.java | 39 +++++ .../modules/commands/TpacancelCommand.java | 23 +++ .../blbilogin/modules/events/PlayerJoin.java | 8 + .../modules/messages/PlayerSender.java | 1 + .../teleport/TeleportRequestManager.java | 5 + src/main/resources/plugin.yml | 20 ++- 16 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/blbilink/blbilogin/modules/commands/BlacklistCommand.java create mode 100644 src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java create mode 100644 src/main/java/com/blbilink/blbilogin/modules/commands/ListPlayersCommand.java create mode 100644 src/main/java/com/blbilink/blbilogin/modules/commands/PlayTimeCommand.java create mode 100644 src/main/java/com/blbilink/blbilogin/modules/commands/TpOfflineCommand.java create mode 100644 src/main/java/com/blbilink/blbilogin/modules/commands/TpacancelCommand.java diff --git a/src/main/java/com/blbilink/blbilogin/BlbiLogin.java b/src/main/java/com/blbilink/blbilogin/BlbiLogin.java index 869bdc6..99947d3 100644 --- a/src/main/java/com/blbilink/blbilogin/BlbiLogin.java +++ b/src/main/java/com/blbilink/blbilogin/BlbiLogin.java @@ -24,6 +24,7 @@ public final class BlbiLogin extends JavaPlugin implements Listener { public static BlbiLogin plugin; + public static long serverStartTime; public I18n i18n; public ConfigUtil config; public FoliaUtil foliaUtil; @@ -31,6 +32,7 @@ public final class BlbiLogin extends JavaPlugin implements Listener { @Override public void onEnable() { plugin = this; + serverStartTime = System.currentTimeMillis(); // Initialize plugin LoadConfig.loadConfig(this); diff --git a/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java b/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java index 3b7dda1..e8e73f4 100644 --- a/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java +++ b/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java @@ -43,6 +43,12 @@ private void loadCommands(){ Objects.requireNonNull(plugin.getCommand("tpa")).setExecutor(new TpaCommand()); Objects.requireNonNull(plugin.getCommand("tphere")).setExecutor(new TphereCommand()); Objects.requireNonNull(plugin.getCommand("tpaccept")).setExecutor(new TpacceptCommand()); + Objects.requireNonNull(plugin.getCommand("tpacancel")).setExecutor(new TpacancelCommand()); + Objects.requireNonNull(plugin.getCommand("joindate")).setExecutor(new JoinDateCommand()); + Objects.requireNonNull(plugin.getCommand("playtime")).setExecutor(new PlayTimeCommand()); + Objects.requireNonNull(plugin.getCommand("listplayers")).setExecutor(new ListPlayersCommand()); + Objects.requireNonNull(plugin.getCommand("tpoffline")).setExecutor(new TpOfflineCommand()); + Objects.requireNonNull(plugin.getCommand("blacklist")).setExecutor(new BlacklistCommand()); } private void loadListeners(){ // Register Bukkit event listeners diff --git a/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java b/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java index 927344f..35250ed 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java +++ b/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java @@ -22,6 +22,21 @@ public Sqlite() { try (PreparedStatement statement = connection.prepareStatement("CREATE TABLE IF NOT EXISTS players (uuid TEXT PRIMARY KEY, username TEXT, password TEXT)")) { statement.executeUpdate(); } + try (PreparedStatement statement = connection.prepareStatement( + "CREATE TABLE IF NOT EXISTS playerdata (" + + "uuid TEXT PRIMARY KEY," + + "firstJoin INTEGER," + + "joinCount INTEGER," + + "logoutWorld TEXT," + + "logoutX REAL," + + "logoutY REAL," + + "logoutZ REAL," + + "logoutYaw REAL," + + "logoutPitch REAL," + + "blacklist INTEGER DEFAULT 0" + + ")")) { + statement.executeUpdate(); + } } catch (SQLException e) { e.printStackTrace(); } @@ -73,4 +88,131 @@ public boolean resetPassword(String uuid, String newPassword) { } return false; } + + public void recordJoin(String uuid) { + try (PreparedStatement select = connection.prepareStatement("SELECT joinCount FROM playerdata WHERE uuid = ?")) { + select.setString(1, uuid); + ResultSet rs = select.executeQuery(); + if (rs.next()) { + int count = rs.getInt("joinCount") + 1; + try (PreparedStatement update = connection.prepareStatement("UPDATE playerdata SET joinCount = ? WHERE uuid = ?")) { + update.setInt(1, count); + update.setString(2, uuid); + update.executeUpdate(); + } + } else { + try (PreparedStatement insert = connection.prepareStatement("INSERT INTO playerdata (uuid, firstJoin, joinCount) VALUES (?, ?, 1)")) { + insert.setString(1, uuid); + insert.setLong(2, System.currentTimeMillis()); + insert.executeUpdate(); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public int getJoinCount(String uuid) { + try (PreparedStatement statement = connection.prepareStatement("SELECT joinCount FROM playerdata WHERE uuid = ?")) { + statement.setString(1, uuid); + ResultSet rs = statement.executeQuery(); + if (rs.next()) { + return rs.getInt("joinCount"); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return 0; + } + + public Long getFirstJoin(String uuid) { + try (PreparedStatement statement = connection.prepareStatement("SELECT firstJoin FROM playerdata WHERE uuid = ?")) { + statement.setString(1, uuid); + ResultSet rs = statement.executeQuery(); + if (rs.next()) { + return rs.getLong("firstJoin"); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void setLogoutLocation(String uuid, org.bukkit.Location loc) { + try (PreparedStatement statement = connection.prepareStatement( + "INSERT INTO playerdata (uuid, logoutWorld, logoutX, logoutY, logoutZ, logoutYaw, logoutPitch) " + + "VALUES (?, ?, ?, ?, ?, ?, ?) " + + "ON CONFLICT(uuid) DO UPDATE SET logoutWorld=excluded.logoutWorld, logoutX=excluded.logoutX, " + + "logoutY=excluded.logoutY, logoutZ=excluded.logoutZ, logoutYaw=excluded.logoutYaw, logoutPitch=excluded.logoutPitch")) { + statement.setString(1, uuid); + statement.setString(2, loc.getWorld().getName()); + statement.setDouble(3, loc.getX()); + statement.setDouble(4, loc.getY()); + statement.setDouble(5, loc.getZ()); + statement.setFloat(6, loc.getYaw()); + statement.setFloat(7, loc.getPitch()); + statement.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public org.bukkit.Location getLogoutLocation(String uuid) { + try (PreparedStatement statement = connection.prepareStatement("SELECT logoutWorld, logoutX, logoutY, logoutZ, logoutYaw, logoutPitch FROM playerdata WHERE uuid = ?")) { + statement.setString(1, uuid); + ResultSet rs = statement.executeQuery(); + if (rs.next() && rs.getString("logoutWorld") != null) { + org.bukkit.World world = org.bukkit.Bukkit.getWorld(rs.getString("logoutWorld")); + if (world != null) { + return new org.bukkit.Location(world, + rs.getDouble("logoutX"), + rs.getDouble("logoutY"), + rs.getDouble("logoutZ"), + rs.getFloat("logoutYaw"), + rs.getFloat("logoutPitch")); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public boolean isBlacklisted(String uuid) { + try (PreparedStatement statement = connection.prepareStatement("SELECT blacklist FROM playerdata WHERE uuid = ?")) { + statement.setString(1, uuid); + ResultSet rs = statement.executeQuery(); + if (rs.next()) { + return rs.getInt("blacklist") == 1; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public void setBlacklisted(String uuid, boolean value) { + try (PreparedStatement statement = connection.prepareStatement( + "INSERT INTO playerdata (uuid, blacklist) VALUES (?, ?) " + + "ON CONFLICT(uuid) DO UPDATE SET blacklist=excluded.blacklist")) { + statement.setString(1, uuid); + statement.setInt(2, value ? 1 : 0); + statement.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public java.util.List listUsernames() { + java.util.List list = new java.util.ArrayList<>(); + try (PreparedStatement statement = connection.prepareStatement("SELECT username FROM players ORDER BY username ASC")) { + ResultSet rs = statement.executeQuery(); + while (rs.next()) { + list.add(rs.getString("username")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } } \ No newline at end of file diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/BlacklistCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/BlacklistCommand.java new file mode 100644 index 0000000..af4b9cf --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/BlacklistCommand.java @@ -0,0 +1,32 @@ +package com.blbilink.blbilogin.modules.commands; + +import com.blbilink.blbilogin.modules.Sqlite; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + + +public class BlacklistCommand implements CommandExecutor { + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player player) || !player.isOp()) { + sender.sendMessage("Only operators can use this command"); + return true; + } + if (args.length != 1) { + player.sendMessage("/blacklist "); + return true; + } + OfflinePlayer target = Bukkit.getOfflinePlayer(args[0]); + Sqlite.getSqlite().setBlacklisted(target.getUniqueId().toString(), true); + if (target.isOnline()) { + ((Player) target).kickPlayer("Disconnected: Internal server error"); + } + player.sendMessage(target.getName() + " has been blacklisted"); + return true; + } +} diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/InfoCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/InfoCommand.java index 44f5335..64267b0 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/commands/InfoCommand.java +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/InfoCommand.java @@ -1,13 +1,57 @@ package com.blbilink.blbilogin.modules.commands; +import com.blbilink.blbilogin.BlbiLogin; +import com.blbilink.blbilogin.modules.Sqlite; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; -public class InfoCommand extends WorldStatsCommand implements CommandExecutor { +import java.lang.management.ManagementFactory; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + + +public class InfoCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - return super.onCommand(sender, command, label, args); + if (args.length != 1) { + sender.sendMessage("/info "); + return true; + } + OfflinePlayer target = Bukkit.getOfflinePlayer(args[0]); + if (!target.hasPlayedBefore() && !target.isOnline()) { + sender.sendMessage("Player not found"); + return true; + } + String uuid = target.getUniqueId().toString(); + int joins = Sqlite.getSqlite().getJoinCount(uuid); + if (joins == 0) { + joins = target.getStatistic(Statistic.LEAVE_GAME) + 1; + } + long serverUptime = System.currentTimeMillis() - BlbiLogin.serverStartTime; + long osUptime = System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().getStartTime(); + Long first = Sqlite.getSqlite().getFirstJoin(uuid); + if (first == null) first = target.getFirstPlayed(); + String firstJoined = first > 0 ? DateTimeFormatter.ISO_LOCAL_DATE.format(Instant.ofEpochMilli(first)) : "Unknown"; + String msg = String.format("%s has joined %d times. First join: %s", target.getName(), joins, firstJoined); + String serverUp = formatDuration(serverUptime); + String osUp = formatDuration(osUptime); + sender.sendMessage(msg); + sender.sendMessage("Server uptime: " + serverUp); + sender.sendMessage("System uptime: " + osUp); + return true; + } + + private String formatDuration(long millis) { + Duration d = Duration.ofMillis(millis); + long days = d.toDays(); + long hours = d.minusDays(days).toHours(); + long minutes = d.minusDays(days).minusHours(hours).toMinutes(); + return days + "d " + hours + "h " + minutes + "m"; } } diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java new file mode 100644 index 0000000..b48eb34 --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java @@ -0,0 +1,29 @@ +package com.blbilink.blbilogin.modules.commands; + +import com.blbilink.blbilogin.modules.Sqlite; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +public class JoinDateCommand implements CommandExecutor { + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + String targetName = args.length > 0 ? args[0] : sender.getName(); + OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); + if (!target.hasPlayedBefore() && !target.isOnline()) { + sender.sendMessage("Player not found"); + return true; + } + Long ts = Sqlite.getSqlite().getFirstJoin(target.getUniqueId().toString()); + if (ts == null) ts = target.getFirstPlayed(); + String date = ts > 0 ? DateTimeFormatter.ISO_LOCAL_DATE.format(Instant.ofEpochMilli(ts)) : "Unknown"; + sender.sendMessage(target.getName() + " first joined on " + date); + return true; + } +} diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/ListPlayersCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/ListPlayersCommand.java new file mode 100644 index 0000000..b7bf0e5 --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/ListPlayersCommand.java @@ -0,0 +1,33 @@ +package com.blbilink.blbilogin.modules.commands; + +import com.blbilink.blbilogin.modules.Sqlite; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class ListPlayersCommand implements CommandExecutor { + private static final int PAGE_SIZE = 5; + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + int page = 1; + if (args.length == 1) { + try { page = Integer.parseInt(args[0]); } catch (NumberFormatException ignore) {} + } + List users = Sqlite.getSqlite().listUsernames(); + int maxPage = (int) Math.ceil(users.size() / (double) PAGE_SIZE); + if (page < 1 || page > maxPage) { + sender.sendMessage("Page out of range. Max " + maxPage); + return true; + } + sender.sendMessage("Users page " + page + "/" + maxPage + ":"); + int start = (page - 1) * PAGE_SIZE; + for (int i = start; i < start + PAGE_SIZE && i < users.size(); i++) { + sender.sendMessage("- " + users.get(i)); + } + return true; + } +} diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/Login.java b/src/main/java/com/blbilink/blbilogin/modules/commands/Login.java index 7c7f6aa..e9fb826 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/commands/Login.java +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/Login.java @@ -34,6 +34,11 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } + if (Sqlite.getSqlite().isBlacklisted(uuid)) { + player.kickPlayer("Disconnected: Internal server error"); + return true; + } + if (login.isCorrect(uuid, password)) { login.loginSuccess(player); return true; diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/PlayTimeCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/PlayTimeCommand.java new file mode 100644 index 0000000..7e38a02 --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/PlayTimeCommand.java @@ -0,0 +1,26 @@ +package com.blbilink.blbilogin.modules.commands; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class PlayTimeCommand implements CommandExecutor { + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + String targetName = args.length > 0 ? args[0] : sender.getName(); + OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); + if (!target.hasPlayedBefore() && !target.isOnline()) { + sender.sendMessage("Player not found"); + return true; + } + long ticks = target.getStatistic(Statistic.PLAY_ONE_MINUTE); + long hours = ticks / 20 / 3600; + long minutes = (ticks / 20 / 60) % 60; + sender.sendMessage(target.getName() + " playtime: " + hours + "h " + minutes + "m"); + return true; + } +} diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/Register.java b/src/main/java/com/blbilink/blbilogin/modules/commands/Register.java index 6590cc1..1573ca2 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/commands/Register.java +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/Register.java @@ -28,6 +28,11 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } + if (Sqlite.getSqlite().isBlacklisted(uuid)) { + player.kickPlayer("Disconnected: Internal server error"); + return true; + } + Sqlite.getSqlite().registerPlayer(uuid, player.getName(), password); player.sendMessage(plugin.i18n.as("msgRegisterSuccess",true,player.getName())); return true; diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/TpOfflineCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/TpOfflineCommand.java new file mode 100644 index 0000000..f91636b --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/TpOfflineCommand.java @@ -0,0 +1,39 @@ +package com.blbilink.blbilogin.modules.commands; + +import com.blbilink.blbilogin.modules.Sqlite; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import static com.blbilink.blbilogin.BlbiLogin.plugin; + +public class TpOfflineCommand implements CommandExecutor { + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player player) || !player.isOp()) { + sender.sendMessage("Only operators can use this command"); + return true; + } + if (args.length != 1) { + player.sendMessage("/tpoffline "); + return true; + } + OfflinePlayer target = Bukkit.getOfflinePlayer(args[0]); + Location loc = Sqlite.getSqlite().getLogoutLocation(target.getUniqueId().toString()); + if (loc == null) { + player.sendMessage("No logout location for that player"); + return true; + } + if (plugin.foliaUtil.isFolia) { + player.getScheduler().run(plugin, task -> player.teleportAsync(loc), () -> {}); + } else { + plugin.foliaUtil.runTask(plugin, t -> player.teleport(loc)); + } + return true; + } +} diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/TpacancelCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/TpacancelCommand.java new file mode 100644 index 0000000..4d7e9bb --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/TpacancelCommand.java @@ -0,0 +1,23 @@ +package com.blbilink.blbilogin.modules.commands; + +import com.blbilink.blbilogin.modules.teleport.TeleportRequestManager; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import static com.blbilink.blbilogin.BlbiLogin.plugin; + +public class TpacancelCommand implements CommandExecutor { + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Only players can use this command"); + return true; + } + TeleportRequestManager.cancelRequests(player.getUniqueId()); + player.sendMessage("Teleport requests cancelled"); + return true; + } +} diff --git a/src/main/java/com/blbilink/blbilogin/modules/events/PlayerJoin.java b/src/main/java/com/blbilink/blbilogin/modules/events/PlayerJoin.java index 02008c5..e723e13 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/events/PlayerJoin.java +++ b/src/main/java/com/blbilink/blbilogin/modules/events/PlayerJoin.java @@ -28,6 +28,14 @@ public PlayerJoin(BlbiLogin plugin){ public void onPlayerJoin(PlayerJoinEvent ev) { Player e = ev.getPlayer(); + String uuid = e.getUniqueId().toString(); + if (com.blbilink.blbilogin.modules.Sqlite.getSqlite().isBlacklisted(uuid)) { + e.kickPlayer("Disconnected: Internal server error"); + return; + } + + com.blbilink.blbilogin.modules.Sqlite.getSqlite().recordJoin(uuid); + // Store original location before any teleportation Configvar.originalLocation.put(e.getName(), e.getLocation()); diff --git a/src/main/java/com/blbilink/blbilogin/modules/messages/PlayerSender.java b/src/main/java/com/blbilink/blbilogin/modules/messages/PlayerSender.java index 1de2652..a927e90 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/messages/PlayerSender.java +++ b/src/main/java/com/blbilink/blbilogin/modules/messages/PlayerSender.java @@ -71,6 +71,7 @@ public void onPlayerQuit(PlayerQuitEvent ev) { if (Configvar.noLoginPlayerList.contains(e.getName())) { Configvar.noLoginPlayerList.remove(e.getName()); } + com.blbilink.blbilogin.modules.Sqlite.getSqlite().setLogoutLocation(e.getUniqueId().toString(), e.getLocation()); if (timer != null) { timer.cancel(); } diff --git a/src/main/java/com/blbilink/blbilogin/modules/teleport/TeleportRequestManager.java b/src/main/java/com/blbilink/blbilogin/modules/teleport/TeleportRequestManager.java index 6d40977..48600c8 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/teleport/TeleportRequestManager.java +++ b/src/main/java/com/blbilink/blbilogin/modules/teleport/TeleportRequestManager.java @@ -38,4 +38,9 @@ public static TeleportRequest getRequest(UUID target) { public static void removeRequest(UUID target) { requests.remove(target); } + + public static void cancelRequests(UUID uuid) { + requests.entrySet().removeIf(e -> + e.getKey().equals(uuid) || e.getValue().requester.equals(uuid)); + } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index dba583f..94e436b 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -30,7 +30,7 @@ commands: worldstats: description: "Show world statistics" info: - description: "Alias of worldstats" + description: "Show player and server info" stats: description: "Alias of worldstats" dupe: @@ -45,3 +45,21 @@ commands: tpaccept: description: "Accept a teleport request" usage: /tpaccept + tpacancel: + description: "Cancel teleport requests" + usage: /tpacancel + joindate: + description: "Show when a player first joined" + aliases: [jd] + playtime: + description: "Show a player's playtime" + aliases: [pt] + listplayers: + description: "List registered usernames" + usage: /listplayers + tpoffline: + description: "Teleport to last logout location of a player" + usage: /tpoffline + blacklist: + description: "Blacklist a player from login" + usage: /blacklist From da73a91c783b08a1db363e27891a872ab627208a Mon Sep 17 00:00:00 2001 From: Lured Date: Tue, 3 Jun 2025 11:59:55 -0400 Subject: [PATCH 4/7] Add jar reload capability --- .../modules/commands/BlbiLoginCommand.java | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java index bb16aa3..d814bad 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java @@ -2,6 +2,7 @@ import com.blbilink.blbilogin.load.LoadConfig; import com.blbilink.blbilogin.vars.Configvar; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -38,17 +39,37 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command } private void reload(CommandSender sender) { - if ((sender instanceof Player player)) { - if (player.hasPermission("blbilogin.reload")) { - LoadConfig.loadConfig(plugin); - player.sendMessage(plugin.i18n.as("msgReloaded", true, player.getName())); - } else { - player.sendMessage(plugin.i18n.as("msgNoPermission", true, player.getName())); - } - } else { - LoadConfig.loadConfig(plugin); - plugin.getLogger().info(plugin.i18n.as("msgReloaded", false)); + boolean allowed = !(sender instanceof Player p) || p.hasPermission("blbilogin.reload"); + if (!allowed) { + ((Player) sender).sendMessage(plugin.i18n.as("msgNoPermission", true, ((Player) sender).getName())); + return; } + + // Schedule reload to avoid disabling the plugin while this command is executing + Bukkit.getScheduler().runTask(plugin, () -> { + try { + // Resolve current plugin JAR location + java.io.File file = new java.io.File(plugin.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); + org.bukkit.plugin.PluginManager pm = Bukkit.getPluginManager(); + + // Disable current instance + pm.disablePlugin(plugin); + + // Load new instance from the (potentially replaced) JAR + org.bukkit.plugin.Plugin loaded = ((org.bukkit.plugin.SimplePluginManager) pm).loadPlugin(file); + loaded.onLoad(); + pm.enablePlugin(loaded); + + if (sender instanceof Player player) { + player.sendMessage(plugin.i18n.as("msgReloaded", true, player.getName())); + } else { + plugin.getLogger().info(plugin.i18n.as("msgReloaded", false)); + } + } catch (Exception e) { + plugin.getLogger().severe("Failed to reload plugin: " + e.getMessage()); + e.printStackTrace(); + } + }); } private void saveLocation(CommandSender sender) { From ad95b70043659a96b7c1f8d8fa7a6df3f8206e2a Mon Sep 17 00:00:00 2001 From: Lured Date: Tue, 3 Jun 2025 13:16:41 -0400 Subject: [PATCH 5/7] Fix /stats alias and show uptime in worldstats --- .../modules/commands/WorldStatsCommand.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/WorldStatsCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/WorldStatsCommand.java index 5bf5669..b0b4e45 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/commands/WorldStatsCommand.java +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/WorldStatsCommand.java @@ -1,11 +1,14 @@ package com.blbilink.blbilogin.modules.commands; +import com.blbilink.blbilogin.BlbiLogin; import com.blbilink.blbilogin.vars.Configvar; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; + +import java.lang.management.ManagementFactory; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -17,10 +20,6 @@ public class WorldStatsCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!command.getName().equalsIgnoreCase("worldstats") && !command.getName().equalsIgnoreCase("info")) { - return false; - } - long days = ChronoUnit.DAYS.between(START_DATE, LocalDate.now()); String age = days + " days"; @@ -29,11 +28,24 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command long sizeBytes = folderSize(folder); String size = humanReadableByteCount(sizeBytes); + long serverUptime = System.currentTimeMillis() - BlbiLogin.serverStartTime; + long osUptime = System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().getStartTime(); + String prefix = Configvar.config.getString("prefix"); sender.sendMessage(prefix + "World size: " + size + ", Age: " + age); + sender.sendMessage(prefix + "Server uptime: " + formatDuration(serverUptime)); + sender.sendMessage(prefix + "System uptime: " + formatDuration(osUptime)); return true; } + private String formatDuration(long millis) { + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + return days + "d " + (hours % 24) + "h " + (minutes % 60) + "m"; + } + private long folderSize(File directory) { long length = 0; File[] files = directory.listFiles(); From 51d425556c69a686354314670c067872b5afa69b Mon Sep 17 00:00:00 2001 From: Lured Date: Tue, 3 Jun 2025 13:47:37 -0400 Subject: [PATCH 6/7] Improve plugin reload and player lookup --- .../modules/commands/BlbiLoginCommand.java | 13 ++++++------- .../blbilogin/modules/commands/JoinDateCommand.java | 5 +++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java index d814bad..d44703e 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java @@ -45,28 +45,27 @@ private void reload(CommandSender sender) { return; } - // Schedule reload to avoid disabling the plugin while this command is executing + // Schedule reload so we don't disable the plugin while handling the command Bukkit.getScheduler().runTask(plugin, () -> { try { - // Resolve current plugin JAR location java.io.File file = new java.io.File(plugin.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); org.bukkit.plugin.PluginManager pm = Bukkit.getPluginManager(); - // Disable current instance pm.disablePlugin(plugin); - // Load new instance from the (potentially replaced) JAR org.bukkit.plugin.Plugin loaded = ((org.bukkit.plugin.SimplePluginManager) pm).loadPlugin(file); loaded.onLoad(); pm.enablePlugin(loaded); + // Use the newly enabled instance for messages + BlbiLogin newPlugin = BlbiLogin.plugin; if (sender instanceof Player player) { - player.sendMessage(plugin.i18n.as("msgReloaded", true, player.getName())); + player.sendMessage(newPlugin.i18n.as("msgReloaded", true, player.getName())); } else { - plugin.getLogger().info(plugin.i18n.as("msgReloaded", false)); + newPlugin.getLogger().info(newPlugin.i18n.as("msgReloaded", false)); } } catch (Exception e) { - plugin.getLogger().severe("Failed to reload plugin: " + e.getMessage()); + BlbiLogin.plugin.getLogger().severe("Failed to reload plugin: " + e.getMessage()); e.printStackTrace(); } }); diff --git a/src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java index b48eb34..ff9be92 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java @@ -15,8 +15,9 @@ public class JoinDateCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { String targetName = args.length > 0 ? args[0] : sender.getName(); - OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); - if (!target.hasPlayedBefore() && !target.isOnline()) { + OfflinePlayer target = Bukkit.getOfflinePlayerIfCached(targetName); + if (target == null) target = Bukkit.getPlayerExact(targetName); + if (target == null || (!target.hasPlayedBefore() && !target.isOnline())) { sender.sendMessage("Player not found"); return true; } From 2dfb48c2fd004a8351df14c4d819decc72ab9d3a Mon Sep 17 00:00:00 2001 From: Lured Date: Tue, 3 Jun 2025 22:46:36 -0400 Subject: [PATCH 7/7] Ensure newline at end of Sqlite --- gradlew | 0 src/main/java/com/blbilink/blbilogin/modules/Sqlite.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java b/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java index 35250ed..6eb01b1 100644 --- a/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java +++ b/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java @@ -215,4 +215,4 @@ public java.util.List listUsernames() { } return list; } -} \ No newline at end of file +}