diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 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 2403607..e8e73f4 100644 --- a/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java +++ b/src/main/java/com/blbilink/blbilogin/load/LoadFunction.java @@ -38,10 +38,17 @@ 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()); 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 @@ -55,6 +62,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/Sqlite.java b/src/main/java/com/blbilink/blbilogin/modules/Sqlite.java index 927344f..6eb01b1 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; } -} \ No newline at end of file + + 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; + } +} 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/BlbiLoginCommand.java b/src/main/java/com/blbilink/blbilogin/modules/commands/BlbiLoginCommand.java index bb16aa3..d44703e 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,36 @@ 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 so we don't disable the plugin while handling the command + Bukkit.getScheduler().runTask(plugin, () -> { + try { + java.io.File file = new java.io.File(plugin.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); + org.bukkit.plugin.PluginManager pm = Bukkit.getPluginManager(); + + pm.disablePlugin(plugin); + + 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(newPlugin.i18n.as("msgReloaded", true, player.getName())); + } else { + newPlugin.getLogger().info(newPlugin.i18n.as("msgReloaded", false)); + } + } catch (Exception e) { + BlbiLogin.plugin.getLogger().severe("Failed to reload plugin: " + e.getMessage()); + e.printStackTrace(); + } + }); } private void saveLocation(CommandSender sender) { 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..ff9be92 --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/commands/JoinDateCommand.java @@ -0,0 +1,30 @@ +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.getOfflinePlayerIfCached(targetName); + if (target == null) target = Bukkit.getPlayerExact(targetName); + if (target == null || (!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/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/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/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(); 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..1da2d5b --- /dev/null +++ b/src/main/java/com/blbilink/blbilogin/modules/events/ChatFilter.java @@ -0,0 +1,76 @@ +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 static final String[] BLOCKED_WORDS = {"jonarchy"}; + + 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; + } + } + + // 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()); + 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/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/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..94e436b 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -30,6 +30,8 @@ commands: worldstats: description: "Show world statistics" info: + description: "Show player and server info" + stats: description: "Alias of worldstats" dupe: description: "Explains the chest boat dupe" @@ -43,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