From aee0fb2a4a74be33d7126ddac97a165623ca1383 Mon Sep 17 00:00:00 2001 From: Moussa Date: Fri, 4 Sep 2026 18:45:28 +0200 Subject: [PATCH 01/30] refactor(commands): remove `endersee` and `invsee` commands and cleanup related registrations --- .../commands/impl/EnderseeCommand.java | 23 ------ .../commands/impl/InvseeCommand.java | 25 ------ .../registry/CommandsRegistry.java | 76 ++++++++----------- blighted-smp/src/main/resources/plugin.yml | 8 +- 4 files changed, 34 insertions(+), 98 deletions(-) delete mode 100644 blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/EnderseeCommand.java delete mode 100644 blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/InvseeCommand.java diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/EnderseeCommand.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/EnderseeCommand.java deleted file mode 100644 index 782f5a70..00000000 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/EnderseeCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package fr.moussax.blightedSMP.commands.impl; - -import fr.moussax.blightedSMP.commands.AdminCommand; -import fr.moussax.blightedSMP.engine.player.menus.EnderSeeMenu; -import fr.moussax.bedrock.text.Messenger; -import org.bukkit.command.Command; -import org.bukkit.entity.Player; - -public final class EnderseeCommand extends AdminCommand { - @Override - protected boolean executeAdmin(Player player, Command command, String label, String[] args) { - if (args.length == 0) { - Messenger.warn(player, "Usage: /endersee "); - return false; - } - - Player target = requireTarget(player, args[0]); - if (target == null) return false; - - new EnderSeeMenu(target).open(player); - return true; - } -} diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/InvseeCommand.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/InvseeCommand.java deleted file mode 100644 index 6fac58c0..00000000 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/InvseeCommand.java +++ /dev/null @@ -1,25 +0,0 @@ -package fr.moussax.blightedSMP.commands.impl; - -import fr.moussax.blightedSMP.commands.AdminCommand; -import fr.moussax.blightedSMP.engine.player.menus.InvSeeMenu; -import org.bukkit.command.Command; -import org.bukkit.entity.Player; - -import static fr.moussax.bedrock.text.Messenger.warn; - -public final class InvseeCommand extends AdminCommand { - - @Override - protected boolean executeAdmin(Player player, Command command, String label, String[] args) { - if (args.length != 1) { - warn(player, "Usage: /invsee "); - return true; - } - - Player target = requireTarget(player, args[0]); - if (target == null) return true; - - new InvSeeMenu(target).open(player); - return true; - } -} diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/registry/CommandsRegistry.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/registry/CommandsRegistry.java index fad5028d..ef0ff73c 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/registry/CommandsRegistry.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/registry/CommandsRegistry.java @@ -1,6 +1,5 @@ package fr.moussax.blightedSMP.registry; -import fr.moussax.blightedSMP.BlightedSMP; import fr.moussax.bedrock.commands.CommandRegistrar; import fr.moussax.bedrock.commands.TabSuggestionRegistry; import fr.moussax.blightedSMP.commands.impl.*; @@ -10,6 +9,7 @@ import fr.moussax.blightedSMP.engine.items.registry.ItemRegistry; import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; /** * Central registry for all plugin commands. @@ -28,35 +28,31 @@ private CommandsRegistry() { * * @param plugin plugin instance used by the command registrar */ - public static void register(BlightedSMP plugin) { + public static void register(JavaPlugin plugin) { TabSuggestionRegistry suggestions = createSuggestionRegistry(); - CommandRegistrar commands = new CommandRegistrar(plugin, suggestions); + CommandRegistrar registrar = new CommandRegistrar(plugin, suggestions); - commands.register("altar", new AltarCommand()); - commands.register("craft", new CraftCommand()); - commands.register("forge", new ForgeCommand()); + registrar.register("altar", new AltarCommand()); + registrar.register("craft", new CraftCommand()); + registrar.register("forge", new ForgeCommand()); // Administrator Commands - commands.register("spawncustommob", new SpawnCustomMobCommand()); - commands.register("giveitem", new GiveItemCommand()); - commands.register("gems", new GemsCommand()); - commands.register("tppos", new TeleportPositionCommand()); - commands.register("invsee", new InvseeCommand()); - commands.register("endersee", new EnderseeCommand()); - - // Administrator Utilities + registrar.register("spawncustommob", new SpawnCustomMobCommand()); + registrar.register("giveitem", new GiveItemCommand()); + registrar.register("gems", new GemsCommand()); + registrar.register("tppos", new TeleportPositionCommand()); GamemodeCommands gamemodeCommands = new GamemodeCommands(); - commands.register("gmc", gamemodeCommands); - commands.register("gms", gamemodeCommands); - commands.register("gma", gamemodeCommands); - commands.register("gmspec", gamemodeCommands); - commands.register("fly", new FlyCommand()); - commands.register("god", new GodCommand()); - commands.register("nightvision", new NightVisionCommand()); - commands.register("speed", new SpeedCommand()); - commands.register("butcher", new ButcherCommand()); - commands.register("loop", new LoopCommand()); - commands.register("test", new TestCommand()); + registrar.register("gmc", gamemodeCommands); + registrar.register("gms", gamemodeCommands); + registrar.register("gma", gamemodeCommands); + registrar.register("gmspec", gamemodeCommands); + registrar.register("fly", new FlyCommand()); + registrar.register("god", new GodCommand()); + registrar.register("nightvision", new NightVisionCommand()); + registrar.register("speed", new SpeedCommand()); + registrar.register("butcher", new ButcherCommand()); + registrar.register("loop", new LoopCommand()); + registrar.register("test", new TestCommand()); } /** @@ -70,28 +66,22 @@ public static void register(BlightedSMP plugin) { private static TabSuggestionRegistry createSuggestionRegistry() { TabSuggestionRegistry suggestions = new TabSuggestionRegistry(); - suggestions.register( - "$players", - () -> Bukkit.getOnlinePlayers() - .stream() - .map(Player::getName) - .toList() + suggestions.register("$players", () -> Bukkit.getOnlinePlayers() + .stream() + .map(Player::getName) + .toList() ); - suggestions.register( - "$items", - () -> ItemRegistry.getAllItems() - .stream() - .map(BlightedItem::getItemId) - .toList() + suggestions.register("$items", () -> ItemRegistry.getAllItems() + .stream() + .map(BlightedItem::getItemId) + .toList() ); - suggestions.register( - "$entities", - () -> EntitiesRegistry.getAll() - .stream() - .map(BlightedEntity::getEntityId) - .toList() + suggestions.register("$entities", () -> EntitiesRegistry.getAll() + .stream() + .map(BlightedEntity::getEntityId) + .toList() ); return suggestions; diff --git a/blighted-smp/src/main/resources/plugin.yml b/blighted-smp/src/main/resources/plugin.yml index a80a1e53..b5c97c38 100644 --- a/blighted-smp/src/main/resources/plugin.yml +++ b/blighted-smp/src/main/resources/plugin.yml @@ -1,4 +1,4 @@ -name: BlightedMC +name: BlightedSMP version: '${version}' authors: [ MrInspection ] @@ -24,13 +24,7 @@ commands: tppos: description: Teleport to a position forge: - description: Forge command altar: - description: Altar command - endersee: - description: Moderator command - invsee: - description: Moderator command gms: description: Switch to survival mode gmc: From 7d05a816e1d94a239f1e33d3b6541067e2cfd31b Mon Sep 17 00:00:00 2001 From: Moussa Date: Fri, 4 Sep 2026 18:46:38 +0200 Subject: [PATCH 02/30] feat: implement moderation system with punishment, command, and event handling --- blighted-mod/pom.xml | 66 +++ .../fr/moussax/blightedMod/BlightedMod.java | 72 +++ .../commands/ModerationCommand.java | 51 +++ .../blightedMod/commands/impl/BanCommand.java | 206 +++++++++ .../commands/impl/ChatCommand.java | 49 +++ .../commands/impl/EnderseeCommand.java | 34 ++ .../commands/impl/FreezeCommand.java | 58 +++ .../commands/impl/InvseeCommand.java | 34 ++ .../commands/impl/KickCommand.java | 70 +++ .../blightedMod/commands/impl/ModCommand.java | 27 ++ .../commands/impl/ModTpCommands.java | 80 ++++ .../commands/impl/MuteCommand.java | 102 +++++ .../commands/impl/ReportCommand.java | 140 ++++++ .../commands/impl/ReportsCommand.java | 18 + .../commands/impl/SanctionsCommand.java | 38 ++ .../commands/impl/SetSlowmodeCommand.java | 48 ++ .../commands/impl/SocialSpyCommand.java | 23 + .../commands/impl/TargetCommand.java | 40 ++ .../commands/impl/UserinfoCommand.java | 59 +++ .../commands/impl/VanishCommand.java | 17 + .../blightedMod/database/PluginDatabase.java | 70 +++ .../moderator/BlightedModerator.java | 177 ++++++++ .../moderator/ModerationManager.java | 198 +++++++++ .../moderator/ModerationTools.java | 67 +++ .../moderator/hud/ModerationHud.java | 47 ++ .../listeners/InteractiveChatListener.java | 34 ++ .../listeners/ModerationListener.java | 416 ++++++++++++++++++ .../moderator/menus/EnderseeMenu.java | 8 +- .../moderator}/menus/InvSeeMenu.java | 4 +- .../moderator/menus/ReportsCenterMenu.java | 170 +++++++ .../menus/SanctionsPaginatedMenu.java | 222 ++++++++++ .../moderator/punishments/DurationParser.java | 71 +++ .../moderator/punishments/PunishmentData.java | 30 ++ .../punishments/PunishmentManager.java | 235 ++++++++++ .../moderator/reports/ReportData.java | 14 + .../moderator/reports/ReportManager.java | 43 ++ .../registry/CommandsRegistry.java | 94 ++++ .../blightedMod/registry/EventsRegistry.java | 54 +++ .../blightedMod/utils/PluginPermissions.java | 17 + blighted-mod/src/main/resources/plugin.yml | 77 ++++ pom.xml | 1 + 41 files changed, 3275 insertions(+), 6 deletions(-) create mode 100644 blighted-mod/pom.xml create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/ModerationCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/EnderseeCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/InvseeCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportsCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SocialSpyCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/VanishCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/database/PluginDatabase.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java rename blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/menus/EnderSeeMenu.java => blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/EnderseeMenu.java (90%) rename {blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player => blighted-mod/src/main/java/fr/moussax/blightedMod/moderator}/menus/InvSeeMenu.java (97%) create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsPaginatedMenu.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentData.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportData.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/utils/PluginPermissions.java create mode 100644 blighted-mod/src/main/resources/plugin.yml diff --git a/blighted-mod/pom.xml b/blighted-mod/pom.xml new file mode 100644 index 00000000..d9185ba5 --- /dev/null +++ b/blighted-mod/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + fr.moussax + blightedmc + 1.0-SNAPSHOT + + + blighted-mod + jar + + BlightedMod + + + + fr.moussax + bedrock + ${project.version} + + + org.spigotmc + spigot-api + + + org.spigotmc + spigot + + + org.projectlombok + lombok + + + + + clean package + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + + shade + + + + + + + + src/main/resources + true + + + + diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java new file mode 100644 index 00000000..9d5ae19e --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java @@ -0,0 +1,72 @@ +package fr.moussax.blightedMod; + +import fr.moussax.bedrock.scheduling.PluginContext; +import fr.moussax.bedrock.utils.debug.Log; +import fr.moussax.blightedMod.database.PluginDatabase; +import fr.moussax.blightedMod.moderator.ModerationManager; +import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import fr.moussax.blightedMod.registry.CommandsRegistry; +import fr.moussax.blightedMod.registry.EventsRegistry; +import org.bukkit.plugin.java.JavaPlugin; + +import java.sql.SQLException; + +public final class BlightedMod extends JavaPlugin { + + private static BlightedMod instance; + + private PluginDatabase pluginDatabase; + private ModerationManager moderationManager; + private EventsRegistry eventsRegistry; + + public static BlightedMod getInstance() { + return instance; + } + + public PluginDatabase getPluginDatabase() { + return pluginDatabase; + } + + public ModerationManager getModerationManager() { + return moderationManager; + } + + @Override + public void onEnable() { + instance = this; + PluginContext.bind(this); + + if (!getDataFolder().exists()) { + getDataFolder().mkdirs(); + } + + try { + pluginDatabase = new PluginDatabase(getDataFolder().getAbsolutePath() + "/blighted_mod.db"); + Log.success("Database", "Successfully connected to BlightedMod database."); + } catch (SQLException exception) { + Log.error("Database", "Unable to connect to BlightedMod database: " + exception.getMessage()); + getServer().getPluginManager().disablePlugin(this); + return; + } + + PunishmentManager punishmentManager = new PunishmentManager(pluginDatabase.getConnection()); + moderationManager = ModerationManager.init(punishmentManager); + + CommandsRegistry.registerCommands(this); + + eventsRegistry = new EventsRegistry(); + eventsRegistry.initializeListeners(); + } + + @Override + public void onDisable() { + if (eventsRegistry != null) { + eventsRegistry.shutdownMenus(); + eventsRegistry.cleanup(); + } + + if (pluginDatabase != null) { + pluginDatabase.closeConnection(); + } + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/ModerationCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/ModerationCommand.java new file mode 100644 index 00000000..4ab1d580 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/ModerationCommand.java @@ -0,0 +1,51 @@ +package fr.moussax.blightedMod.commands; + +import fr.moussax.bedrock.commands.PlayerCommand; +import fr.moussax.blightedMod.moderator.ModerationManager; +import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.warn; + +/** + * Base abstract class for all moderation commands in BlightedMod. + * Validates that the sender is a qualified moderator before delegating execution. + */ +public abstract class ModerationCommand extends PlayerCommand { + + @Override + protected final boolean execute(Player player, Command command, String label, String[] arguments) { + if (!getModerationManager().isModerator(player)) { + warn(player, "You do not have permission to execute moderation commands."); + return true; + } + + return executeModeration(player, command, label, arguments); + } + + /** + * Executes the moderation command logic. + * + * @param moderator validated player executing the moderation command + * @param command command executed + * @param label alias used to invoke the command + * @param arguments command arguments + * @return {@code true} if handled; otherwise {@code false} + */ + protected abstract boolean executeModeration( + Player moderator, + Command command, + String label, + String[] arguments + ); + + protected ModerationManager getModerationManager() { + return ModerationManager.getInstance(); + } + + protected PunishmentManager getPunishmentManager() { + return getModerationManager().getPunishmentManager(); + } +} + diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java new file mode 100644 index 00000000..cbc330f9 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java @@ -0,0 +1,206 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.punishments.DurationParser; +import fr.moussax.blightedMod.moderator.punishments.PunishmentData; +import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import java.util.Arrays; + +import static fr.moussax.bedrock.text.Messenger.warn; + +/** + * Moderation command to ban (/ban, /banip) and unban (/unban, /unbanip) players. + */ +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class BanCommand extends ModerationCommand { + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (label.equalsIgnoreCase("unban")) { + return handleUnban(moderator, arguments); + } + + if (label.equalsIgnoreCase("banip")) { + return handleBanIp(moderator, arguments); + } + + if (label.equalsIgnoreCase("unbanip")) { + return handleUnbanIp(moderator, arguments); + } + + return handleBan(moderator, arguments); + } + + private boolean handleBan(Player moderator, String[] arguments) { + if (arguments.length < 1) { + warn(moderator, "Usage: /ban [duration] [reason]"); + moderator.sendMessage("§7Duration format: 1d, 3w, 1m, 1y (omit for permanent)"); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot ban yourself."); + return false; + } + + if (getModerationManager().isModerator(target)) { + warn(moderator, "You cannot ban another moderator."); + return false; + } + + Long expiresAt = arguments.length > 1 ? DurationParser.parseDuration(arguments[1]) : null; + int reasonStartIndex = expiresAt != null ? 2 : 1; + + String reason = arguments.length > reasonStartIndex + ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) + : "No reason specified"; + String ipAddress = PunishmentManager.getPlayerIp(target); + + getPunishmentManager().addPunishment( + target.getUniqueId(), + target.getName(), + PunishmentData.PunishmentType.BAN, + reason, + moderator.getUniqueId(), + moderator.getName(), + expiresAt, + ipAddress + ); + + String durationText = expiresAt != null ? DurationParser.formatDuration(arguments[1]) : "Permanent"; + String banMessage = """ + §cYou are banned from this server! + + §7Reason: §f%s + §7Duration: §f%s + + §7Appeal on our Discord if you believe this was a mistake.""".formatted(reason, durationText); + + target.kickPlayer(banMessage); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 banned §9" + target.getName() + + "§7 for §c" + reason + "§7 (" + durationText + ")"; + getModerationManager().broadcastToModerators(notification); + + return true; + } + + private boolean handleUnban(Player moderator, String[] arguments) { + if (arguments.length < 1) { + warn(moderator, "Usage: /unban "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (!getPunishmentManager().isBanned(target.getUniqueId())) { + warn(moderator, target.getName() + " is not banned."); + return false; + } + + getPunishmentManager().removePunishment(target.getUniqueId(), PunishmentData.PunishmentType.BAN); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 unbanned §9" + target.getName(); + getModerationManager().broadcastToModerators(notification); + + return true; + } + + private boolean handleBanIp(Player moderator, String[] arguments) { + if (arguments.length < 1) { + warn(moderator, "Usage: /banip [duration] [reason]"); + moderator.sendMessage("§7Duration format: 1d, 3w, 1m, 1y (omit for permanent)"); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot IP ban yourself."); + return false; + } + + if (getModerationManager().isModerator(target)) { + warn(moderator, "You cannot IP ban another moderator."); + return false; + } + + Long expiresAt = arguments.length > 1 ? DurationParser.parseDuration(arguments[1]) : null; + int reasonStartIndex = expiresAt != null ? 2 : 1; + + String reason = arguments.length > reasonStartIndex + ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) + : "No reason specified"; + String ipAddress = PunishmentManager.getPlayerIp(target); + + getPunishmentManager().addPunishment( + target.getUniqueId(), + target.getName(), + PunishmentData.PunishmentType.IP_BAN, + reason, + moderator.getUniqueId(), + moderator.getName(), + expiresAt, + ipAddress + ); + + String durationText = expiresAt != null ? DurationParser.formatDuration(arguments[1]) : "Permanent"; + String banMessage = """ + §cYour IP address is banned from this server! + + §7Reason: §f%s + §7Duration: §f%s + + §7Appeal on our Discord if you believe this was a mistake.""".formatted(reason, durationText); + + target.kickPlayer(banMessage); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 IP banned §9" + target.getName() + + "§7 for §c" + reason + "§7 (" + durationText + ")"; + getModerationManager().broadcastToModerators(notification); + + return true; + } + + private boolean handleUnbanIp(Player moderator, String[] arguments) { + if (arguments.length < 1) { + warn(moderator, "Usage: /unbanip "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + String ipAddress = PunishmentManager.getPlayerIp(target); + + if (!getPunishmentManager().isIpBanned(ipAddress)) { + warn(moderator, "This IP is not banned."); + return false; + } + + getPunishmentManager().removeIpPunishment(ipAddress); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 unbanned IP for §9" + target.getName(); + getModerationManager().broadcastToModerators(notification); + + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java new file mode 100644 index 00000000..218f72e9 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java @@ -0,0 +1,49 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.ModerationManager; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.inform; +import static fr.moussax.bedrock.text.Messenger.warn; + +/** + * Command to switch active chat channel (/chat staff, /chat all). + */ +@CommandArgument(position = 0, suggestions = {"$channels"}) +public final class ChatCommand extends ModerationCommand { + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player player, Command command, String label, String[] arguments) { + if (arguments.length == 0) { + ModerationManager.ChatChannel currentChannel = getModerationManager().getChatChannel(player); + inform(player, PREFIX + "Currently in active chat channel: §e" + currentChannel.name()); + warn(player, "Usage: /chat "); + return true; + } + + String targetChannel = arguments[0].toLowerCase(); + if (targetChannel.equals("staff") || targetChannel.equals("s")) { + if (!getModerationManager().isModerator(player)) { + warn(player, "Only staff members can access the staff chat channel."); + return false; + } + + getModerationManager().setChatChannel(player, ModerationManager.ChatChannel.STAFF); + inform(player, PREFIX + "Switched active chat channel to §9STAFF§7."); + return true; + } + + if (targetChannel.equals("all") || targetChannel.equals("a") || targetChannel.equals("global") || targetChannel.equals("normal")) { + getModerationManager().setChatChannel(player, ModerationManager.ChatChannel.ALL); + inform(player, PREFIX + "Switched active chat channel to §bALL§7."); + return true; + } + + warn(player, "Unknown chat channel. Usage: /chat "); + return false; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/EnderseeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/EnderseeCommand.java new file mode 100644 index 00000000..bbc89009 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/EnderseeCommand.java @@ -0,0 +1,34 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.menus.EnderseeMenu; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.warn; + +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class EnderseeCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length == 0) { + warn(moderator, "Usage: /endersee "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot view your own ender chest."); + return false; + } + + new EnderseeMenu(target).open(moderator); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java new file mode 100644 index 00000000..cbc79855 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java @@ -0,0 +1,58 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.warn; + +/** + * Moderation command to toggle freeze status for a player (/freeze ). + */ +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class FreezeCommand extends ModerationCommand { + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length < 1) { + warn(moderator, "Usage: /freeze "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot freeze yourself."); + return false; + } + + if (getModerationManager().isModerator(target)) { + warn(moderator, "You cannot freeze another moderator."); + return false; + } + + boolean isNowFrozen = getModerationManager().toggleFreeze(target); + + if (isNowFrozen) { + moderator.sendMessage(PREFIX + target.getName() + " is now §bFROZEN"); + target.sendMessage("§c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); + target.sendMessage("§cDo not log out or you will be banned."); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 froze §9" + target.getName(); + getModerationManager().broadcastToModerators(notification); + } else { + moderator.sendMessage(PREFIX + target.getName() + " is now §aUNFROZEN"); + target.sendMessage("§aYou have been unfrozen."); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 unfroze §9" + target.getName(); + getModerationManager().broadcastToModerators(notification); + } + + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/InvseeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/InvseeCommand.java new file mode 100644 index 00000000..6c084006 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/InvseeCommand.java @@ -0,0 +1,34 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.menus.InvSeeMenu; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.warn; + +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class InvseeCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length == 0) { + warn(moderator, "Usage: /invsee "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot view your own inventory."); + return false; + } + + new InvSeeMenu(target).open(moderator); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java new file mode 100644 index 00000000..3a7e5fdc --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java @@ -0,0 +1,70 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.punishments.PunishmentData; +import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import java.util.Arrays; + +import static fr.moussax.bedrock.text.Messenger.warn; + +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class KickCommand extends ModerationCommand { + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length < 1) { + warn(moderator, "Usage: /kick [reason]"); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot kick yourself."); + return false; + } + + if (getModerationManager().isModerator(target)) { + warn(moderator, "You cannot kick another moderator."); + return false; + } + + String reason = arguments.length > 1 + ? String.join(" ", Arrays.copyOfRange(arguments, 1, arguments.length)) + : "No reason specified"; + String ipAddress = PunishmentManager.getPlayerIp(target); + + getPunishmentManager().addPunishment( + target.getUniqueId(), + target.getName(), + PunishmentData.PunishmentType.KICK, + reason, + moderator.getUniqueId(), + moderator.getName(), + null, + ipAddress + ); + + String kickMessage = """ + §cYou are kicked from this server! + + §7Reason: §f%s + + §7If you believe this was a mistake, please appeal on our Discord.""".formatted(reason); + + target.kickPlayer(kickMessage); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 kicked §9" + target.getName() + "§7 for §c" + reason; + getModerationManager().broadcastToModerators(notification); + + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModCommand.java new file mode 100644 index 00000000..664c84d0 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModCommand.java @@ -0,0 +1,27 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.blightedMod.commands.ModerationCommand; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +/** + * Moderation command to toggle moderation mode (/mod) or broadcast a staff announcement (/mod ). + */ +public final class ModCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length > 0) { + String message = String.join(" ", arguments); + String formattedMessage = " §9§lMOD §9" + moderator.getName() + " §f§l» §e§l" + message; + Bukkit.broadcastMessage(" "); + Bukkit.broadcastMessage(formattedMessage); + Bukkit.broadcastMessage(" "); + return true; + } + + getModerationManager().toggleModerationMode(moderator); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java new file mode 100644 index 00000000..2854fe87 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java @@ -0,0 +1,80 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.blightedMod.commands.ModerationCommand; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.warn; + +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class ModTpCommands extends ModerationCommand { + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (label.equalsIgnoreCase("mtp")) { + return handleTeleport(moderator, arguments); + } else if (label.equalsIgnoreCase("mtphere")) { + return handleTeleportHere(moderator, arguments); + } + return false; + } + + private boolean handleTeleport(Player moderator, String[] arguments) { + if (arguments.length == 0) { + warn(moderator, "Usage: /mtp "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot teleport to yourself."); + return false; + } + + moderator.teleport(target.getLocation()); + getModerationManager().getModerator(moderator).setTargetPlayer(target); + + InteractiveMessage.text(PREFIX + "Teleported to §9" + target.getName() + "§7. ") + .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + target.getName() + "§7 (/userinfo)", "/userinfo " + target.getName()) + .send(moderator); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 teleported to §9" + target.getName(); + getModerationManager().broadcastToModerators(notification); + return true; + } + + private boolean handleTeleportHere(Player moderator, String[] arguments) { + if (arguments.length == 0) { + warn(moderator, "Usage: /mtphere "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot teleport yourself to yourself."); + return false; + } + + target.teleport(moderator.getLocation()); + getModerationManager().getModerator(moderator).setTargetPlayer(target); + + InteractiveMessage.text(PREFIX + "Teleported §9" + target.getName() + "§7 to you. ") + .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + target.getName() + "§7 (/userinfo)", "/userinfo " + target.getName()) + .send(moderator); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 teleported §9" + target.getName() + "§7 to them"; + getModerationManager().broadcastToModerators(notification); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java new file mode 100644 index 00000000..f155dfd5 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java @@ -0,0 +1,102 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.punishments.DurationParser; +import fr.moussax.blightedMod.moderator.punishments.PunishmentData; +import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import java.util.Arrays; + +import static fr.moussax.bedrock.text.Messenger.warn; + +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class MuteCommand extends ModerationCommand { + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (label.equalsIgnoreCase("unmute")) { + return handleUnmute(moderator, arguments); + } + + return handleMute(moderator, arguments); + } + + private boolean handleMute(Player moderator, String[] arguments) { + if (arguments.length < 1) { + warn(moderator, "Usage: /mute [duration] [reason]"); + moderator.sendMessage("§7Duration format: 1d, 3w, 1m, 1y (omit for permanent)"); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (target.equals(moderator)) { + warn(moderator, "You cannot mute yourself."); + return false; + } + + if (getModerationManager().isModerator(target)) { + warn(moderator, "You cannot mute another moderator."); + return false; + } + + Long expiresAt = arguments.length > 1 ? DurationParser.parseDuration(arguments[1]) : null; + int reasonStartIndex = expiresAt != null ? 2 : 1; + + String reason = arguments.length > reasonStartIndex + ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) + : "No reason specified"; + String ipAddress = PunishmentManager.getPlayerIp(target); + + getPunishmentManager().addPunishment( + target.getUniqueId(), + target.getName(), + PunishmentData.PunishmentType.MUTE, + reason, + moderator.getUniqueId(), + moderator.getName(), + expiresAt, + ipAddress + ); + + String durationText = expiresAt != null ? " (" + DurationParser.formatDuration(arguments[1]) + ")" : " (Permanent)"; + String notification = PREFIX + "§9" + moderator.getName() + "§e muted §d" + target.getName() + + "§e for §f" + reason + "§6" + durationText; + + getModerationManager().broadcastToModerators(notification); + target.sendMessage(" §f§lSANCTION §f| §7You have been muted for: §f" + reason + durationText); + + return true; + } + + private boolean handleUnmute(Player moderator, String[] arguments) { + if (arguments.length < 1) { + warn(moderator, "Usage: /unmute "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + if (!getPunishmentManager().isMuted(target.getUniqueId())) { + warn(moderator, target.getName() + " is not muted."); + return false; + } + + getPunishmentManager().removePunishment(target.getUniqueId(), PunishmentData.PunishmentType.MUTE); + + String notification = PREFIX + "§9" + moderator.getName() + "§7 unmuted §9" + target.getName(); + getModerationManager().broadcastToModerators(notification); + target.sendMessage("§aYou have been unmuted."); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java new file mode 100644 index 00000000..45abbb66 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java @@ -0,0 +1,140 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.PlayerCommand; +import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.bedrock.ui.book.BookMenu; +import fr.moussax.blightedMod.moderator.ModerationManager; +import fr.moussax.blightedMod.moderator.reports.ReportManager; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import java.util.Arrays; + +import static fr.moussax.bedrock.text.Messenger.inform; +import static fr.moussax.bedrock.text.Messenger.warn; + +/** + * Handles /report command execution for general reports and chat reports with BookMenu interface. + */ +public final class ReportCommand extends PlayerCommand { + + private static final String PREFIX = " §c§lREPORT §f| §7"; + + @Override + protected boolean execute(Player player, Command command, String label, String[] arguments) { + if (arguments.length == 0) { + warn(player, "Usage: /report [reason] or click a chat flag ⚑."); + return false; + } + + String subAction = arguments[0]; + + if (subAction.equalsIgnoreCase("chat") && arguments.length >= 2) { + String targetName = arguments[1]; + String chatMessage = arguments.length > 2 + ? String.join(" ", Arrays.copyOfRange(arguments, 2, arguments.length)) + : "No message content"; + + openChatReportBook(player, targetName, chatMessage); + return true; + } + + if (subAction.equalsIgnoreCase("submit") && arguments.length >= 3) { + String targetName = arguments[1]; + String reason = arguments[2]; + String chatMessage = arguments.length > 3 + ? String.join(" ", Arrays.copyOfRange(arguments, 3, arguments.length)) + : "No message content"; + + submitChatReport(player, targetName, reason, chatMessage); + return true; + } + + Player target = requireTarget(player, subAction); + if (target == null) { + return false; + } + + if (target.equals(player)) { + warn(player, "You cannot report yourself."); + return false; + } + + if (arguments.length > 1) { + String reason = String.join(" ", Arrays.copyOfRange(arguments, 1, arguments.length)); + submitGeneralReport(player, target.getName(), reason); + } else { + openGeneralReportBook(player, target.getName()); + } + + return true; + } + + private void openChatReportBook(Player reporter, String targetName, String chatMessage) { + if (reporter.getName().equalsIgnoreCase(targetName)) { + warn(reporter, "You cannot report yourself."); + return; + } + + BookMenu bookMenu = BookMenu.builder() + .addPage(page -> page + .append(" §1§lCHAT REPORT\n") + .append("§0Reporting: §c" + targetName + "\n\n") + .append("§0Message:\n") + .append("§8\"§7" + chatMessage + "§8\"\n\n") + .append("§0Select Reason:\n") + .hoverAndExecute("§c▶ Abuse / Harassment\n", "§eSubmit report for Abuse / Harassment", "/report submit " + targetName + " Abuse/Harassment " + chatMessage) + .hoverAndExecute("§c▶ Spam / Flood\n", "§eSubmit report for Spam / Flood", "/report submit " + targetName + " Spam/Flood " + chatMessage) + .hoverAndExecute("§c▶ Inappropriate Content\n", "§eSubmit report for Inappropriate Content", "/report submit " + targetName + " Inappropriate " + chatMessage) + .hoverAndExecute("§c▶ Advertising / Scam", "§eSubmit report for Advertising / Scam", "/report submit " + targetName + " Advertising/Scam " + chatMessage) + ); + + bookMenu.open(reporter); + } + + private void openGeneralReportBook(Player reporter, String targetName) { + BookMenu bookMenu = BookMenu.builder() + .addPage(page -> page + .append(" §1§lPLAYER REPORT\n") + .append("§0Reporting: §c" + targetName + "\n\n") + .append("§0Select Reason:\n") + .hoverAndExecute("§c▶ Cheating / Hacks\n", "§eSubmit report for Cheating / Hacks", "/report submit " + targetName + " Cheating None") + .hoverAndExecute("§c▶ Toxicity / Abuse\n", "§eSubmit report for Toxicity / Abuse", "/report submit " + targetName + " Toxicity None") + .hoverAndExecute("§c▶ Griefing / Scamming\n", "§eSubmit report for Griefing / Scamming", "/report submit " + targetName + " Griefing None") + .hoverAndExecute("§c▶ Cross-Teaming", "§eSubmit report for Cross-Teaming", "/report submit " + targetName + " Cross-Teaming None") + ); + + bookMenu.open(reporter); + } + + private void submitChatReport(Player reporter, String targetName, String reason, String chatMessage) { + ReportManager.getInstance().submitReport(reporter.getName(), targetName, reason, chatMessage); + + String formattedReason = reason.replace("/", " / "); + String notificationText = chatMessage.equalsIgnoreCase("None") + ? PREFIX + "§e" + reporter.getName() + " §7reported §c" + targetName + " §7(" + formattedReason + ")" + : PREFIX + "§e" + reporter.getName() + " §7reported §c" + targetName + " §7(" + formattedReason + "): §f\"" + chatMessage + "\""; + + InteractiveMessage notificationMessage = InteractiveMessage.text(notificationText + " ") + .hoverAndExecute("§a§l[TP]", "§7Click to teleport to §e" + targetName + "§7 (/mtp)", "/mtp " + targetName) + .append(" ") + .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + targetName + "§7 (/userinfo)", "/userinfo " + targetName); + + ModerationManager.getInstance().broadcastToModerators(notificationMessage); + inform(reporter, "§aThank you! Your report against §f" + targetName + " §ahas been submitted to online moderators."); + } + + private void submitGeneralReport(Player reporter, String targetName, String reason) { + ReportManager.getInstance().submitReport(reporter.getName(), targetName, reason, "General player report"); + + String notificationText = PREFIX + "§e" + reporter.getName() + " §7reported §c" + targetName + " §7for: §f\"" + reason + "\""; + + InteractiveMessage notificationMessage = InteractiveMessage.text(notificationText + " ") + .hoverAndExecute("§a§l[TP]", "§7Click to teleport to §e" + targetName + "§7 (/mtp)", "/mtp " + targetName) + .append(" ") + .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + targetName + "§7 (/userinfo)", "/userinfo " + targetName); + + ModerationManager.getInstance().broadcastToModerators(notificationMessage); + inform(reporter, "§aThank you! Your report against §f" + targetName + " §ahas been submitted to online moderators."); + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportsCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportsCommand.java new file mode 100644 index 00000000..d25d9f86 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportsCommand.java @@ -0,0 +1,18 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.menus.ReportsCenterMenu; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +/** + * Moderation command to view active player reports via a paginated menu (/reports). + */ +public final class ReportsCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + new ReportsCenterMenu().open(moderator); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java new file mode 100644 index 00000000..24a32d55 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java @@ -0,0 +1,38 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.menus.SanctionsPaginatedMenu; +import fr.moussax.blightedMod.moderator.punishments.PunishmentData; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import java.util.List; + +import static fr.moussax.bedrock.text.Messenger.warn; + +/** + * Moderation command to view all sanctions for a player (/sanctions ). + */ +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class SanctionsCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length == 0) { + warn(moderator, "Usage: /sanctions "); + return false; + } + + String targetName = arguments[0]; + List punishments = getPunishmentManager().getAllPunishments(targetName); + + if (punishments.isEmpty()) { + warn(moderator, "No recorded sanctions found for player §4" + targetName + "§c."); + return true; + } + + new SanctionsPaginatedMenu(targetName, punishments).open(moderator); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java new file mode 100644 index 00000000..ad7ee923 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java @@ -0,0 +1,48 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.punishments.DurationParser; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.inform; +import static fr.moussax.bedrock.text.Messenger.warn; + +public final class SetSlowmodeCommand extends ModerationCommand { + + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length == 0) { + int currentSlowmode = getModerationManager().getSlowmodeDelaySeconds(); + if (currentSlowmode > 0) { + inform(moderator, PREFIX + "Chat slowmode is currently §aENABLED §7(§e" + currentSlowmode + "s§7)."); + } else { + inform(moderator, PREFIX + "Chat slowmode is currently §cDISABLED§7."); + } + warn(moderator, "Usage: /slowmode "); + return true; + } + + String input = arguments[0]; + if (input.equalsIgnoreCase("off") || input.equalsIgnoreCase("disable") || input.equalsIgnoreCase("0")) { + getModerationManager().setSlowmodeDelaySeconds(0); + Bukkit.broadcastMessage(PREFIX + "Chat slowmode has been §cDISABLED§7 by §e" + moderator.getName() + "§7."); + return true; + } + + Long secondsDuration = DurationParser.parseDurationSeconds(input); + if (secondsDuration == null || secondsDuration <= 0) { + getModerationManager().setSlowmodeDelaySeconds(0); + Bukkit.broadcastMessage(PREFIX + "Chat slowmode has been §cDISABLED§7 by §e" + moderator.getName() + "§7."); + return true; + } + + int seconds = (int) Math.min(secondsDuration, Integer.MAX_VALUE); + getModerationManager().setSlowmodeDelaySeconds(seconds); + Bukkit.broadcastMessage(PREFIX + "Chat slowmode set to §e" + seconds + "§7 second(s) by §e" + moderator.getName() + "§7."); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SocialSpyCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SocialSpyCommand.java new file mode 100644 index 00000000..f90ad6f2 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SocialSpyCommand.java @@ -0,0 +1,23 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.ModerationManager; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.inform; + +public final class SocialSpyCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + ModerationManager manager = getModerationManager(); + boolean enabled = manager.toggleMessageInspect(moderator); + if (enabled) { + inform(moderator, "§9[SPY] §7Private message inspection is now §aENABLED§7."); + } else { + inform(moderator, "§9[SPY] §7Private message inspection is now §cDISABLED§7."); + } + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java new file mode 100644 index 00000000..dc4f08e6 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java @@ -0,0 +1,40 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.BlightedModerator; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.inform; + +/** + * Moderation command to set or clear the targeted player for the moderation actionbar HUD (/target ). + */ +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class TargetCommand extends ModerationCommand { + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + BlightedModerator blightedModerator = getModerationManager().getModerator(moderator); + + if (arguments.length == 0) { + blightedModerator.setTargetPlayer(null); + inform(moderator, PREFIX + "Cleared moderation HUD target."); + return true; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + blightedModerator.setTargetPlayer(target); + InteractiveMessage.text(PREFIX + "Now targeting §e" + target.getName() + " §7for moderation HUD. ") + .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + target.getName() + "§7 (/userinfo)", "/userinfo " + target.getName()) + .send(moderator); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java new file mode 100644 index 00000000..fd7fa2ca --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java @@ -0,0 +1,59 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.commands.CommandArgument; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.punishments.PunishmentData; +import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import java.util.List; + +import static fr.moussax.bedrock.text.Messenger.inform; +import static fr.moussax.bedrock.text.Messenger.warn; + +/** + * Moderation command to view detailed player info (/userinfo ). + */ +@CommandArgument(position = 0, suggestions = {"$players"}) +public final class UserInfoCommand extends ModerationCommand { + private static final String PREFIX = " §9§lMOD §f| §7"; + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length == 0) { + warn(moderator, "Usage: /userinfo "); + return false; + } + + Player target = requireTarget(moderator, arguments[0]); + if (target == null) { + return false; + } + + String ipAddress = PunishmentManager.getPlayerIp(target); + Location location = target.getLocation(); + // ponytail: kept — Spigot 26.2 returns double for getMaxHealth(); direct Bukkit API call used for health percentage. + int healthPercent = (int) Math.round((target.getHealth() / target.getMaxHealth()) * 100.0); + + List punishments = getPunishmentManager().getAllPunishments(target.getName()); + long mutesCount = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.MUTE).count(); + long bansCount = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.BAN || p.type() == PunishmentData.PunishmentType.IP_BAN).count(); + long kicksCount = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.KICK).count(); + + boolean isFrozen = getModerationManager().isFrozen(target); + boolean isMuted = getPunishmentManager().isMuted(target.getUniqueId()); + boolean isVanished = getModerationManager().getModerator(target).isVanished(); + + inform(moderator, PREFIX + "User info for §e" + target.getName() + "§7:"); + moderator.sendMessage(" §f- §7UUID: §f" + target.getUniqueId()); + moderator.sendMessage(" §f- §7IP Address: §f" + ipAddress); + moderator.sendMessage(" §f- §7State: §a" + healthPercent + "% HP §f| §e" + target.getFoodLevel() + " Food §f| §b" + target.getGameMode().name() + " §f| §dLvl " + target.getLevel()); + moderator.sendMessage(" §f- §7Location: §f" + location.getWorld().getName() + " (" + location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ() + ")"); + moderator.sendMessage(" §f- §7Status: §fFrozen: " + (isFrozen ? "§aYES" : "§cNO") + " §f| Muted: " + (isMuted ? "§aYES" : "§cNO") + " §f| Vanished: " + (isVanished ? "§aYES" : "§cNO")); + moderator.sendMessage(" §f- §7History: §f" + mutesCount + " Mutes | " + bansCount + " Bans | " + kicksCount + " Kicks"); + + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/VanishCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/VanishCommand.java new file mode 100644 index 00000000..c7843b7b --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/VanishCommand.java @@ -0,0 +1,17 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.BlightedModerator; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +public final class VanishCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + BlightedModerator currentModerator = getModerationManager().getModerator(moderator); + boolean newVanishState = !currentModerator.isVanished(); + currentModerator.setVanished(newVanishState); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PluginDatabase.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PluginDatabase.java new file mode 100644 index 00000000..46e71200 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PluginDatabase.java @@ -0,0 +1,70 @@ +package fr.moussax.blightedMod.database; + +import fr.moussax.bedrock.utils.debug.Log; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Manages SQLite database connection and schema initialization for BlightedMod. + */ +public final class PluginDatabase { + private final Connection connection; + + /** + * Creates a database manager at the specified file path and initializes the schema. + * + * @param path absolute file path to the SQLite database file + * @throws SQLException if a database access error occurs during connection or initialization + */ + public PluginDatabase(String path) throws SQLException { + this.connection = DriverManager.getConnection("jdbc:sqlite:" + path); + initializeSchema(); + } + + public Connection getConnection() { + return connection; + } + + /** + * Closes the active database connection if currently open. + */ + public void closeConnection() { + if (connection == null) return; + synchronized (connection) { + try { + if (!connection.isClosed()) { + connection.close(); + } + } catch (SQLException exception) { + Log.error("PluginDatabase", exception.getMessage()); + throw new RuntimeException("Unable to close the database connection", exception); + } + } + } + + private void initializeSchema() throws SQLException { + synchronized (connection) { + try (Statement statement = connection.createStatement()) { + statement.execute(""" + CREATE TABLE IF NOT EXISTS punishments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_uuid TEXT NOT NULL, + player_name TEXT NOT NULL, + punishment_type TEXT NOT NULL, + reason TEXT NOT NULL, + moderator_uuid TEXT NOT NULL, + moderator_name TEXT NOT NULL, + created_at INTEGER NOT NULL, + expires_at INTEGER, + is_active INTEGER NOT NULL DEFAULT 1, + ip_address TEXT + ) + """ + ); + } + } + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java new file mode 100644 index 00000000..019c8793 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java @@ -0,0 +1,177 @@ +package fr.moussax.blightedMod.moderator; + +import fr.moussax.blightedMod.BlightedMod; +import fr.moussax.blightedMod.utils.PluginPermissions; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.potion.PotionEffect; + +import java.util.Collection; + +public final class BlightedModerator { + private static final String PREFIX = " §9§lMOD §f| §7"; + + private final Player player; + private boolean isInModerationMode; + private boolean isVanished; + private Player targetPlayer; + + private ItemStack[] savedInventory; + private ItemStack[] savedArmor; + private ItemStack savedOffHand; + private GameMode savedGameMode; + private float savedExperience; + private int savedLevel; + private Collection savedPotionEffects; + private org.bukkit.Location savedLocation; + + public BlightedModerator(Player player) { + this.player = player; + this.isInModerationMode = false; + this.isVanished = false; + } + + public Player getPlayer() { + return player; + } + + public boolean isInModerationMode() { + return isInModerationMode; + } + + public boolean isVanished() { + return isVanished; + } + + public Player getTargetPlayer() { + return targetPlayer; + } + + public void setTargetPlayer(Player targetPlayer) { + this.targetPlayer = targetPlayer; + } + + public void onEnable() { + saveState(); + clearPlayerState(); + applyModerationState(); + setVanished(true, true); + giveModerationTools(); + this.isInModerationMode = true; + + player.sendMessage(PREFIX + "You are now in §9moderation §7mode."); + } + + public void onDisable() { + player.getInventory().clear(); + restoreState(); + setVanished(false, true); + this.isInModerationMode = false; + this.targetPlayer = null; + + player.sendMessage(PREFIX + "You are no longer in §9moderation §7mode."); + } + + private void saveState() { + PlayerInventory inventory = player.getInventory(); + this.savedInventory = inventory.getContents(); + this.savedArmor = inventory.getArmorContents(); + this.savedOffHand = inventory.getItemInOffHand(); + this.savedGameMode = player.getGameMode(); + this.savedExperience = player.getExp(); + this.savedLevel = player.getLevel(); + this.savedPotionEffects = player.getActivePotionEffects(); + this.savedLocation = player.getLocation(); + } + + private void restoreState() { + PlayerInventory inventory = player.getInventory(); + inventory.setContents(savedInventory); + inventory.setArmorContents(savedArmor); + inventory.setItemInOffHand(savedOffHand); + + player.setGameMode(savedGameMode); + player.setExp(savedExperience); + player.setLevel(savedLevel); + + player.getActivePotionEffects().forEach(effect -> player.removePotionEffect(effect.getType())); + player.addPotionEffects(savedPotionEffects); + + if (savedLocation != null) { + player.teleport(savedLocation); + } + + player.setInvulnerable(false); + player.setAllowFlight(false); + player.setFlying(false); + } + + private void clearPlayerState() { + player.getInventory().clear(); + player.setExp(0); + player.setLevel(0); + player.getActivePotionEffects().forEach(effect -> player.removePotionEffect(effect.getType())); + player.setHealth(20); + player.setFoodLevel(20); + } + + private void applyModerationState() { + player.setGameMode(GameMode.SURVIVAL); + player.setAllowFlight(true); + player.setFlying(true); + player.setInvulnerable(true); + } + + public void giveModerationTools() { + PlayerInventory inventory = player.getInventory(); + inventory.setItem(0, ModerationTools.getRandomTeleporter()); + inventory.setItem(1, ModerationTools.getReportViewer()); + inventory.setItem(2, ModerationTools.getSanctionsInspector()); + inventory.setItem(3, ModerationTools.getKnockbackStick()); + inventory.setItem(4, ModerationTools.getModerationMenu()); + inventory.setItem(6, ModerationTools.getFreezer()); + inventory.setItem(7, ModerationTools.getInventoryInspector()); + inventory.setItem(8, ModerationTools.getVanishTool(isVanished)); + } + + public void setVanished(boolean vanished, boolean notifyPlayer) { + this.isVanished = vanished; + JavaPlugin instance = BlightedMod.getInstance(); + + if (vanished) { + hideFromNonModerators(instance); + if (!notifyPlayer) player.sendMessage(PREFIX + "You are now §avanished§7."); + } else { + showToAllPlayers(instance); + if (!notifyPlayer) player.sendMessage(PREFIX + "You are now §cvisible§7."); + } + updateVanishTool(); + } + + public void setVanished(boolean vanished) { + setVanished(vanished, false); + } + + private void hideFromNonModerators(JavaPlugin instance) { + Bukkit.getOnlinePlayers().forEach(onlinePlayer -> { + if (!onlinePlayer.hasPermission(PluginPermissions.MODERATOR.toString())) { + onlinePlayer.hidePlayer(instance, player); + } + }); + } + + private void showToAllPlayers(JavaPlugin instance) { + Bukkit.getOnlinePlayers().forEach(onlinePlayer -> onlinePlayer.showPlayer(instance, player)); + } + + private void updateVanishTool() { + ItemStack currentItem = player.getInventory().getItem(8); + if (currentItem != null && currentItem.getType().name().contains("DYE")) { + player.getInventory().setItem(8, ModerationTools.getVanishTool(isVanished)); + } + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java new file mode 100644 index 00000000..d882bef4 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java @@ -0,0 +1,198 @@ +package fr.moussax.blightedMod.moderator; + +import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import fr.moussax.blightedMod.utils.PluginPermissions; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.*; + +import java.util.concurrent.ConcurrentHashMap; + +public final class ModerationManager { + private static ModerationManager instance; + + public enum ChatChannel { + ALL, + STAFF + } + + private final PunishmentManager punishmentManager; + private final Map moderators = new HashMap<>(); + private final Set frozenPlayers = new HashSet<>(); + private final Map lastChatTimestamps = new ConcurrentHashMap<>(); + private final Map playerChatChannels = new ConcurrentHashMap<>(); + private final Set messageInspectEnabled = ConcurrentHashMap.newKeySet(); + private final Map lastMessageTargets = new ConcurrentHashMap<>(); + private int slowmodeDelaySeconds = 0; + + public PunishmentManager getPunishmentManager() { + return punishmentManager; + } + + private ModerationManager(PunishmentManager punishmentManager) { + this.punishmentManager = punishmentManager; + } + + public static ModerationManager init(PunishmentManager punishmentManager) { + if (instance != null) { + throw new IllegalStateException("ModerationManager already initialized"); + } + instance = new ModerationManager(punishmentManager); + return instance; + } + + public static ModerationManager getInstance() { + if (instance == null) { + throw new IllegalStateException("ModerationManager not initialized yet"); + } + return instance; + } + + public BlightedModerator getModerator(Player player) { + return moderators.computeIfAbsent(player.getUniqueId(), _ -> new BlightedModerator(player)); + } + + public boolean isModerator(Player player) { + return player.hasPermission(PluginPermissions.MODERATOR.toString()); + } + + public boolean isInModerationMode(Player player) { + BlightedModerator moderator = moderators.get(player.getUniqueId()); + return moderator != null && moderator.isInModerationMode(); + } + + public void toggleModerationMode(Player player) { + BlightedModerator moderator = getModerator(player); + if (moderator.isInModerationMode()) { + moderator.onDisable(); + } else { + moderator.onEnable(); + } + } + + public void disableModeration(Player player) { + BlightedModerator moderator = moderators.get(player.getUniqueId()); + if (moderator != null && moderator.isInModerationMode()) moderator.onDisable(); + } + + public boolean isFrozen(Player player) { + return frozenPlayers.contains(player.getUniqueId()); + } + + public boolean toggleFreeze(Player player) { + UUID playerId = player.getUniqueId(); + if (frozenPlayers.contains(playerId)) { + frozenPlayers.remove(playerId); + return false; + } + frozenPlayers.add(playerId); + return true; + } + + public void setSlowmodeDelaySeconds(int seconds) { + this.slowmodeDelaySeconds = Math.max(0, seconds); + } + + public int getSlowmodeDelaySeconds() { + return slowmodeDelaySeconds; + } + + public boolean isSlowmodeEnabled() { + return slowmodeDelaySeconds > 0; + } + + public int getRemainingSlowmodeCooldown(Player player) { + if (slowmodeDelaySeconds <= 0 || isModerator(player)) { + return 0; + } + + Long lastChatTime = lastChatTimestamps.get(player.getUniqueId()); + if (lastChatTime == null) { + return 0; + } + + long elapsedSeconds = (System.currentTimeMillis() - lastChatTime) / 1000L; + long remainingSeconds = slowmodeDelaySeconds - elapsedSeconds; + return (int) Math.max(0, remainingSeconds); + } + + public void recordPlayerChat(Player player) { + lastChatTimestamps.put(player.getUniqueId(), System.currentTimeMillis()); + } + + public void broadcastToModerators(String message) { + Bukkit.getOnlinePlayers().stream() + .filter(this::isModerator) + .forEach(moderator -> moderator.sendMessage(message)); + } + + public void broadcastToModerators(InteractiveMessage interactiveMessage) { + Bukkit.getOnlinePlayers().stream() + .filter(this::isModerator) + .forEach(interactiveMessage::send); + } + + public ChatChannel getChatChannel(Player player) { + return playerChatChannels.getOrDefault(player.getUniqueId(), ChatChannel.ALL); + } + + public void setChatChannel(Player player, ChatChannel channel) { + if (channel == ChatChannel.ALL) { + playerChatChannels.remove(player.getUniqueId()); + } else { + playerChatChannels.put(player.getUniqueId(), channel); + } + } + + public void handlePlayerQuit(Player player) { + UUID playerId = player.getUniqueId(); + BlightedModerator moderator = moderators.get(playerId); + if (moderator != null && moderator.isInModerationMode()) { + moderator.onDisable(); + } + moderators.remove(playerId); + frozenPlayers.remove(playerId); + lastChatTimestamps.remove(playerId); + playerChatChannels.remove(playerId); + messageInspectEnabled.remove(playerId); + lastMessageTargets.remove(playerId); + } + + public boolean isMessageInspectEnabled(Player player) { + return messageInspectEnabled.contains(player.getUniqueId()); + } + + public boolean toggleMessageInspect(Player player) { + UUID playerId = player.getUniqueId(); + if (messageInspectEnabled.contains(playerId)) { + messageInspectEnabled.remove(playerId); + return false; + } + messageInspectEnabled.add(playerId); + return true; + } + + public void setLastMessageTarget(UUID senderId, UUID recipientId) { + lastMessageTargets.put(senderId, recipientId); + lastMessageTargets.put(recipientId, senderId); + } + + public UUID getLastMessageTarget(UUID playerId) { + return lastMessageTargets.get(playerId); + } + + public void broadcastSpyMessage(String senderName, String recipientName, String content) { + String formatted = " §9[SPY] §7" + senderName + " §8» §7" + recipientName + "§f: " + content; + Bukkit.getOnlinePlayers().stream() + .filter(this::isModerator) + .filter(this::isMessageInspectEnabled) + .forEach(moderator -> moderator.sendMessage(formatted)); + } + + public Map getModeratorsView() { + return Collections.unmodifiableMap(moderators); + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java new file mode 100644 index 00000000..4285ecd4 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java @@ -0,0 +1,67 @@ +package fr.moussax.blightedMod.moderator; + +import fr.moussax.bedrock.utils.ItemBuilder; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; + +public final class ModerationTools { + public static ItemStack getRandomTeleporter() { + return new ItemBuilder(Material.ENDER_EYE) + .setDisplayName("§dRandom Teleport §7(Right Click)") + .addLore("§7Teleport to a random player.") + .addEnchantmentGlint() + .toItemStack(); + } + + public static ItemStack getReportViewer() { + return new ItemBuilder(Material.KNOWLEDGE_BOOK) + .setDisplayName("§dReport Center §7(Right Click)") + .addLore("§7View and manage player reports.") + .toItemStack(); + } + + public static ItemStack getSanctionsInspector() { + return new ItemBuilder(Material.ENCHANTED_BOOK) + .setDisplayName("§dSanctions Inspector §7(Right Click)") + .addLore("§7View target player's sanctions history.") + .setEnchantmentGlint(false) + .toItemStack(); + } + + public static ItemStack getKnockbackStick() { + return new ItemBuilder(Material.STICK) + .setDisplayName("§dAnti Knockback") + .addLore("§7Test if target has Anti-KB Cheat.") + .addEnchantment(Enchantment.KNOCKBACK, 4) + .toItemStack(); + } + + public static ItemStack getModerationMenu() { + return new ItemBuilder(Material.NETHER_STAR) + .setDisplayName("§dModeration Menu §7(Right Click)") + .addLore("§7Access to extra tools and settings.") + .toItemStack(); + } + + public static ItemStack getFreezer() { + return new ItemBuilder(Material.PACKED_ICE) + .setDisplayName("§dFreeze Target §7(Right Click)") + .addLore("§7Prevent the target from moving.") + .toItemStack(); + } + + public static ItemStack getInventoryInspector() { + return new ItemBuilder(Material.CHEST) + .setDisplayName("§dInspect Inventory §7(Right Click)") + .addLore("§7View target inventory's inventory contents.") + .toItemStack(); + } + + public static ItemStack getVanishTool(boolean isVanished) { + return new ItemBuilder(isVanished ? Material.PURPLE_DYE : Material.GRAY_DYE) + .setDisplayName(isVanished ? "§aBecome Visible §7(Right Click)" : "§cBecome Invisible §7(Right Click)") + .addLore("§7Current status: " + (isVanished ? "§dVanished" : "§cVisible")) + .toItemStack(); + } +} \ No newline at end of file diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java new file mode 100644 index 00000000..02201cf6 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java @@ -0,0 +1,47 @@ +package fr.moussax.blightedMod.moderator.hud; + +import fr.moussax.bedrock.ui.actionbar.ActionbarSection; +import fr.moussax.blightedMod.moderator.BlightedModerator; +import fr.moussax.blightedMod.moderator.ModerationManager; +import org.bukkit.entity.Player; + +import java.util.Locale; + +public final class ModerationHud { + public static final String SECTION_ID = "moderation_hud"; + public static final int PRIORITY = 100; + + private ModerationHud() { + } + + public static ActionbarSection createSection(ModerationManager moderationManager) { + return ActionbarSection.of( + SECTION_ID, + PRIORITY, + player -> render(moderationManager, player), + moderationManager::isModerator + ); + } + + private static String render(ModerationManager moderationManager, Player player) { + BlightedModerator moderator = moderationManager.getModerator(player); + if (moderator == null) { + return null; + } + + Player target = moderator.getTargetPlayer(); + if (target == null || !target.isOnline()) { + return null; + } + + int healthPercent = (int) Math.round((target.getHealth() / target.getHealth()) * 100.0); + + String distanceText = player.getWorld().equals(target.getWorld()) + ? String.format(Locale.ROOT, "%.2f blocks", player.getLocation().distance(target.getLocation())) + : "N/A"; + + String frozenText = moderationManager.isFrozen(target) ? "§b§lYUp!" : "§c§lNOPE!"; + + return "§fTarget: §d" + target.getName() + " §fHP: §d" + healthPercent + "% §fDistance: §d" + distanceText + " §fFrozen: " + frozenText; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java new file mode 100644 index 00000000..1a590230 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java @@ -0,0 +1,34 @@ +package fr.moussax.blightedMod.moderator.listeners; + +import fr.moussax.bedrock.text.InteractiveMessage; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; + +/** + * Handles interactive chat flag rendering for chat reporting via /report chat . + */ +public final class InteractiveChatListener implements Listener { + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNormalPlayerChat(AsyncPlayerChatEvent event) { + event.setCancelled(true); + + Player sender = event.getPlayer(); + String chatMessage = event.getMessage(); + + InteractiveMessage interactiveMessage = InteractiveMessage.text(" ") + .hoverAndExecute( + "§c⚑ ", + "§cClick to report §f" + sender.getName(), + "/report chat " + sender.getName() + " " + chatMessage + ) + .append("§7" + sender.getName() + " §f§l» §7" + chatMessage); + + for (Player recipient : event.getRecipients()) { + interactiveMessage.send(recipient); + } + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java new file mode 100644 index 00000000..53c59011 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java @@ -0,0 +1,416 @@ +package fr.moussax.blightedMod.moderator.listeners; + +import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.blightedMod.BlightedMod; +import fr.moussax.blightedMod.moderator.BlightedModerator; +import fr.moussax.blightedMod.moderator.ModerationManager; +import fr.moussax.blightedMod.moderator.menus.InvSeeMenu; +import fr.moussax.blightedMod.moderator.punishments.PunishmentData; +import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.*; +import org.bukkit.event.server.ServerListPingEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +import static fr.moussax.bedrock.text.Messenger.warn; + +public final class ModerationListener implements Listener { + private static final String PREFIX = " §9§lMOD §f| §7"; + + private final ModerationManager moderationManager; + private final PunishmentManager punishmentManager; + private final Random random = new Random(); + + public ModerationListener(ModerationManager moderationManager) { + this.moderationManager = moderationManager; + this.punishmentManager = moderationManager.getPunishmentManager(); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + + if (!moderationManager.isModerator(player) && punishmentManager.isMuted(player.getUniqueId())) { + event.setCancelled(true); + PunishmentData mute = punishmentManager.getActivePunishment(player.getUniqueId(), PunishmentData.PunishmentType.MUTE); + if (mute != null) { + String durationText = mute.isPermanent() ? "permanently" : "temporarily"; + player.sendMessage("§cYou are " + durationText + " muted for: §f" + mute.reason()); + } + return; + } + + if (moderationManager.isModerator(player) && moderationManager.getChatChannel(player) == ModerationManager.ChatChannel.STAFF) { + event.setCancelled(true); + String formatted = " §9§lMOD §f| §9" + player.getName() + "§f§l » §b" + event.getMessage(); + moderationManager.broadcastToModerators(formatted); + return; + } + + int remainingSlowmodeSeconds = moderationManager.getRemainingSlowmodeCooldown(player); + if (remainingSlowmodeSeconds > 0) { + event.setCancelled(true); + warn(player, "Chat is in slowmode. Please wait §e" + remainingSlowmodeSeconds + "§7 second(s) before typing again."); + return; + } + + String message = event.getMessage(); + if (message.startsWith("!!") && moderationManager.isModerator(player)) { + event.setCancelled(true); + String modMessage = message.substring(2).trim(); + String formatted = " §9§lMOD §f| §9" + player.getName() + "§f§l » §b" + modMessage; + moderationManager.broadcastToModerators(formatted); + return; + } + + moderationManager.recordPlayerChat(player); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerLogin(PlayerLoginEvent event) { + Player player = event.getPlayer(); + if (moderationManager.isModerator(player)) { + return; + } + + String ipAddress = PunishmentManager.getPlayerIp(player); + + PunishmentData ipBan = punishmentManager.getActiveIpBan(ipAddress); + if (ipBan != null) { + event.disallow(PlayerLoginEvent.Result.KICK_BANNED, buildBanMessage(ipBan, "IP BANNED")); + return; + } + + PunishmentData ban = punishmentManager.getActivePunishment(player.getUniqueId(), PunishmentData.PunishmentType.BAN); + if (ban != null) { + event.disallow(PlayerLoginEvent.Result.KICK_BANNED, buildBanMessage(ban, "BANNED")); + } + } + + private String buildBanMessage(PunishmentData ban, String title) { + String durationText = ban.isPermanent() ? "Permanent" : "Temporary"; + return """ + §c§l%s + + §7Reason: §f%s + §7Duration: §f%s + + §7Appeal on our Discord if you believe this was a mistake.""".formatted(title, ban.reason(), durationText); + } + + @EventHandler + public void onToolInteract(PlayerInteractEvent event) { + Player player = event.getPlayer(); + if (!moderationManager.isInModerationMode(player)) return; + + event.setCancelled(true); + + ItemStack item = event.getItem(); + if (item == null) return; + + Action action = event.getAction(); + if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) return; + + handleToolInteraction(player, item.getType()); + } + + @EventHandler + public void onEntityInteract(PlayerInteractEntityEvent event) { + Player player = event.getPlayer(); + if (!moderationManager.isInModerationMode(player)) return; + + event.setCancelled(true); + + if (!(event.getRightClicked() instanceof Player target)) return; + + Material tool = player.getInventory().getItemInMainHand().getType(); + handleEntityInteraction(player, target, tool); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player joiningPlayer = event.getPlayer(); + if (moderationManager.isModerator(joiningPlayer)) { + BlightedModerator moderator = moderationManager.getModerator(joiningPlayer); + if (moderator.isVanished()) { + event.setJoinMessage(null); + } + return; + } + + moderationManager.getModeratorsView().values().stream() + .filter(BlightedModerator::isVanished) + .forEach(moderator -> joiningPlayer.hidePlayer( + BlightedMod.getInstance(), moderator.getPlayer())); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + Player quittingPlayer = event.getPlayer(); + if (moderationManager.isModerator(quittingPlayer)) { + BlightedModerator moderator = moderationManager.getModerator(quittingPlayer); + if (moderator.isVanished()) { + event.setQuitMessage(null); + } + } + + if (!moderationManager.isModerator(quittingPlayer) && moderationManager.isFrozen(quittingPlayer)) { + moderationManager.toggleFreeze(quittingPlayer); + String reason = "Disconnecting while frozen by a moderator"; + String ipAddress = PunishmentManager.getPlayerIp(quittingPlayer); + punishmentManager.addPunishment( + quittingPlayer.getUniqueId(), + quittingPlayer.getName(), + PunishmentData.PunishmentType.BAN, + reason, + quittingPlayer.getUniqueId(), + "CONSOLE", + null, + ipAddress + ); + + String notification = PREFIX + "§c" + quittingPlayer.getName() + "§7 disconnected while frozen and was automatically banned."; + moderationManager.broadcastToModerators(notification); + } + + moderationManager.handlePlayerQuit(quittingPlayer); + } + + @EventHandler + public void onServerListPing(ServerListPingEvent event) { + Iterator iterator = event.iterator(); + while (iterator.hasNext()) { + Player player = iterator.next(); + if (moderationManager.isModerator(player)) { + BlightedModerator moderator = moderationManager.getModerator(player); + if (moderator != null && moderator.isVanished()) { + iterator.remove(); + } + } + } + } + + @EventHandler + public void onFrozenPlayerMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + if (moderationManager.isModerator(player) || !moderationManager.isFrozen(player)) return; + + boolean movedHorizontally = event.getFrom().getX() != Objects.requireNonNull(event.getTo()).getX() + || event.getFrom().getZ() != event.getTo().getZ(); + + if (movedHorizontally) event.setTo(event.getFrom()); + } + + @EventHandler + public void onFrozenPlayerCommand(PlayerCommandPreprocessEvent event) { + Player player = event.getPlayer(); + if (moderationManager.isModerator(player) || !moderationManager.isFrozen(player)) return; + + event.setCancelled(true); + player.sendMessage("§cYou cannot execute commands while frozen by a moderator."); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPrivateMessageSpy(PlayerCommandPreprocessEvent event) { + String rawCommandLine = event.getMessage(); + if (rawCommandLine == null || !rawCommandLine.startsWith("/")) { + return; + } + + String[] parts = rawCommandLine.substring(1).trim().split("\\s+"); + if (parts.length == 0 || parts[0].isEmpty()) { + return; + } + + String commandLabel = parts[0].toLowerCase(Locale.ROOT); + Player sender = event.getPlayer(); + + if (Set.of("msg", "tell", "w", "whisper", "pm").contains(commandLabel)) { + if (parts.length < 3) { + return; + } + + String targetName = parts[1]; + Player targetPlayer = Bukkit.getPlayer(targetName); + String recipientName = targetPlayer != null ? targetPlayer.getName() : targetName; + String messageContent = String.join(" ", Arrays.copyOfRange(parts, 2, parts.length)); + + if (targetPlayer != null) { + moderationManager.setLastMessageTarget(sender.getUniqueId(), targetPlayer.getUniqueId()); + } + + moderationManager.broadcastSpyMessage(sender.getName(), recipientName, messageContent); + } else if (Set.of("r", "reply").contains(commandLabel)) { + if (parts.length < 2) { + return; + } + + UUID lastTargetId = moderationManager.getLastMessageTarget(sender.getUniqueId()); + String recipientName = "Unknown"; + if (lastTargetId != null) { + Player lastTargetPlayer = Bukkit.getPlayer(lastTargetId); + if (lastTargetPlayer != null) { + recipientName = lastTargetPlayer.getName(); + } + } + + String messageContent = String.join(" ", Arrays.copyOfRange(parts, 1, parts.length)); + moderationManager.broadcastSpyMessage(sender.getName(), recipientName, messageContent); + } + } + + @EventHandler + public void onFrozenPlayerAttack(EntityDamageByEntityEvent event) { + if (!(event.getDamager() instanceof Player attacker)) return; + if (moderationManager.isModerator(attacker) || !moderationManager.isFrozen(attacker)) return; + + event.setCancelled(true); + attacker.sendMessage("§cYou cannot attack while frozen."); + } + + @EventHandler + public void onModeratorDropItem(PlayerDropItemEvent event) { + Player player = event.getPlayer(); + if (moderationManager.isInModerationMode(player) || (!moderationManager.isModerator(player) && moderationManager.isFrozen(player))) { + event.setCancelled(true); + } + } + + @EventHandler + public void onModeratorPickupItem(EntityPickupItemEvent event) { + if (event.getEntity() instanceof Player player) { + if (moderationManager.isInModerationMode(player) || (!moderationManager.isModerator(player) && moderationManager.isFrozen(player))) { + event.setCancelled(true); + } + } + } + + @EventHandler + public void onModeratorBreakBlock(BlockBreakEvent event) { + if (moderationManager.isInModerationMode(event.getPlayer())) event.setCancelled(true); + } + + @EventHandler + public void onModeratorPlaceBlock(BlockPlaceEvent event) { + if (moderationManager.isInModerationMode(event.getPlayer())) event.setCancelled(true); + } + + /** Non-entity damage (fall, fire, drown, etc.) against a moderator in moderation mode. */ + @EventHandler + public void onModeratorTakeDamage(EntityDamageEvent event) { + if (event.getEntity() instanceof Player player && moderationManager.isInModerationMode(player)) { + event.setCancelled(true); + } + } + + @EventHandler + public void onModeratorEntityDamage(EntityDamageByEntityEvent event) { + if (event.getEntity() instanceof Player victim && moderationManager.isInModerationMode(victim)) { + event.setCancelled(true); + return; + } + + if (!(event.getDamager() instanceof Player attacker)) return; + if (!moderationManager.isInModerationMode(attacker)) return; + + Material tool = attacker.getInventory().getItemInMainHand().getType(); + if (tool == Material.STICK) { + event.setDamage(0.0); + } else { + event.setCancelled(true); + } + } + + @EventHandler + public void onModeratorInventoryClick(InventoryClickEvent event) { + if (event.getWhoClicked() instanceof Player player) { + if (moderationManager.isInModerationMode(player) || (!moderationManager.isModerator(player) && moderationManager.isFrozen(player))) { + event.setCancelled(true); + } + } + } + + private void handleToolInteraction(Player player, Material tool) { + switch (tool) { + case ENDER_EYE -> teleportToRandomPlayer(player); + case BOOK -> openReportsMenu(player); + case PURPLE_DYE, GRAY_DYE -> toggleVanish(player); + default -> { } + } + } + + private void openReportsMenu(Player moderator) { + moderator.performCommand("reports"); + } + + private void handleEntityInteraction(Player player, Player target, Material tool) { + switch (tool) { + case CHEST -> openInventory(player, target); + case PACKED_ICE -> toggleFreeze(player, target); + default -> { } + } + } + + private void toggleVanish(Player player) { + BlightedModerator moderator = moderationManager.getModerator(player); + moderator.setVanished(!moderator.isVanished()); + } + + private void openInventory(Player moderator, Player target) { + new InvSeeMenu(target).open(moderator); + } + + private void toggleFreeze(Player moderator, Player target) { + boolean isFrozen = moderationManager.toggleFreeze(target); + String status = isFrozen ? "§bFROZEN" : "§aUNFROZEN"; + + moderator.sendMessage(PREFIX + target.getName() + " is now " + status); + + if (isFrozen) { + target.sendMessage("§c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); + target.sendMessage("§cDo not log out or you will be banned."); + } else { + target.sendMessage("§aYou have been unfrozen."); + } + } + + private void teleportToRandomPlayer(Player moderator) { + List eligiblePlayers = new ArrayList<>(Bukkit.getOnlinePlayers()); + eligiblePlayers.remove(moderator); + + if (eligiblePlayers.isEmpty()) { + moderator.sendMessage("§cNo other players online."); + return; + } + + Player target = eligiblePlayers.get(random.nextInt(eligiblePlayers.size())); + moderator.teleport(target.getLocation()); + moderationManager.getModerator(moderator).setTargetPlayer(target); + + InteractiveMessage.text(PREFIX + "Teleported to §d" + target.getName() + " ") + .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + target.getName() + "§7 (/userinfo)", "/userinfo " + target.getName()) + .send(moderator); + } +} diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/menus/EnderSeeMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/EnderseeMenu.java similarity index 90% rename from blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/menus/EnderSeeMenu.java rename to blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/EnderseeMenu.java index 0953e724..74d3595a 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/menus/EnderSeeMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/EnderseeMenu.java @@ -1,4 +1,4 @@ -package fr.moussax.blightedSMP.engine.player.menus; +package fr.moussax.blightedMod.moderator.menus; import fr.moussax.bedrock.ui.menu.Menu; import fr.moussax.bedrock.ui.menu.TickableMenu; @@ -11,15 +11,15 @@ import java.util.Objects; -public final class EnderSeeMenu extends Menu implements TickableMenu { +public final class EnderseeMenu extends Menu implements TickableMenu { private final Player target; private final Menu previousMenu; - public EnderSeeMenu(Player target) { + public EnderseeMenu(Player target) { this(target, null); } - public EnderSeeMenu(Player target, @Nullable Menu previousMenu) { + public EnderseeMenu(Player target, @Nullable Menu previousMenu) { super(target.getName() + "'s Ender Chest", 36); this.target = target; this.previousMenu = previousMenu; diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/menus/InvSeeMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java similarity index 97% rename from blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/menus/InvSeeMenu.java rename to blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java index 2ed23650..5fa022cc 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/menus/InvSeeMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java @@ -1,4 +1,4 @@ -package fr.moussax.blightedSMP.engine.player.menus; +package fr.moussax.blightedMod.moderator.menus; import fr.moussax.bedrock.ui.menu.Menu; import fr.moussax.bedrock.ui.menu.TickableMenu; @@ -42,7 +42,7 @@ public void build(Player player) { .setDisplayName("§dView Ender Chest") .addLore("§7Click to view ender chest.") .toItemStack(), - (_, _) -> openSubMenu(new EnderSeeMenu(target, this)) + (_, _) -> openSubMenu(new EnderseeMenu(target, this)) ); updateContents(); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java new file mode 100644 index 00000000..ed1816f3 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java @@ -0,0 +1,170 @@ +package fr.moussax.blightedMod.moderator.menus; + +import fr.moussax.bedrock.ui.menu.TickableMenu; +import fr.moussax.bedrock.ui.menu.interaction.MenuElementPreset; +import fr.moussax.bedrock.ui.menu.interaction.MenuItemInteraction; +import fr.moussax.bedrock.ui.menu.types.PaginatedMenu; +import fr.moussax.bedrock.utils.ItemBuilder; +import fr.moussax.blightedMod.moderator.ModerationManager; +import fr.moussax.blightedMod.moderator.reports.ReportData; +import fr.moussax.blightedMod.moderator.reports.ReportManager; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NonNull; + +import java.util.List; + +import static fr.moussax.bedrock.text.Messenger.inform; +import static fr.moussax.bedrock.text.Messenger.warn; + +/** + * Paginated menu displaying active player reports for staff review. + * Ticks periodically to support real-time updates while open. + */ +public final class ReportsCenterMenu extends PaginatedMenu implements TickableMenu { + + private static final int[] REPORT_SLOTS = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34, + 37, 38, 39, 40, 41, 42, 43 + }; + + public ReportsCenterMenu() { + super("§9Active Reports", 54); + } + + @Override + public long tickPeriodTicks() { + return 20L; + } + + @Override + public void onTick(Player player) { + refresh(player); + } + + @Override + protected int getTotalItems(@NonNull Player player) { + return ReportManager.getInstance().getActiveReports().size(); + } + + @Override + protected int getItemsPerPage() { + return REPORT_SLOTS.length; + } + + @Override + protected ItemStack getItem(@NonNull Player player, int index) { + List reports = ReportManager.getInstance().getActiveReports(); + if (index >= reports.size()) { + return new ItemStack(Material.AIR); + } + + ReportData report = reports.get(index); + long elapsedSeconds = (System.currentTimeMillis() - report.timestamp()) / 1000L; + String timeAgo = elapsedSeconds < 60 ? elapsedSeconds + "s ago" : (elapsedSeconds / 60) + "m ago"; + + ItemBuilder builder = new ItemBuilder(Material.PAPER) + .setDisplayName("§eReport: §b" + report.targetName()) + .addLore("§7Report ID: §f#" + report.id()) + .addLore("§7Target: §b" + report.targetName()) + .addLore("§7Reporter: §e" + report.reporterName()) + .addLore("§7Reason: §c" + report.reason()); + + if (report.message() != null && !report.message().isBlank()) { + builder.addLore("§7Message: §f\"" + report.message() + "\""); + } + + builder.addLore("§7Submitted: §e" + timeAgo) + .addLore("") + .addLore("§eLeft-Click §7to teleport & target") + .addLore("§cRight-Click §7to dismiss"); + + return builder.toItemStack(); + } + + @Override + public void build(@NonNull Player viewer) { + totalItems = Math.max(0, getTotalItems(viewer)); + + int itemsPerPage = getItemsPerPage(); + int maxPage = Math.max(0, (totalItems - 1) / itemsPerPage); + currentPage = Math.min(currentPage, maxPage); + + int startIndex = currentPage * itemsPerPage; + int endIndex = Math.min(startIndex + itemsPerPage, totalItems); + + clearInventory(); + + List reports = ReportManager.getInstance().getActiveReports(); + if (reports.isEmpty()) { + ItemStack noReportsItem = new ItemBuilder(Material.RED_STAINED_GLASS_PANE) + .setDisplayName("§cNo Active Reports") + .addLore("§7There are currently no active reports.") + .toItemStack(); + + setItem(22, noReportsItem, MenuItemInteraction.ANY_CLICK, (player, _) -> { }); + } else { + int slotIndex = 0; + for (int i = startIndex; i < endIndex && slotIndex < REPORT_SLOTS.length; i++, slotIndex++) { + final int index = i; + setItem( + REPORT_SLOTS[slotIndex], + getItem(viewer, index), + MenuItemInteraction.ANY_CLICK, + (player, click) -> onItemClick(player, index, click) + ); + } + } + + if (currentPage > 0) { + setBackButton(48, (player, _) -> { + currentPage--; + refresh(player); + }); + } + + if (endIndex < totalItems) { + setItem(50, MenuElementPreset.NEXT_BUTTON, (player, _) -> { + currentPage++; + refresh(player); + }); + } + + setCloseButton(49); + } + + @Override + protected void onItemClick(@NonNull Player moderator, int index, @NonNull ClickType clickType) { + List reports = ReportManager.getInstance().getActiveReports(); + if (index >= reports.size()) { + return; + } + + ReportData report = reports.get(index); + + if (clickType.isRightClick()) { + ReportManager.getInstance().dismissReport(report.id()); + inform(moderator, "§aDismissed report #" + report.id() + " against §f" + report.targetName()); + refresh(moderator); + return; + } + + if (clickType.isLeftClick()) { + Player target = Bukkit.getPlayerExact(report.targetName()); + if (target == null) { + warn(moderator, "Target player §4" + report.targetName() + " §cis no longer online."); + return; + } + + moderator.teleport(target.getLocation()); + ModerationManager.getInstance().getModerator(moderator).setTargetPlayer(target); + inform(moderator, "§aTeleported to §f" + target.getName() + " §aand set as HUD target."); + close(); + } + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsPaginatedMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsPaginatedMenu.java new file mode 100644 index 00000000..7e5a5b09 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsPaginatedMenu.java @@ -0,0 +1,222 @@ +package fr.moussax.blightedMod.moderator.menus; + +import fr.moussax.bedrock.ui.menu.TickableMenu; +import fr.moussax.bedrock.ui.menu.interaction.MenuElementPreset; +import fr.moussax.bedrock.ui.menu.interaction.MenuItemInteraction; +import fr.moussax.bedrock.ui.menu.types.PaginatedMenu; +import fr.moussax.bedrock.utils.ItemBuilder; +import fr.moussax.blightedMod.moderator.punishments.PunishmentData; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.jspecify.annotations.NonNull; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * Paginated menu displaying historical sanctions for a specific player. + * Features target player head, Hypixel-style hopper filtering, and real-time updates. + */ +public final class SanctionsPaginatedMenu extends PaginatedMenu implements TickableMenu { + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + private static final int[] SANCTION_SLOTS = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34, + 37, 38, 39, 40, 41, 42, 43 + }; + + public enum SanctionFilter { + ALL("All Sanctions"), + ACTIVE("Active Only"), + BAN("Bans Only"), + MUTE("Mutes Only"), + KICK("Kicks Only"); + + private final String displayName; + + SanctionFilter(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + public SanctionFilter next() { + SanctionFilter[] values = values(); + return values[(ordinal() + 1) % values.length]; + } + } + + private final String targetName; + private final List punishments; + private SanctionFilter currentFilter = SanctionFilter.ALL; + + public SanctionsPaginatedMenu(String targetName, List punishments) { + super("§9Sanctions for " + targetName, 54); + this.targetName = targetName; + this.punishments = punishments; + } + + @Override + public long tickPeriodTicks() { + return 20L; + } + + @Override + public void onTick(Player player) { + refresh(player); + } + + private List getFilteredPunishments() { + return punishments.stream().filter(punishment -> switch (currentFilter) { + case ALL -> true; + case ACTIVE -> punishment.active() && !punishment.isExpired(); + case BAN -> punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN; + case MUTE -> punishment.type() == PunishmentData.PunishmentType.MUTE; + case KICK -> punishment.type() == PunishmentData.PunishmentType.KICK; + }).toList(); + } + + @Override + protected int getTotalItems(@NonNull Player player) { + return getFilteredPunishments().size(); + } + + @Override + protected int getItemsPerPage() { + return SANCTION_SLOTS.length; + } + + @Override + protected ItemStack getItem(@NonNull Player player, int index) { + List filtered = getFilteredPunishments(); + if (index >= filtered.size()) { + return new ItemStack(Material.AIR); + } + + PunishmentData punishment = filtered.get(index); + Material icon = switch (punishment.type()) { + case BAN, IP_BAN -> Material.REDSTONE_BLOCK; + case MUTE -> Material.WRITABLE_BOOK; + case KICK -> Material.LEATHER_BOOTS; + }; + + String dateText = DATE_FORMAT.format(new Date(punishment.createdAt())); + String durationText = punishment.expiresAt() == null ? "Permanent" : DATE_FORMAT.format(new Date(punishment.expiresAt())); + String activeStatus = punishment.active() && !punishment.isExpired() ? "§aACTIVE" : "§cEXPIRED"; + + return new ItemBuilder(icon) + .setDisplayName("§c" + punishment.type().name().replace("_", " ") + " §f| §e#" + punishment.id()) + .addLore("§7Target: §b" + targetName) + .addLore("§7Issued By: §e" + punishment.moderatorName()) + .addLore("§7Reason: §f" + punishment.reason()) + .addLore("§7Issued At: §e" + dateText) + .addLore("§7Expires: §f" + durationText) + .addLore("§7Status: " + activeStatus) + .toItemStack(); + } + + @Override + public void build(@NonNull Player viewer) { + List filtered = getFilteredPunishments(); + totalItems = Math.max(0, filtered.size()); + + int itemsPerPage = getItemsPerPage(); + int maxPage = Math.max(0, (totalItems - 1) / itemsPerPage); + currentPage = Math.min(currentPage, maxPage); + + int startIndex = currentPage * itemsPerPage; + int endIndex = Math.min(startIndex + itemsPerPage, totalItems); + + clearInventory(); + + renderTargetHead(); + + if (filtered.isEmpty()) { + ItemStack noSanctionsItem = new ItemBuilder(Material.RED_STAINED_GLASS_PANE) + .setDisplayName("§cNo Sanctions Found") + .addLore("§7No sanctions match the current filter.") + .toItemStack(); + + setItem(22, noSanctionsItem, MenuItemInteraction.ANY_CLICK, (player, _) -> { }); + } else { + int slotIndex = 0; + for (int i = startIndex; i < endIndex && slotIndex < SANCTION_SLOTS.length; i++, slotIndex++) { + final int index = i; + setItem( + SANCTION_SLOTS[slotIndex], + getItem(viewer, index), + MenuItemInteraction.ANY_CLICK, + (player, click) -> onItemClick(player, index, click) + ); + } + } + + if (currentPage > 0) { + setBackButton(48, (player, _) -> { + currentPage--; + refresh(player); + }); + } + + if (endIndex < totalItems) { + setItem(50, MenuElementPreset.NEXT_BUTTON, (player, _) -> { + currentPage++; + refresh(player); + }); + } + + renderFilterHopper(); + setCloseButton(49); + } + + private void renderTargetHead() { + ItemStack headStack = new ItemStack(Material.PLAYER_HEAD); + SkullMeta skullMeta = (SkullMeta) headStack.getItemMeta(); + if (skullMeta != null) { + skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(targetName)); + skullMeta.setDisplayName("§eTarget: §b" + targetName); + + long activeBans = punishments.stream().filter(p -> (p.type() == PunishmentData.PunishmentType.BAN || p.type() == PunishmentData.PunishmentType.IP_BAN) && p.active() && !p.isExpired()).count(); + long activeMutes = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.MUTE && p.active() && !p.isExpired()).count(); + long totalKicks = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.KICK).count(); + + skullMeta.setLore(List.of( + "§7Total Sanctions: §e" + punishments.size(), + "§7Active Bans: §c" + activeBans, + "§7Active Mutes: §d" + activeMutes, + "§7Total Kicks: §f" + totalKicks + )); + headStack.setItemMeta(skullMeta); + } + setItem(4, headStack, MenuItemInteraction.ANY_CLICK, (player, _) -> { }); + } + + private void renderFilterHopper() { + ItemBuilder hopperBuilder = new ItemBuilder(Material.HOPPER) + .setDisplayName("§eFilter: §b" + currentFilter.getDisplayName()) + .addLore("§7Current: §a" + currentFilter.getDisplayName()) + .addLore("") + .addLore("§7Filters:") + .addLore(currentFilter == SanctionFilter.ALL ? " §b» All Sanctions" : " §7 All Sanctions") + .addLore(currentFilter == SanctionFilter.ACTIVE ? " §b» Active Only" : " §7 Active Only") + .addLore(currentFilter == SanctionFilter.BAN ? " §b» Bans Only" : " §7 Bans Only") + .addLore(currentFilter == SanctionFilter.MUTE ? " §b» Mutes Only" : " §7 Mutes Only") + .addLore(currentFilter == SanctionFilter.KICK ? " §b» Kicks Only" : " §7 Kicks Only") + .addLore("") + .addLore("§eClick to cycle filter!"); + + setItem(52, hopperBuilder.toItemStack(), MenuItemInteraction.ANY_CLICK, (player, _) -> { + currentFilter = currentFilter.next(); + currentPage = 0; + refresh(player); + }); + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java new file mode 100644 index 00000000..701790cb --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java @@ -0,0 +1,71 @@ +package fr.moussax.blightedMod.moderator.punishments; + +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class DurationParser { + private static final Pattern DURATION_PATTERN = Pattern.compile("(\\d+)([smhdwy])"); + + public static Long parseDurationSeconds(String input) { + if (input == null || input.isEmpty()) { + return null; + } + + Matcher matcher = DURATION_PATTERN.matcher(input.toLowerCase()); + if (!matcher.matches()) { + try { + return Long.parseLong(input); + } catch (NumberFormatException _) { + return null; + } + } + + long value = Long.parseLong(matcher.group(1)); + String unit = matcher.group(2); + + return switch (unit) { + case "s" -> value; + case "m" -> value * 60L; + case "h" -> value * 3600L; + case "d" -> value * 86400L; + case "w" -> value * 604800L; + case "y" -> value * 31536000L; + default -> 0L; + }; + } + + public static Long parseDuration(String input) { + Long durationSeconds = parseDurationSeconds(input); + if (durationSeconds == null) { + return null; + } + return System.currentTimeMillis() + (durationSeconds * 1000L); + } + + public static String formatDuration(String input) { + Matcher matcher = DURATION_PATTERN.matcher(input.toLowerCase()); + if (!matcher.matches()) { + return input; + } + + String value = matcher.group(1); + String unit = matcher.group(2); + + String unitName = switch (unit) { + case "s" -> "second"; + case "m" -> "minute"; + case "h" -> "hour"; + case "d" -> "day"; + case "w" -> "week"; + case "y" -> "year"; + default -> ""; + }; + + int parsedAmount = Integer.parseInt(value); + if (parsedAmount > 1) { + unitName += "s"; + } + return value + " " + unitName; + } +} \ No newline at end of file diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentData.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentData.java new file mode 100644 index 00000000..55700b43 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentData.java @@ -0,0 +1,30 @@ +package fr.moussax.blightedMod.moderator.punishments; + +import java.util.UUID; + +/** + * Record data carrier for player punishment records. + */ +public record PunishmentData( + int id, + UUID playerUuid, + String playerName, + PunishmentType type, + String reason, + UUID moderatorUuid, + String moderatorName, + long createdAt, + Long expiresAt, + boolean active, + String ipAddress +) { + public boolean isPermanent() { + return expiresAt == null; + } + + public boolean isExpired() { + return expiresAt != null && System.currentTimeMillis() > expiresAt; + } + + public enum PunishmentType {MUTE, KICK, BAN, IP_BAN} +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java new file mode 100644 index 00000000..31706a95 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java @@ -0,0 +1,235 @@ +package fr.moussax.blightedMod.moderator.punishments; + +import org.bukkit.entity.Player; + +import java.net.InetSocketAddress; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public final class PunishmentManager { + private final Connection connection; + + public PunishmentManager(Connection connection) { + this.connection = connection; + } + + public void addPunishment(UUID playerUuid, String playerName, PunishmentData.PunishmentType type, + String reason, UUID moderatorUuid, String moderatorName, + Long expiresAt, String ipAddress) { + String query = """ + INSERT INTO punishments (player_uuid, player_name, punishment_type, reason, + moderator_uuid, moderator_name, created_at, expires_at, ip_address) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + synchronized (connection) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, playerUuid.toString()); + statement.setString(2, playerName); + statement.setString(3, type.name()); + statement.setString(4, reason); + statement.setString(5, moderatorUuid.toString()); + statement.setString(6, moderatorName); + statement.setLong(7, System.currentTimeMillis()); + if (expiresAt != null) statement.setLong(8, expiresAt); + else statement.setNull(8, Types.INTEGER); + statement.setString(9, ipAddress); + statement.executeUpdate(); + } catch (SQLException exception) { + throw new RuntimeException("Failed to add punishment", exception); + } + } + } + + public void removePunishment(UUID playerUuid, PunishmentData.PunishmentType type) { + String query = """ + UPDATE punishments SET is_active = 0 + WHERE player_uuid = ? AND punishment_type = ? AND is_active = 1 + """; + + synchronized (connection) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, playerUuid.toString()); + statement.setString(2, type.name()); + statement.executeUpdate(); + } catch (SQLException exception) { + throw new RuntimeException("Failed to remove punishment", exception); + } + } + } + + public void removeIpPunishment(String ipAddress) { + String query = """ + UPDATE punishments SET is_active = 0 + WHERE ip_address = ? AND punishment_type = 'IP_BAN' AND is_active = 1 + """; + + synchronized (connection) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, ipAddress); + statement.executeUpdate(); + } catch (SQLException exception) { + throw new RuntimeException("Failed to remove IP ban", exception); + } + } + } + + public PunishmentData getActivePunishment(UUID playerUuid, PunishmentData.PunishmentType type) { + String query = """ + SELECT * FROM punishments + WHERE player_uuid = ? AND punishment_type = ? AND is_active = 1 + ORDER BY created_at DESC LIMIT 1 + """; + + PunishmentData punishment; + synchronized (connection) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, playerUuid.toString()); + statement.setString(2, type.name()); + + try (ResultSet resultSet = statement.executeQuery()) { + if (!resultSet.next()) return null; + punishment = mapResultSet(resultSet); + } + } catch (SQLException exception) { + throw new RuntimeException("Failed to get punishment", exception); + } + } + + if (punishment.isExpired()) { + removePunishment(playerUuid, type); + return null; + } + return punishment; + } + + public PunishmentData getActiveIpBan(String ipAddress) { + String query = """ + SELECT * FROM punishments + WHERE ip_address = ? AND punishment_type = 'IP_BAN' AND is_active = 1 + ORDER BY created_at DESC LIMIT 1 + """; + + PunishmentData punishment; + synchronized (connection) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, ipAddress); + + try (ResultSet resultSet = statement.executeQuery()) { + if (!resultSet.next()) return null; + punishment = mapResultSet(resultSet); + } + } catch (SQLException exception) { + throw new RuntimeException("Failed to get IP ban", exception); + } + } + + if (punishment.isExpired()) { + removeIpPunishment(ipAddress); + return null; + } + return punishment; + } + + public boolean isMuted(UUID playerUuid) { + return getActivePunishment(playerUuid, PunishmentData.PunishmentType.MUTE) != null; + } + + public boolean isBanned(UUID playerUuid) { + return getActivePunishment(playerUuid, PunishmentData.PunishmentType.BAN) != null; + } + + public boolean isIpBanned(String ipAddress) { + return getActiveIpBan(ipAddress) != null; + } + + public void addBan(Player target, Player moderator, String reason, Long expiresAt) { + addPunishment( + target.getUniqueId(), + target.getName(), + PunishmentData.PunishmentType.BAN, + reason, + moderator.getUniqueId(), + moderator.getName(), + expiresAt, + getPlayerIp(target) + ); + } + + public void addMute(Player target, Player moderator, String reason, Long expiresAt) { + addPunishment( + target.getUniqueId(), + target.getName(), + PunishmentData.PunishmentType.MUTE, + reason, + moderator.getUniqueId(), + moderator.getName(), + expiresAt, + getPlayerIp(target) + ); + } + + public void addIpBan(Player target, Player moderator, String reason, Long expiresAt) { + addPunishment( + target.getUniqueId(), + target.getName(), + PunishmentData.PunishmentType.IP_BAN, + reason, + moderator.getUniqueId(), + moderator.getName(), + expiresAt, + getPlayerIp(target) + ); + } + + private PunishmentData mapResultSet(ResultSet resultSet) throws SQLException { + Long expiresAt = resultSet.getLong("expires_at"); + if (resultSet.wasNull()) expiresAt = null; + + return new PunishmentData( + resultSet.getInt("id"), + UUID.fromString(resultSet.getString("player_uuid")), + resultSet.getString("player_name"), + PunishmentData.PunishmentType.valueOf(resultSet.getString("punishment_type")), + resultSet.getString("reason"), + UUID.fromString(resultSet.getString("moderator_uuid")), + resultSet.getString("moderator_name"), + resultSet.getLong("created_at"), + expiresAt, + resultSet.getInt("is_active") == 1, + resultSet.getString("ip_address") + ); + } + + public List getAllPunishments(String playerName) { + String query = """ + SELECT * FROM punishments + WHERE LOWER(player_name) = LOWER(?) + ORDER BY created_at DESC + """; + + List list = new ArrayList<>(); + synchronized (connection) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, playerName); + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + list.add(mapResultSet(resultSet)); + } + } + } catch (SQLException exception) { + throw new RuntimeException("Failed to fetch player punishments", exception); + } + } + return list; + } + + public static String getPlayerIp(Player player) { + InetSocketAddress address = player.getAddress(); + if (address == null || address.getAddress() == null) return "0.0.0.0"; + String host = address.getAddress().getHostAddress(); + return host != null ? host : "0.0.0.0"; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportData.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportData.java new file mode 100644 index 00000000..3202635f --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportData.java @@ -0,0 +1,14 @@ +package fr.moussax.blightedMod.moderator.reports; + +/** + * Immutable data carrier for a player report. + */ +public record ReportData( + int id, + String reporterName, + String targetName, + String reason, + String message, + long timestamp +) { +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java new file mode 100644 index 00000000..2a6dd545 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java @@ -0,0 +1,43 @@ +package fr.moussax.blightedMod.moderator.reports; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public final class ReportManager { + private static final ReportManager INSTANCE = new ReportManager(); + + private final List reports = Collections.synchronizedList(new ArrayList<>()); + private final AtomicInteger idCounter = new AtomicInteger(1); + + private ReportManager() { + } + + public static ReportManager getInstance() { + return INSTANCE; + } + + public ReportData submitReport(String reporterName, String targetName, String reason, String message) { + ReportData report = new ReportData( + idCounter.getAndIncrement(), + reporterName, + targetName, + reason, + message, + System.currentTimeMillis() + ); + reports.addFirst(report); + return report; + } + + public List getActiveReports() { + synchronized (reports) { + return new ArrayList<>(reports); + } + } + + public boolean dismissReport(int reportId) { + return reports.removeIf(report -> report.id() == reportId); + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java new file mode 100644 index 00000000..be3f9334 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java @@ -0,0 +1,94 @@ +package fr.moussax.blightedMod.registry; + +import fr.moussax.bedrock.commands.CommandRegistrar; +import fr.moussax.bedrock.commands.TabSuggestionRegistry; +import fr.moussax.blightedMod.commands.impl.BanCommand; +import fr.moussax.blightedMod.commands.impl.EnderseeCommand; +import fr.moussax.blightedMod.commands.impl.FreezeCommand; +import fr.moussax.blightedMod.commands.impl.InvseeCommand; +import fr.moussax.blightedMod.commands.impl.KickCommand; +import fr.moussax.blightedMod.commands.impl.ModCommand; +import fr.moussax.blightedMod.commands.impl.ModTpCommands; +import fr.moussax.blightedMod.commands.impl.MuteCommand; +import fr.moussax.blightedMod.commands.impl.ReportCommand; +import fr.moussax.blightedMod.commands.impl.ReportsCommand; +import fr.moussax.blightedMod.commands.impl.SanctionsCommand; +import fr.moussax.blightedMod.commands.impl.SetSlowmodeCommand; +import fr.moussax.blightedMod.commands.impl.SocialSpyCommand; +import fr.moussax.blightedMod.commands.impl.TargetCommand; +import fr.moussax.blightedMod.commands.impl.UserInfoCommand; +import fr.moussax.blightedMod.commands.impl.VanishCommand; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +public final class CommandsRegistry { + + private CommandsRegistry() { + } + + public static void registerCommands(JavaPlugin plugin) { + TabSuggestionRegistry suggestions = createSuggestionRegistry(); + CommandRegistrar registrar = new CommandRegistrar(plugin, suggestions); + + ModTpCommands teleportCommands = new ModTpCommands(); + registrar.register("mtp", teleportCommands); + registrar.register("mtphere", teleportCommands); + registrar.register("vanish", new VanishCommand()); + + MuteCommand muteCommand = new MuteCommand(); + registrar.register("mute", muteCommand); + registrar.register("unmute", muteCommand); + + registrar.register("mod", new ModCommand()); + registrar.register("invsee", new InvseeCommand()); + registrar.register("endersee", new EnderseeCommand()); + registrar.register("kick", new KickCommand()); + + BanCommand banCommand = new BanCommand(); + registrar.register("ban", banCommand); + registrar.register("unban", banCommand); + registrar.register("banip", banCommand); + registrar.register("unbanip", banCommand); + + registrar.register("freeze", new FreezeCommand()); + registrar.register("report", new ReportCommand()); + + SetSlowmodeCommand slowmodeCommand = new SetSlowmodeCommand(); + registrar.register("slowmode", slowmodeCommand); + registrar.register("setslowmode", slowmodeCommand); + + registrar.register("reports", new ReportsCommand()); + registrar.register("target", new TargetCommand()); + registrar.register("sanctions", new SanctionsCommand()); + registrar.register("userinfo", new UserInfoCommand()); + registrar.register("chat", new fr.moussax.blightedMod.commands.impl.ChatCommand()); + + SocialSpyCommand socialSpyCommand = new SocialSpyCommand(); + registrar.register("msginspect", socialSpyCommand); + registrar.register("spy", socialSpyCommand); + registrar.register("socialspy", socialSpyCommand); + } + + + + + + + + + + private static TabSuggestionRegistry createSuggestionRegistry() { + TabSuggestionRegistry suggestions = new TabSuggestionRegistry(); + + suggestions.register("$players", () -> Bukkit.getOnlinePlayers() + .stream() + .map(Player::getName) + .toList() + ); + + suggestions.register("$channels", () -> java.util.List.of("staff", "all")); + + return suggestions; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java new file mode 100644 index 00000000..3f7242ad --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java @@ -0,0 +1,54 @@ +package fr.moussax.blightedMod.registry; + +import fr.moussax.bedrock.ui.actionbar.ActionbarService; +import fr.moussax.bedrock.ui.menu.system.MenuListener; +import fr.moussax.bedrock.ui.menu.system.MenuSystem; +import fr.moussax.bedrock.ui.sign.SignInputListener; +import fr.moussax.blightedMod.BlightedMod; +import fr.moussax.blightedMod.moderator.listeners.InteractiveChatListener; +import fr.moussax.blightedMod.moderator.listeners.ModerationListener; +import org.bukkit.Bukkit; +import org.bukkit.plugin.PluginManager; + +import fr.moussax.blightedMod.moderator.hud.ModerationHud; + +public final class EventsRegistry { + + private final BlightedMod instance = BlightedMod.getInstance(); + private MenuSystem menuSystem; + private SignInputListener signInputListener; + private ActionbarService actionBarService; + + public void initializeListeners() { + PluginManager manager = Bukkit.getPluginManager(); + + signInputListener = new SignInputListener(); + menuSystem = new MenuSystem(instance); + actionBarService = new ActionbarService(instance); + actionBarService.start(10L); + + actionBarService.registerSection(ModerationHud.createSection(instance.getModerationManager())); + + manager.registerEvents(signInputListener, instance); + manager.registerEvents(new MenuListener(menuSystem), instance); + manager.registerEvents(new ModerationListener(instance.getModerationManager()), instance); + manager.registerEvents(new InteractiveChatListener(), instance); + } + + + public void cleanup() { + if (signInputListener != null) { + signInputListener.cleanup(); + } + if (actionBarService != null) { + actionBarService.stop(); + } + } + + public void shutdownMenus() { + if (menuSystem != null) { + menuSystem.shutdown(); + } + } +} + diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/utils/PluginPermissions.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/utils/PluginPermissions.java new file mode 100644 index 00000000..f2c74bd8 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/utils/PluginPermissions.java @@ -0,0 +1,17 @@ +package fr.moussax.blightedMod.utils; + +public enum PluginPermissions { + MODERATOR("blightedmod.moderator"), + ADMIN("blightedmod.administrator"); + + private final String node; + + PluginPermissions(String node) { + this.node = node; + } + + @Override + public String toString() { + return node; + } +} diff --git a/blighted-mod/src/main/resources/plugin.yml b/blighted-mod/src/main/resources/plugin.yml new file mode 100644 index 00000000..a18a7398 --- /dev/null +++ b/blighted-mod/src/main/resources/plugin.yml @@ -0,0 +1,77 @@ +name: BlightedMod +version: '${version}' +authors: [ MrInspection ] + +main: fr.moussax.blightedMod.BlightedMod +api-version: '26.2' +load: STARTUP + +commands: + mtp: + description: Teleport to a player. + permission: blightedmod.moderator + mtphere: + description: Teleport a player to you. + permission: blightedmod.moderator + vanish: + description: Toggle vanish mode. + aliases: [v] + permission: blightedmod.moderator + mute: + description: Mute a player. + permission: blightedmod.moderator + unmute: + description: Unmute a player. + permission: blightedmod.moderator + mod: + description: Toggle moderation mode or broadcast staff announcement. + permission: blightedmod.moderator + invsee: + description: View and inspect a player's inventory. + permission: blightedmod.moderator + endersee: + description: View and inspect a player's ender chest. + permission: blightedmod.moderator + kick: + description: Kick a player from the server. + permission: blightedmod.moderator + ban: + description: Ban a player. + permission: blightedmod.moderator + unban: + description: Unban a player. + permission: blightedmod.moderator + banip: + description: IP ban a player. + permission: blightedmod.moderator + unbanip: + description: Unban an IP address. + permission: blightedmod.moderator + freeze: + description: Toggle freeze state for a player. + permission: blightedmod.moderator + report: + description: Report a player or message to online moderators. + slowmode: + description: Set or toggle chat slowmode delay in seconds. + aliases: [setslowmode] + permission: blightedmod.moderator + reports: + description: View active player reports. + permission: blightedmod.moderator + target: + description: Manually set or clear targeted player for moderation HUD. + permission: blightedmod.moderator + sanctions: + description: View all recorded sanctions for a player. + permission: blightedmod.moderator + userinfo: + description: Get detailed user information. + permission: blightedmod.moderator + chat: + description: Switch active chat channel (staff, all). + aliases: [c] + msginspect: + description: Toggle private message inspection (SocialSpy). + aliases: [spy, socialspy] + permission: blightedmod.moderator \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4fd3d70e..ed28b61d 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ bedrock blighted-smp + blighted-mod From 2222b37049062512d58ff76370417bf77e1ffe75 Mon Sep 17 00:00:00 2001 From: Moussa Date: Fri, 4 Sep 2026 23:23:02 +0200 Subject: [PATCH 03/30] feat(mod): standardize moderator chat and actionbar formatting Update staff chat, alert, punishment, and freeze messages across command implementations and the moderation HUD to adhere to the standardized visual format. --- .../blightedMod/commands/impl/BanCommand.java | 12 +- .../commands/impl/ChatCommand.java | 7 +- .../commands/impl/CheckReportCommand.java | 50 ++++++++ .../commands/impl/FreezeCommand.java | 18 +-- .../commands/impl/KickCommand.java | 3 +- .../commands/impl/ModTpCommands.java | 15 +-- .../commands/impl/MuteCommand.java | 13 +- .../commands/impl/ReportCommand.java | 34 +++--- .../commands/impl/SanctionsCommand.java | 4 +- .../commands/impl/SetSlowmodeCommand.java | 12 +- .../commands/impl/SocialSpyCommand.java | 4 +- .../commands/impl/TargetCommand.java | 7 +- .../commands/impl/TestModCommand.java | 113 ++++++++++++++++++ .../commands/impl/UserinfoCommand.java | 9 +- .../database/PlayerDataHandler.java | 4 + .../moderator/BlightedModerator.java | 15 +-- .../moderator/ModerationManager.java | 2 +- .../moderator/hud/ModerationHud.java | 19 ++- ...sPaginatedMenu.java => SanctionsMenu.java} | 8 +- 19 files changed, 245 insertions(+), 104 deletions(-) create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/CheckReportCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/database/PlayerDataHandler.java rename blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/{SanctionsPaginatedMenu.java => SanctionsMenu.java} (93%) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java index cbc330f9..63b3840d 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java @@ -17,7 +17,6 @@ */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class BanCommand extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { @@ -88,8 +87,8 @@ private boolean handleBan(Player moderator, String[] arguments) { target.kickPlayer(banMessage); - String notification = PREFIX + "§9" + moderator.getName() + "§7 banned §9" + target.getName() + - "§7 for §c" + reason + "§7 (" + durationText + ")"; + String durationString = expiresAt != null ? " for §6" + durationText + "§e" : " permanently"; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e banned §9" + target.getName() + "§e" + durationString + " for §c" + reason + "§e."; getModerationManager().broadcastToModerators(notification); return true; @@ -113,7 +112,7 @@ private boolean handleUnban(Player moderator, String[] arguments) { getPunishmentManager().removePunishment(target.getUniqueId(), PunishmentData.PunishmentType.BAN); - String notification = PREFIX + "§9" + moderator.getName() + "§7 unbanned §9" + target.getName(); + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unbanned §9" + target.getName() + "§e."; getModerationManager().broadcastToModerators(notification); return true; @@ -171,8 +170,7 @@ private boolean handleBanIp(Player moderator, String[] arguments) { target.kickPlayer(banMessage); - String notification = PREFIX + "§9" + moderator.getName() + "§7 IP banned §9" + target.getName() + - "§7 for §c" + reason + "§7 (" + durationText + ")"; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e IP banned §9" + target.getName() + "§e for §c" + reason + "§e."; getModerationManager().broadcastToModerators(notification); return true; @@ -198,7 +196,7 @@ private boolean handleUnbanIp(Player moderator, String[] arguments) { getPunishmentManager().removeIpPunishment(ipAddress); - String notification = PREFIX + "§9" + moderator.getName() + "§7 unbanned IP for §9" + target.getName(); + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unbanned IP for §d" + target.getName() + "§e."; getModerationManager().broadcastToModerators(notification); return true; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java index 218f72e9..17ddf78d 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java @@ -14,13 +14,12 @@ */ @CommandArgument(position = 0, suggestions = {"$channels"}) public final class ChatCommand extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; @Override protected boolean executeModeration(Player player, Command command, String label, String[] arguments) { if (arguments.length == 0) { ModerationManager.ChatChannel currentChannel = getModerationManager().getChatChannel(player); - inform(player, PREFIX + "Currently in active chat channel: §e" + currentChannel.name()); + inform(player, " §7You are now in the §d" + currentChannel.name() + " §7channel."); warn(player, "Usage: /chat "); return true; } @@ -33,13 +32,13 @@ protected boolean executeModeration(Player player, Command command, String label } getModerationManager().setChatChannel(player, ModerationManager.ChatChannel.STAFF); - inform(player, PREFIX + "Switched active chat channel to §9STAFF§7."); + inform(player, " §7You are now in the §dSTAFF §7channel."); return true; } if (targetChannel.equals("all") || targetChannel.equals("a") || targetChannel.equals("global") || targetChannel.equals("normal")) { getModerationManager().setChatChannel(player, ModerationManager.ChatChannel.ALL); - inform(player, PREFIX + "Switched active chat channel to §bALL§7."); + inform(player, " §7You are now in the §dALL §7channel."); return true; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/CheckReportCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/CheckReportCommand.java new file mode 100644 index 00000000..41efd385 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/CheckReportCommand.java @@ -0,0 +1,50 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.ui.book.BookMenu; +import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.reports.ReportData; +import fr.moussax.blightedMod.moderator.reports.ReportManager; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.warn; + +public final class CheckReportCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length == 0) { + warn(moderator, "Usage: /checkreport "); + return false; + } + + int reportId; + try { + reportId = Integer.parseInt(arguments[0]); + } catch (NumberFormatException _) { + warn(moderator, "Invalid report ID format."); + return false; + } + + ReportData report = ReportManager.getInstance().getReportById(reportId); + if (report == null) { + warn(moderator, "Report #" + reportId + " not found or has been resolved."); + return true; + } + + BookMenu bookMenu = BookMenu.builder() + .addPage(page -> page + .append(" §1§lREPORT DETAILS\n") + .append("§0Report ID: §8#" + report.id() + "\n") + .append("§0Target: §c" + report.targetName() + "\n") + .append("§0Reporter: §9" + report.reporterName() + "\n\n") + .append("§0Reason: §8" + report.reason() + "\n\n") + .append("§0Message:\n") + .append("§8\"§7" + report.message() + "§8\"\n\n") + .hoverAndExecute("§c▶ Dismiss Report", "§eClick to dismiss this report", "/reports dismiss " + report.id()) + ); + + bookMenu.open(moderator); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java index cbc79855..ab7b0f59 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java @@ -12,7 +12,6 @@ */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class FreezeCommand extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { @@ -39,18 +38,13 @@ protected boolean executeModeration(Player moderator, Command command, String la boolean isNowFrozen = getModerationManager().toggleFreeze(target); if (isNowFrozen) { - moderator.sendMessage(PREFIX + target.getName() + " is now §bFROZEN"); - target.sendMessage("§c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); - target.sendMessage("§cDo not log out or you will be banned."); - - String notification = PREFIX + "§9" + moderator.getName() + "§7 froze §9" + target.getName(); - getModerationManager().broadcastToModerators(notification); + moderator.sendMessage("§b ❄ §eYou froze §d" + target.getName() + "§e."); + target.sendMessage("§b ❄ §c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); + target.sendMessage(" §7Do not log out or you will be permanently banned."); } else { - moderator.sendMessage(PREFIX + target.getName() + " is now §aUNFROZEN"); - target.sendMessage("§aYou have been unfrozen."); - - String notification = PREFIX + "§9" + moderator.getName() + "§7 unfroze §9" + target.getName(); - getModerationManager().broadcastToModerators(notification); + moderator.sendMessage("§b ❄ §eYou unfroze §d" + target.getName() + "§e."); + target.sendMessage("§b ❄ §a§lYOU HAVE BEEN UNFROZEN!"); + target.sendMessage(" §7You are now free to move again."); } return true; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java index 3a7e5fdc..334dc97e 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java @@ -13,7 +13,6 @@ @CommandArgument(position = 0, suggestions = {"$players"}) public final class KickCommand extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { @@ -62,7 +61,7 @@ protected boolean executeModeration(Player moderator, Command command, String la target.kickPlayer(kickMessage); - String notification = PREFIX + "§9" + moderator.getName() + "§7 kicked §9" + target.getName() + "§7 for §c" + reason; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e kicked §d" + target.getName() + "§e for §c" + reason + "§e."; getModerationManager().broadcastToModerators(notification); return true; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java index 2854fe87..dddbd7f2 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java @@ -10,7 +10,6 @@ @CommandArgument(position = 0, suggestions = {"$players"}) public final class ModTpCommands extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { @@ -41,12 +40,9 @@ private boolean handleTeleport(Player moderator, String[] arguments) { moderator.teleport(target.getLocation()); getModerationManager().getModerator(moderator).setTargetPlayer(target); - InteractiveMessage.text(PREFIX + "Teleported to §9" + target.getName() + "§7. ") - .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + target.getName() + "§7 (/userinfo)", "/userinfo " + target.getName()) + InteractiveMessage.text(" §eTeleported to §d" + target.getName() + "§e. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + target.getName() + "§f.", "/userinfo " + target.getName()) .send(moderator); - - String notification = PREFIX + "§9" + moderator.getName() + "§7 teleported to §9" + target.getName(); - getModerationManager().broadcastToModerators(notification); return true; } @@ -69,12 +65,9 @@ private boolean handleTeleportHere(Player moderator, String[] arguments) { target.teleport(moderator.getLocation()); getModerationManager().getModerator(moderator).setTargetPlayer(target); - InteractiveMessage.text(PREFIX + "Teleported §9" + target.getName() + "§7 to you. ") - .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + target.getName() + "§7 (/userinfo)", "/userinfo " + target.getName()) + InteractiveMessage.text(" §eTeleported §d" + target.getName() + "§e to you. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + target.getName() + "§f.", "/userinfo " + target.getName()) .send(moderator); - - String notification = PREFIX + "§9" + moderator.getName() + "§7 teleported §9" + target.getName() + "§7 to them"; - getModerationManager().broadcastToModerators(notification); return true; } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java index f155dfd5..7678809d 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java @@ -14,7 +14,6 @@ @CommandArgument(position = 0, suggestions = {"$players"}) public final class MuteCommand extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { @@ -66,12 +65,12 @@ private boolean handleMute(Player moderator, String[] arguments) { ipAddress ); - String durationText = expiresAt != null ? " (" + DurationParser.formatDuration(arguments[1]) + ")" : " (Permanent)"; - String notification = PREFIX + "§9" + moderator.getName() + "§e muted §d" + target.getName() - + "§e for §f" + reason + "§6" + durationText; + String durationText = expiresAt != null ? "for §6" + DurationParser.formatDuration(arguments[1]) + " " : ""; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e muted §d" + target.getName() + " §e" + durationText + "for §c" + reason + "§e."; getModerationManager().broadcastToModerators(notification); - target.sendMessage(" §f§lSANCTION §f| §7You have been muted for: §f" + reason + durationText); + String durationString = expiresAt != null ? "for §6" + DurationParser.formatDuration(arguments[1]) + " §7" : ""; + target.sendMessage(" §c§lMUTED! §7You have been muted " + durationString + "for: §f" + reason); return true; } @@ -94,9 +93,9 @@ private boolean handleUnmute(Player moderator, String[] arguments) { getPunishmentManager().removePunishment(target.getUniqueId(), PunishmentData.PunishmentType.MUTE); - String notification = PREFIX + "§9" + moderator.getName() + "§7 unmuted §9" + target.getName(); + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unmuted §d" + target.getName() + "§e."; getModerationManager().broadcastToModerators(notification); - target.sendMessage("§aYou have been unmuted."); + target.sendMessage(" §a§lUNMUTED! §7You are no longer muted."); return true; } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java index 45abbb66..92584ab9 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java @@ -4,6 +4,7 @@ import fr.moussax.bedrock.text.InteractiveMessage; import fr.moussax.bedrock.ui.book.BookMenu; import fr.moussax.blightedMod.moderator.ModerationManager; +import fr.moussax.blightedMod.moderator.reports.ReportData; import fr.moussax.blightedMod.moderator.reports.ReportManager; import org.bukkit.command.Command; import org.bukkit.entity.Player; @@ -18,8 +19,6 @@ */ public final class ReportCommand extends PlayerCommand { - private static final String PREFIX = " §c§lREPORT §f| §7"; - @Override protected boolean execute(Player player, Command command, String label, String[] arguments) { if (arguments.length == 0) { @@ -108,33 +107,30 @@ private void openGeneralReportBook(Player reporter, String targetName) { } private void submitChatReport(Player reporter, String targetName, String reason, String chatMessage) { - ReportManager.getInstance().submitReport(reporter.getName(), targetName, reason, chatMessage); - - String formattedReason = reason.replace("/", " / "); - String notificationText = chatMessage.equalsIgnoreCase("None") - ? PREFIX + "§e" + reporter.getName() + " §7reported §c" + targetName + " §7(" + formattedReason + ")" - : PREFIX + "§e" + reporter.getName() + " §7reported §c" + targetName + " §7(" + formattedReason + "): §f\"" + chatMessage + "\""; + ReportData report = ReportManager.getInstance().submitReport(reporter.getName(), targetName, reason, chatMessage); - InteractiveMessage notificationMessage = InteractiveMessage.text(notificationText + " ") - .hoverAndExecute("§a§l[TP]", "§7Click to teleport to §e" + targetName + "§7 (/mtp)", "/mtp " + targetName) + InteractiveMessage notificationMessage = InteractiveMessage.text(" §6§lALERT! §f" + reporter.getName() + " §ereported §d" + targetName + "§e's chat message: §7\"" + chatMessage + "\"§e. ") + .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport " + report.id()) + .append(" ") + .hoverAndExecute("§b[MTP]", "§fClick to teleport to §d" + targetName + "§f.", "/mtp " + targetName) .append(" ") - .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + targetName + "§7 (/userinfo)", "/userinfo " + targetName); + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + targetName + "§f.", "/userinfo " + targetName); ModerationManager.getInstance().broadcastToModerators(notificationMessage); - inform(reporter, "§aThank you! Your report against §f" + targetName + " §ahas been submitted to online moderators."); + inform(reporter, " §a§lREPORT SENT! §7Your report for §d" + targetName + " §7has been submitted to online staff."); } private void submitGeneralReport(Player reporter, String targetName, String reason) { - ReportManager.getInstance().submitReport(reporter.getName(), targetName, reason, "General player report"); + ReportData report = ReportManager.getInstance().submitReport(reporter.getName(), targetName, reason, "General player report"); - String notificationText = PREFIX + "§e" + reporter.getName() + " §7reported §c" + targetName + " §7for: §f\"" + reason + "\""; - - InteractiveMessage notificationMessage = InteractiveMessage.text(notificationText + " ") - .hoverAndExecute("§a§l[TP]", "§7Click to teleport to §e" + targetName + "§7 (/mtp)", "/mtp " + targetName) + InteractiveMessage notificationMessage = InteractiveMessage.text(" §6§lALERT! §d" + targetName + " §ewas reported. ") + .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport " + report.id()) + .append(" ") + .hoverAndExecute("§b[MTP]", "§fClick to teleport to §d" + targetName + "§f.", "/mtp " + targetName) .append(" ") - .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + targetName + "§7 (/userinfo)", "/userinfo " + targetName); + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + targetName + "§f.", "/userinfo " + targetName); ModerationManager.getInstance().broadcastToModerators(notificationMessage); - inform(reporter, "§aThank you! Your report against §f" + targetName + " §ahas been submitted to online moderators."); + inform(reporter, " §a§lREPORT SENT! §7Your report for §d" + targetName + " §7has been submitted to online staff."); } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java index 24a32d55..36754593 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java @@ -2,7 +2,7 @@ import fr.moussax.bedrock.commands.CommandArgument; import fr.moussax.blightedMod.commands.ModerationCommand; -import fr.moussax.blightedMod.moderator.menus.SanctionsPaginatedMenu; +import fr.moussax.blightedMod.moderator.menus.SanctionsMenu; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; import org.bukkit.command.Command; import org.bukkit.entity.Player; @@ -32,7 +32,7 @@ protected boolean executeModeration(Player moderator, Command command, String la return true; } - new SanctionsPaginatedMenu(targetName, punishments).open(moderator); + new SanctionsMenu(targetName, punishments).open(moderator); return true; } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java index ad7ee923..b4124ed6 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java @@ -11,16 +11,14 @@ public final class SetSlowmodeCommand extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; - @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { if (arguments.length == 0) { int currentSlowmode = getModerationManager().getSlowmodeDelaySeconds(); if (currentSlowmode > 0) { - inform(moderator, PREFIX + "Chat slowmode is currently §aENABLED §7(§e" + currentSlowmode + "s§7)."); + inform(moderator, " §fChat Slowmode §eis currently §aENABLED §7(§e" + currentSlowmode + "s§7)."); } else { - inform(moderator, PREFIX + "Chat slowmode is currently §cDISABLED§7."); + inform(moderator, " §fChat Slowmode §eis currently §cDISABLED§e."); } warn(moderator, "Usage: /slowmode "); return true; @@ -29,20 +27,20 @@ protected boolean executeModeration(Player moderator, Command command, String la String input = arguments[0]; if (input.equalsIgnoreCase("off") || input.equalsIgnoreCase("disable") || input.equalsIgnoreCase("0")) { getModerationManager().setSlowmodeDelaySeconds(0); - Bukkit.broadcastMessage(PREFIX + "Chat slowmode has been §cDISABLED§7 by §e" + moderator.getName() + "§7."); + getModerationManager().broadcastToModerators(" §d§lSTAFF! §9" + moderator.getName() + " §ehas disabled §fChat Slowmode§e."); return true; } Long secondsDuration = DurationParser.parseDurationSeconds(input); if (secondsDuration == null || secondsDuration <= 0) { getModerationManager().setSlowmodeDelaySeconds(0); - Bukkit.broadcastMessage(PREFIX + "Chat slowmode has been §cDISABLED§7 by §e" + moderator.getName() + "§7."); + getModerationManager().broadcastToModerators(" §d§lSTAFF! §9" + moderator.getName() + " §ehas disabled §fChat Slowmode§e."); return true; } int seconds = (int) Math.min(secondsDuration, Integer.MAX_VALUE); getModerationManager().setSlowmodeDelaySeconds(seconds); - Bukkit.broadcastMessage(PREFIX + "Chat slowmode set to §e" + seconds + "§7 second(s) by §e" + moderator.getName() + "§7."); + getModerationManager().broadcastToModerators(" §d§lSTAFF! §9" + moderator.getName() + " §eset §fChat Slowmode §eto §6" + seconds + "s§e."); return true; } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SocialSpyCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SocialSpyCommand.java index f90ad6f2..6ce0ecdc 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SocialSpyCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SocialSpyCommand.java @@ -14,9 +14,9 @@ protected boolean executeModeration(Player moderator, Command command, String la ModerationManager manager = getModerationManager(); boolean enabled = manager.toggleMessageInspect(moderator); if (enabled) { - inform(moderator, "§9[SPY] §7Private message inspection is now §aENABLED§7."); + inform(moderator, " §6Social Spy §etoggled §aON§e."); } else { - inform(moderator, "§9[SPY] §7Private message inspection is now §cDISABLED§7."); + inform(moderator, " §6Social Spy §etoggled §cOFF§e."); } return true; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java index dc4f08e6..af1b29b1 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java @@ -14,7 +14,6 @@ */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class TargetCommand extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { @@ -22,7 +21,7 @@ protected boolean executeModeration(Player moderator, Command command, String la if (arguments.length == 0) { blightedModerator.setTargetPlayer(null); - inform(moderator, PREFIX + "Cleared moderation HUD target."); + inform(moderator, " §eTarget cleared from §fModeration HUD§e."); return true; } @@ -32,8 +31,8 @@ protected boolean executeModeration(Player moderator, Command command, String la } blightedModerator.setTargetPlayer(target); - InteractiveMessage.text(PREFIX + "Now targeting §e" + target.getName() + " §7for moderation HUD. ") - .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + target.getName() + "§7 (/userinfo)", "/userinfo " + target.getName()) + InteractiveMessage.text(" §eTargeting §d" + target.getName() + " §ewith §fModeration HUD§e. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + target.getName() + "§f.", "/userinfo " + target.getName()) .send(moderator); return true; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java new file mode 100644 index 00000000..80bc5c34 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java @@ -0,0 +1,113 @@ +package fr.moussax.blightedMod.commands.impl; + +import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.blightedMod.commands.ModerationCommand; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; + +import static fr.moussax.bedrock.text.Messenger.inform; + +public final class TestModCommand extends ModerationCommand { + + @Override + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + String category = arguments.length > 0 ? arguments[0].toLowerCase() : "all"; + + inform(moderator, "§e=== [BlightedMod Message Formatting Test] ==="); + + if (category.equals("all") || category.equals("mode")) { + inform(moderator, "§8--- Moderation Mode & Vanish ---"); + inform(moderator, " §dModeration Mode §etoggled §aON§e."); + inform(moderator, " §dModeration Mode §etoggled §cOFF§e."); + inform(moderator, " §dVanish §etoggled §aON§e."); + inform(moderator, " §dVanish §etoggled §cOFF§e."); + } + + if (category.equals("all") || category.equals("chat")) { + inform(moderator, "§8--- Staff Chat & SocialSpy ---"); + inform(moderator, " §d§lSTAFF! §9Steve§f§l » §bHello staff team!"); + inform(moderator, " §6§lSPY! §fSteve §d➟ §fAlex §7§l» §dHey check out this secret base!"); + inform(moderator, " §6Social Spy §etoggled §aON§e."); + inform(moderator, " §6Social Spy §etoggled §cOFF§e."); + } + + if (category.equals("all") || category.equals("tp")) { + inform(moderator, "§8--- Teleportation & Targeting ---"); + InteractiveMessage.text(" §eTeleported to §dAlex§e. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") + .send(moderator); + + InteractiveMessage.text(" §eTeleported §dAlex§e to you. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") + .send(moderator); + + InteractiveMessage.text(" §eRandomly teleported to §dAlex§e. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") + .send(moderator); + + InteractiveMessage.text(" §eTargeting §dAlex §ewith §fModeration HUD§e. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") + .send(moderator); + + inform(moderator, " §eTarget cleared from §fModeration HUD§e."); + inform(moderator, " §d§lSTAFF! §9Steve§e teleported to §dAlex§e."); + } + + if (category.equals("all") || category.equals("reports")) { + inform(moderator, "§8--- Reports & Alerts ---"); + InteractiveMessage.text(" §6§lALERT! §dAlex §ewas reported. ") + .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport ") + .append(" ") + .hoverAndExecute("§b[MTP]", "§fClick to teleport to §dAlex§f.", "/mtp Alex") + .append(" ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") + .send(moderator); + + InteractiveMessage.text(" §6§lALERT! §fSteve §ereported §dAlex&e's chat message: §7\"J'vous baise vous grand mort\"§e. ") + .hoverAndExecute("§b[MTP]", "§fClick to teleport to §dAlex§f.", "/mtp Alex") + .append(" ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") + .send(moderator); + + inform(moderator, " §6§lALERT! §dAlex§e was automatically banned for §fdisconnecting §ewhile being frozen by a moderator."); + } + + if (category.equals("all") || category.equals("sanctions")) { + inform(moderator, "§8--- Punishments & Sanctions ---"); + inform(moderator, " §d§lSTAFF! §9Steve§e banned §9Alex§e for §61d§e for §cFly hacking§e."); + inform(moderator, " §d§lSTAFF! §9Steve§e unbanned §9Alex§e."); + inform(moderator, " §d§lSTAFF! §9Steve§e IP banned §9Alex§e for §cAlting§e."); + inform(moderator, " §d§lSTAFF! §9Steve§e unbanned IP for §dAlex§e."); + inform(moderator, " §d§lSTAFF! §9Steve§e muted §dAlex §efor §61h §efor §cSpamming§e."); + inform(moderator, " §d§lSTAFF! §9Steve§e unmuted §dAlex§e."); + inform(moderator, " §d§lSTAFF! §9Steve§e kicked §dAlex§e for §cGriefin§e."); + inform(moderator, "§b ❄ §eYou froze §dAlex§e."); + inform(moderator, "§b ❄ §eYou unfroze §dAlex§e."); + } + + if (category.equals("all") || category.equals("settings")) { + inform(moderator, "§8--- Chat Settings & User Info ---"); + inform(moderator, " §d§lSTAFF! §9Steve §eset §fChat Slowmode §eto §610s§e."); + inform(moderator, " §d§lSTAFF! §9Steve §ehas disabled §fChat Slowmode§e."); + inform(moderator, " §7You are now in the §dSTAFF §7channel."); + } + + if (category.equals("all") || category.equals("target") || category.equals("user") || category.equals("player")) { + inform(moderator, "§8--- Target / Player Facing Messages ---"); + inform(moderator, " §c§lMUTED! §7You have been muted for §61h §7for: §fSpamming"); + inform(moderator, " §c§lMUTED! §7You are temporarily muted for: §fSpamming"); + inform(moderator, " §a§lUNMUTED! §7You are no longer muted."); + inform(moderator, "§b ❄ §c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); + inform(moderator, " §7Do not log out or you will be permanently banned."); + inform(moderator, "§b ❄ §cYou cannot run commands while frozen."); + inform(moderator, "§b ❄ §cYou cannot attack while frozen."); + inform(moderator, "§b ❄ §a§lYOU HAVE BEEN UNFROZEN!"); + inform(moderator, " §7You are now free to move again."); + inform(moderator, " §c§lSLOWMODE! §7Please wait §e5s §7before chatting again."); + inform(moderator, " §a§lREPORT SENT! §7Your report for §dAlex §7has been submitted to online staff."); + } + + inform(moderator, "§e=== [End of Formatting Test] ==="); + return true; + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java index fd7fa2ca..63f3bf09 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java @@ -18,7 +18,6 @@ */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class UserInfoCommand extends ModerationCommand { - private static final String PREFIX = " §9§lMOD §f| §7"; @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { @@ -38,15 +37,15 @@ protected boolean executeModeration(Player moderator, Command command, String la int healthPercent = (int) Math.round((target.getHealth() / target.getMaxHealth()) * 100.0); List punishments = getPunishmentManager().getAllPunishments(target.getName()); - long mutesCount = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.MUTE).count(); - long bansCount = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.BAN || p.type() == PunishmentData.PunishmentType.IP_BAN).count(); - long kicksCount = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.KICK).count(); + long mutesCount = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.MUTE).count(); + long bansCount = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN).count(); + long kicksCount = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.KICK).count(); boolean isFrozen = getModerationManager().isFrozen(target); boolean isMuted = getPunishmentManager().isMuted(target.getUniqueId()); boolean isVanished = getModerationManager().getModerator(target).isVanished(); - inform(moderator, PREFIX + "User info for §e" + target.getName() + "§7:"); + inform(moderator, " §eUser info for §d" + target.getName() + "§7:"); moderator.sendMessage(" §f- §7UUID: §f" + target.getUniqueId()); moderator.sendMessage(" §f- §7IP Address: §f" + ipAddress); moderator.sendMessage(" §f- §7State: §a" + healthPercent + "% HP §f| §e" + target.getFoodLevel() + " Food §f| §b" + target.getGameMode().name() + " §f| §dLvl " + target.getLevel()); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PlayerDataHandler.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PlayerDataHandler.java new file mode 100644 index 00000000..23e53cd2 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PlayerDataHandler.java @@ -0,0 +1,4 @@ +package fr.moussax.blightedMod.database; + +public class DataHandler { +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java index 019c8793..df62312e 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java @@ -13,7 +13,6 @@ import java.util.Collection; public final class BlightedModerator { - private static final String PREFIX = " §9§lMOD §f| §7"; private final Player player; private boolean isInModerationMode; @@ -27,7 +26,6 @@ public final class BlightedModerator { private float savedExperience; private int savedLevel; private Collection savedPotionEffects; - private org.bukkit.Location savedLocation; public BlightedModerator(Player player) { this.player = player; @@ -63,7 +61,7 @@ public void onEnable() { giveModerationTools(); this.isInModerationMode = true; - player.sendMessage(PREFIX + "You are now in §9moderation §7mode."); + player.sendMessage(" §dModeration Mode §etoggled §aON§e."); } public void onDisable() { @@ -73,7 +71,7 @@ public void onDisable() { this.isInModerationMode = false; this.targetPlayer = null; - player.sendMessage(PREFIX + "You are no longer in §9moderation §7mode."); + player.sendMessage(" §dModeration Mode §etoggled §cOFF§e."); } private void saveState() { @@ -85,7 +83,6 @@ private void saveState() { this.savedExperience = player.getExp(); this.savedLevel = player.getLevel(); this.savedPotionEffects = player.getActivePotionEffects(); - this.savedLocation = player.getLocation(); } private void restoreState() { @@ -101,10 +98,6 @@ private void restoreState() { player.getActivePotionEffects().forEach(effect -> player.removePotionEffect(effect.getType())); player.addPotionEffects(savedPotionEffects); - if (savedLocation != null) { - player.teleport(savedLocation); - } - player.setInvulnerable(false); player.setAllowFlight(false); player.setFlying(false); @@ -144,10 +137,10 @@ public void setVanished(boolean vanished, boolean notifyPlayer) { if (vanished) { hideFromNonModerators(instance); - if (!notifyPlayer) player.sendMessage(PREFIX + "You are now §avanished§7."); + if (!notifyPlayer) player.sendMessage(" §dVanish §etoggled §aON§e."); } else { showToAllPlayers(instance); - if (!notifyPlayer) player.sendMessage(PREFIX + "You are now §cvisible§7."); + if (!notifyPlayer) player.sendMessage(" §dVanish §etoggled §cOFF§e."); } updateVanishTool(); } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java index d882bef4..acad88c4 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java @@ -185,7 +185,7 @@ public UUID getLastMessageTarget(UUID playerId) { } public void broadcastSpyMessage(String senderName, String recipientName, String content) { - String formatted = " §9[SPY] §7" + senderName + " §8» §7" + recipientName + "§f: " + content; + String formatted = " §6§lSPY! §f" + senderName + " §d➟ §f" + recipientName + " §7§l» §d" + content; Bukkit.getOnlinePlayers().stream() .filter(this::isModerator) .filter(this::isMessageInspectEnabled) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java index 02201cf6..1c342828 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java @@ -15,9 +15,7 @@ private ModerationHud() { } public static ActionbarSection createSection(ModerationManager moderationManager) { - return ActionbarSection.of( - SECTION_ID, - PRIORITY, + return ActionbarSection.of(SECTION_ID, PRIORITY, player -> render(moderationManager, player), moderationManager::isModerator ); @@ -40,8 +38,17 @@ private static String render(ModerationManager moderationManager, Player player) ? String.format(Locale.ROOT, "%.2f blocks", player.getLocation().distance(target.getLocation())) : "N/A"; - String frozenText = moderationManager.isFrozen(target) ? "§b§lYUp!" : "§c§lNOPE!"; - - return "§fTarget: §d" + target.getName() + " §fHP: §d" + healthPercent + "% §fDistance: §d" + distanceText + " §fFrozen: " + frozenText; + String frozenText = moderationManager.isFrozen(target) ? "§b§lYUP!" : "§c§lNOPE!"; + String pingText = target.getPing() < 100 ? "§a" + target.getPing() + "ms" + : target.getPing() < 200 ? "§e" + target.getPing() + "ms" + : target.getPing() < 300 ? "§6" + target.getPing() + "ms" + : target.getPing() < 500 ? "§c" + target.getPing() + "ms" + : "§4" + target.getPing() + "ms"; + + return "§fTarget: §d" + target.getName() + + " §fHP: §d" + healthPercent + "%" + + " §fDistance: §d" + distanceText + + " §fPing: " + pingText + + " §fFrozen: " + frozenText; } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsPaginatedMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java similarity index 93% rename from blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsPaginatedMenu.java rename to blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java index 7e5a5b09..d9f320d3 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsPaginatedMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java @@ -59,7 +59,7 @@ public SanctionFilter next() { private SanctionFilter currentFilter = SanctionFilter.ALL; public SanctionsPaginatedMenu(String targetName, List punishments) { - super("§9Sanctions for " + targetName, 54); + super("Sanctions for " + targetName, 54); this.targetName = targetName; this.punishments = punishments; } @@ -184,9 +184,9 @@ private void renderTargetHead() { skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(targetName)); skullMeta.setDisplayName("§eTarget: §b" + targetName); - long activeBans = punishments.stream().filter(p -> (p.type() == PunishmentData.PunishmentType.BAN || p.type() == PunishmentData.PunishmentType.IP_BAN) && p.active() && !p.isExpired()).count(); - long activeMutes = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.MUTE && p.active() && !p.isExpired()).count(); - long totalKicks = punishments.stream().filter(p -> p.type() == PunishmentData.PunishmentType.KICK).count(); + long activeBans = punishments.stream().filter(punishment -> (punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN) && punishment.active() && !punishment.isExpired()).count(); + long activeMutes = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.MUTE && punishment.active() && !punishment.isExpired()).count(); + long totalKicks = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.KICK).count(); skullMeta.setLore(List.of( "§7Total Sanctions: §e" + punishments.size(), From c61a51238dde24745560de3f2528e7aac030553c Mon Sep 17 00:00:00 2001 From: Moussa Date: Fri, 4 Sep 2026 23:23:08 +0200 Subject: [PATCH 04/30] feat(mod): add book detail view for reports and testmod preview command - add /checkreport command to display detailed report contents in a book UI - add /testmod command to verify chat message formatting live - rename SanctionsPaginatedMenu to SanctionsMenu for naming consistency --- .../commands/impl/TestModCommand.java | 2 -- .../database/PlayerDataHandler.java | 4 --- .../moderator/menus/ReportsCenterMenu.java | 2 +- .../moderator/menus/SanctionsMenu.java | 32 ++++++++----------- .../moderator/reports/ReportManager.java | 9 ++++++ .../registry/CommandsRegistry.java | 5 +++ blighted-mod/src/main/resources/plugin.yml | 6 ++++ 7 files changed, 34 insertions(+), 26 deletions(-) delete mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/database/PlayerDataHandler.java diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java index 80bc5c34..83c104d4 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java @@ -99,8 +99,6 @@ protected boolean executeModeration(Player moderator, Command command, String la inform(moderator, " §a§lUNMUTED! §7You are no longer muted."); inform(moderator, "§b ❄ §c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); inform(moderator, " §7Do not log out or you will be permanently banned."); - inform(moderator, "§b ❄ §cYou cannot run commands while frozen."); - inform(moderator, "§b ❄ §cYou cannot attack while frozen."); inform(moderator, "§b ❄ §a§lYOU HAVE BEEN UNFROZEN!"); inform(moderator, " §7You are now free to move again."); inform(moderator, " §c§lSLOWMODE! §7Please wait §e5s §7before chatting again."); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PlayerDataHandler.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PlayerDataHandler.java deleted file mode 100644 index 23e53cd2..00000000 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PlayerDataHandler.java +++ /dev/null @@ -1,4 +0,0 @@ -package fr.moussax.blightedMod.database; - -public class DataHandler { -} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java index ed1816f3..5c1eaf8a 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java @@ -34,7 +34,7 @@ public final class ReportsCenterMenu extends PaginatedMenu implements TickableMe }; public ReportsCenterMenu() { - super("§9Active Reports", 54); + super("Active Reports", 54); } @Override diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java index d9f320d3..d2968d2c 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java @@ -21,7 +21,7 @@ * Paginated menu displaying historical sanctions for a specific player. * Features target player head, Hypixel-style hopper filtering, and real-time updates. */ -public final class SanctionsPaginatedMenu extends PaginatedMenu implements TickableMenu { +public final class SanctionsMenu extends PaginatedMenu implements TickableMenu { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); private static final int[] SANCTION_SLOTS = { @@ -58,17 +58,12 @@ public SanctionFilter next() { private final List punishments; private SanctionFilter currentFilter = SanctionFilter.ALL; - public SanctionsPaginatedMenu(String targetName, List punishments) { - super("Sanctions for " + targetName, 54); + public SanctionsMenu(String targetName, List punishments) { + super(targetName +"'s sanctions", 54); this.targetName = targetName; this.punishments = punishments; } - @Override - public long tickPeriodTicks() { - return 20L; - } - @Override public void onTick(Player player) { refresh(player); @@ -126,7 +121,7 @@ protected ItemStack getItem(@NonNull Player player, int index) { @Override public void build(@NonNull Player viewer) { List filtered = getFilteredPunishments(); - totalItems = Math.max(0, filtered.size()); + totalItems = filtered.size(); int itemsPerPage = getItemsPerPage(); int maxPage = Math.max(0, (totalItems - 1) / itemsPerPage); @@ -182,7 +177,7 @@ private void renderTargetHead() { SkullMeta skullMeta = (SkullMeta) headStack.getItemMeta(); if (skullMeta != null) { skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(targetName)); - skullMeta.setDisplayName("§eTarget: §b" + targetName); + skullMeta.setDisplayName("§d" + targetName); long activeBans = punishments.stream().filter(punishment -> (punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN) && punishment.active() && !punishment.isExpired()).count(); long activeMutes = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.MUTE && punishment.active() && !punishment.isExpired()).count(); @@ -201,17 +196,16 @@ private void renderTargetHead() { private void renderFilterHopper() { ItemBuilder hopperBuilder = new ItemBuilder(Material.HOPPER) - .setDisplayName("§eFilter: §b" + currentFilter.getDisplayName()) - .addLore("§7Current: §a" + currentFilter.getDisplayName()) + .setDisplayName("§fFilter") .addLore("") - .addLore("§7Filters:") - .addLore(currentFilter == SanctionFilter.ALL ? " §b» All Sanctions" : " §7 All Sanctions") - .addLore(currentFilter == SanctionFilter.ACTIVE ? " §b» Active Only" : " §7 Active Only") - .addLore(currentFilter == SanctionFilter.BAN ? " §b» Bans Only" : " §7 Bans Only") - .addLore(currentFilter == SanctionFilter.MUTE ? " §b» Mutes Only" : " §7 Mutes Only") - .addLore(currentFilter == SanctionFilter.KICK ? " §b» Kicks Only" : " §7 Kicks Only") + .addLore(currentFilter == SanctionFilter.ALL ? " §b▶ All Sanctions" : "§7 All Sanctions") + .addLore(currentFilter == SanctionFilter.ACTIVE ? " §b▶ Active Only" : "§7 Active Only") + .addLore(currentFilter == SanctionFilter.BAN ? " §b▶ Bans Only" : "§7 Bans Only") + .addLore(currentFilter == SanctionFilter.MUTE ? " §b▶ Mutes Only" : "§7 Mutes Only") + .addLore(currentFilter == SanctionFilter.KICK ? " §b▶ Kicks Only" : "§7 Kicks Only") .addLore("") - .addLore("§eClick to cycle filter!"); + .addLore("§bRight-click to go backwards!") + .addLore("§eClick to switch filter!"); setItem(52, hopperBuilder.toItemStack(), MenuItemInteraction.ANY_CLICK, (player, _) -> { currentFilter = currentFilter.next(); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java index 2a6dd545..0495e106 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java @@ -40,4 +40,13 @@ public List getActiveReports() { public boolean dismissReport(int reportId) { return reports.removeIf(report -> report.id() == reportId); } + + public ReportData getReportById(int reportId) { + synchronized (reports) { + return reports.stream() + .filter(report -> report.id() == reportId) + .findFirst() + .orElse(null); + } + } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java index be3f9334..ec77cf5b 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java @@ -3,6 +3,7 @@ import fr.moussax.bedrock.commands.CommandRegistrar; import fr.moussax.bedrock.commands.TabSuggestionRegistry; import fr.moussax.blightedMod.commands.impl.BanCommand; +import fr.moussax.blightedMod.commands.impl.CheckReportCommand; import fr.moussax.blightedMod.commands.impl.EnderseeCommand; import fr.moussax.blightedMod.commands.impl.FreezeCommand; import fr.moussax.blightedMod.commands.impl.InvseeCommand; @@ -16,6 +17,7 @@ import fr.moussax.blightedMod.commands.impl.SetSlowmodeCommand; import fr.moussax.blightedMod.commands.impl.SocialSpyCommand; import fr.moussax.blightedMod.commands.impl.TargetCommand; +import fr.moussax.blightedMod.commands.impl.TestModCommand; import fr.moussax.blightedMod.commands.impl.UserInfoCommand; import fr.moussax.blightedMod.commands.impl.VanishCommand; import org.bukkit.Bukkit; @@ -59,6 +61,7 @@ public static void registerCommands(JavaPlugin plugin) { registrar.register("setslowmode", slowmodeCommand); registrar.register("reports", new ReportsCommand()); + registrar.register("checkreport", new CheckReportCommand()); registrar.register("target", new TargetCommand()); registrar.register("sanctions", new SanctionsCommand()); registrar.register("userinfo", new UserInfoCommand()); @@ -68,6 +71,8 @@ public static void registerCommands(JavaPlugin plugin) { registrar.register("msginspect", socialSpyCommand); registrar.register("spy", socialSpyCommand); registrar.register("socialspy", socialSpyCommand); + + registrar.register("testmod", new TestModCommand()); } diff --git a/blighted-mod/src/main/resources/plugin.yml b/blighted-mod/src/main/resources/plugin.yml index a18a7398..a6b18984 100644 --- a/blighted-mod/src/main/resources/plugin.yml +++ b/blighted-mod/src/main/resources/plugin.yml @@ -59,6 +59,9 @@ commands: reports: description: View active player reports. permission: blightedmod.moderator + checkreport: + description: Inspect detailed information for a specific report. + permission: blightedmod.moderator target: description: Manually set or clear targeted player for moderation HUD. permission: blightedmod.moderator @@ -74,4 +77,7 @@ commands: msginspect: description: Toggle private message inspection (SocialSpy). aliases: [spy, socialspy] + permission: blightedmod.moderator + testmod: + description: Test and review all moderator chat message formatting. permission: blightedmod.moderator \ No newline at end of file From 7cc6f0eba8609a4b8e293df33abc0596228f6953 Mon Sep 17 00:00:00 2001 From: Moussa Date: Fri, 4 Sep 2026 23:23:12 +0200 Subject: [PATCH 05/30] fix(mod): resolve double-click tool interact and update report viewer item - prevent duplicate execution of tool actions on target player right-click - replace report viewer knowledge book tool with a standard book --- .../moderator/ModerationTools.java | 4 +- .../listeners/ModerationListener.java | 123 ++++++++++-------- .../blightedMod/registry/EventsRegistry.java | 10 +- 3 files changed, 79 insertions(+), 58 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java index 4285ecd4..b4a5a598 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java @@ -3,6 +3,7 @@ import fr.moussax.bedrock.utils.ItemBuilder; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; public final class ModerationTools { @@ -15,7 +16,7 @@ public static ItemStack getRandomTeleporter() { } public static ItemStack getReportViewer() { - return new ItemBuilder(Material.KNOWLEDGE_BOOK) + return new ItemBuilder(Material.BOOK) .setDisplayName("§dReport Center §7(Right Click)") .addLore("§7View and manage player reports.") .toItemStack(); @@ -34,6 +35,7 @@ public static ItemStack getKnockbackStick() { .setDisplayName("§dAnti Knockback") .addLore("§7Test if target has Anti-KB Cheat.") .addEnchantment(Enchantment.KNOCKBACK, 4) + .addItemFlag(ItemFlag.HIDE_ENCHANTS) .toItemStack(); } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java index 53c59011..e1287542 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java @@ -22,6 +22,7 @@ import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.*; import org.bukkit.event.server.ServerListPingEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import java.util.ArrayList; @@ -37,7 +38,6 @@ import static fr.moussax.bedrock.text.Messenger.warn; public final class ModerationListener implements Listener { - private static final String PREFIX = " §9§lMOD §f| §7"; private final ModerationManager moderationManager; private final PunishmentManager punishmentManager; @@ -48,6 +48,7 @@ public ModerationListener(ModerationManager moderationManager) { this.punishmentManager = moderationManager.getPunishmentManager(); } + // ponytail: kept — Spigot target requires AsyncPlayerChatEvent; non-deprecated replacement (AsyncChatEvent) is Paper-only @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerChat(AsyncPlayerChatEvent event) { Player player = event.getPlayer(); @@ -57,14 +58,14 @@ public void onPlayerChat(AsyncPlayerChatEvent event) { PunishmentData mute = punishmentManager.getActivePunishment(player.getUniqueId(), PunishmentData.PunishmentType.MUTE); if (mute != null) { String durationText = mute.isPermanent() ? "permanently" : "temporarily"; - player.sendMessage("§cYou are " + durationText + " muted for: §f" + mute.reason()); + player.sendMessage(" §c§lMUTED! §7You are " + durationText + " muted for: §f" + mute.reason()); } return; } if (moderationManager.isModerator(player) && moderationManager.getChatChannel(player) == ModerationManager.ChatChannel.STAFF) { event.setCancelled(true); - String formatted = " §9§lMOD §f| §9" + player.getName() + "§f§l » §b" + event.getMessage(); + String formatted = " §d§lSTAFF! §9" + player.getName() + "§f§l » §b" + event.getMessage(); moderationManager.broadcastToModerators(formatted); return; } @@ -72,15 +73,15 @@ public void onPlayerChat(AsyncPlayerChatEvent event) { int remainingSlowmodeSeconds = moderationManager.getRemainingSlowmodeCooldown(player); if (remainingSlowmodeSeconds > 0) { event.setCancelled(true); - warn(player, "Chat is in slowmode. Please wait §e" + remainingSlowmodeSeconds + "§7 second(s) before typing again."); + player.sendMessage(" §c§lSLOWMODE! §7Please wait §e" + remainingSlowmodeSeconds + "s §7before chatting again."); return; } String message = event.getMessage(); if (message.startsWith("!!") && moderationManager.isModerator(player)) { event.setCancelled(true); - String modMessage = message.substring(2).trim(); - String formatted = " §9§lMOD §f| §9" + player.getName() + "§f§l » §b" + modMessage; + String moderatorMessage = message.substring(2).trim(); + String formatted = " §d§lSTAFF! §9" + player.getName() + "§f§l » §b" + moderatorMessage; moderationManager.broadcastToModerators(formatted); return; } @@ -122,6 +123,7 @@ private String buildBanMessage(PunishmentData ban, String title) { @EventHandler public void onToolInteract(PlayerInteractEvent event) { + if (event.getHand() != EquipmentSlot.HAND) return; Player player = event.getPlayer(); if (!moderationManager.isInModerationMode(player)) return; @@ -138,6 +140,7 @@ public void onToolInteract(PlayerInteractEvent event) { @EventHandler public void onEntityInteract(PlayerInteractEntityEvent event) { + if (event.getHand() != EquipmentSlot.HAND) return; Player player = event.getPlayer(); if (!moderationManager.isInModerationMode(player)) return; @@ -145,8 +148,16 @@ public void onEntityInteract(PlayerInteractEntityEvent event) { if (!(event.getRightClicked() instanceof Player target)) return; + BlightedModerator moderator = moderationManager.getModerator(player); + boolean alreadyTargeted = Objects.equals(moderator.getTargetPlayer(), target); + moderator.setTargetPlayer(target); + Material tool = player.getInventory().getItemInMainHand().getType(); - handleEntityInteraction(player, target, tool); + if (tool != Material.CHEST && tool != Material.PACKED_ICE && tool != Material.ENCHANTED_BOOK && !alreadyTargeted) { + InteractiveMessage.text(" §eTargeting §d" + target.getName() + " §ewith §fModeration HUD§e. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + target.getName() + "§f.", "/userinfo " + target.getName()) + .send(player); + } } @EventHandler @@ -191,7 +202,7 @@ public void onPlayerQuit(PlayerQuitEvent event) { ipAddress ); - String notification = PREFIX + "§c" + quittingPlayer.getName() + "§7 disconnected while frozen and was automatically banned."; + String notification = " §6§lALERT! §d" + quittingPlayer.getName() + "§e was automatically banned for §fdisconnecting §ewhile being frozen by a moderator."; moderationManager.broadcastToModerators(notification); } @@ -223,15 +234,6 @@ public void onFrozenPlayerMove(PlayerMoveEvent event) { if (movedHorizontally) event.setTo(event.getFrom()); } - @EventHandler - public void onFrozenPlayerCommand(PlayerCommandPreprocessEvent event) { - Player player = event.getPlayer(); - if (moderationManager.isModerator(player) || !moderationManager.isFrozen(player)) return; - - event.setCancelled(true); - player.sendMessage("§cYou cannot execute commands while frozen by a moderator."); - } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPrivateMessageSpy(PlayerCommandPreprocessEvent event) { String rawCommandLine = event.getMessage(); @@ -281,29 +283,17 @@ public void onPrivateMessageSpy(PlayerCommandPreprocessEvent event) { } } - @EventHandler - public void onFrozenPlayerAttack(EntityDamageByEntityEvent event) { - if (!(event.getDamager() instanceof Player attacker)) return; - if (moderationManager.isModerator(attacker) || !moderationManager.isFrozen(attacker)) return; - - event.setCancelled(true); - attacker.sendMessage("§cYou cannot attack while frozen."); - } - @EventHandler public void onModeratorDropItem(PlayerDropItemEvent event) { - Player player = event.getPlayer(); - if (moderationManager.isInModerationMode(player) || (!moderationManager.isModerator(player) && moderationManager.isFrozen(player))) { + if (moderationManager.isInModerationMode(event.getPlayer())) { event.setCancelled(true); } } @EventHandler public void onModeratorPickupItem(EntityPickupItemEvent event) { - if (event.getEntity() instanceof Player player) { - if (moderationManager.isInModerationMode(player) || (!moderationManager.isModerator(player) && moderationManager.isFrozen(player))) { - event.setCancelled(true); - } + if (event.getEntity() instanceof Player player && moderationManager.isInModerationMode(player)) { + event.setCancelled(true); } } @@ -337,7 +327,10 @@ public void onModeratorEntityDamage(EntityDamageByEntityEvent event) { Material tool = attacker.getInventory().getItemInMainHand().getType(); if (tool == Material.STICK) { - event.setDamage(0.0); + // ponytail: kept — Allow Anti-KB stick hit and damage processing so Minecraft hurt() pipeline applies knockback + if (event.getEntity() instanceof Player victim) { + moderationManager.getModerator(attacker).setTargetPlayer(victim); + } } else { event.setCancelled(true); } @@ -345,33 +338,51 @@ public void onModeratorEntityDamage(EntityDamageByEntityEvent event) { @EventHandler public void onModeratorInventoryClick(InventoryClickEvent event) { - if (event.getWhoClicked() instanceof Player player) { - if (moderationManager.isInModerationMode(player) || (!moderationManager.isModerator(player) && moderationManager.isFrozen(player))) { - event.setCancelled(true); - } + if (event.getWhoClicked() instanceof Player player && moderationManager.isInModerationMode(player)) { + event.setCancelled(true); } } private void handleToolInteraction(Player player, Material tool) { switch (tool) { case ENDER_EYE -> teleportToRandomPlayer(player); - case BOOK -> openReportsMenu(player); + case BOOK, NETHER_STAR -> openReportsMenu(player); case PURPLE_DYE, GRAY_DYE -> toggleVanish(player); + case PACKED_ICE -> { + Player target = getActiveTarget(player); + if (target != null) { + toggleFreeze(player, target); + } + } + case CHEST -> { + Player target = getActiveTarget(player); + if (target != null) { + openInventory(player, target); + } + } + case ENCHANTED_BOOK -> { + Player target = getActiveTarget(player); + if (target != null) { + openSanctionsMenu(player, target); + } + } default -> { } } } + private Player getActiveTarget(Player moderator) { + Player target = moderationManager.getModerator(moderator).getTargetPlayer(); + if (target == null || !target.isOnline()) { + warn(moderator, "No target player selected. Right-click a player or use §e/target §c."); + return null; + } + return target; + } + private void openReportsMenu(Player moderator) { moderator.performCommand("reports"); } - private void handleEntityInteraction(Player player, Player target, Material tool) { - switch (tool) { - case CHEST -> openInventory(player, target); - case PACKED_ICE -> toggleFreeze(player, target); - default -> { } - } - } private void toggleVanish(Player player) { BlightedModerator moderator = moderationManager.getModerator(player); @@ -379,20 +390,28 @@ private void toggleVanish(Player player) { } private void openInventory(Player moderator, Player target) { + moderationManager.getModerator(moderator).setTargetPlayer(target); new InvSeeMenu(target).open(moderator); } + private void openSanctionsMenu(Player moderator, Player target) { + moderationManager.getModerator(moderator).setTargetPlayer(target); + moderator.performCommand("sanctions " + target.getName()); + } + private void toggleFreeze(Player moderator, Player target) { + moderationManager.getModerator(moderator).setTargetPlayer(target); boolean isFrozen = moderationManager.toggleFreeze(target); - String status = isFrozen ? "§bFROZEN" : "§aUNFROZEN"; + String actionText = isFrozen ? "froze" : "unfroze"; - moderator.sendMessage(PREFIX + target.getName() + " is now " + status); + moderator.sendMessage("§b ❄ §eYou " + actionText + " §d" + target.getName() + "§e."); if (isFrozen) { - target.sendMessage("§c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); - target.sendMessage("§cDo not log out or you will be banned."); + target.sendMessage("§b ❄ §c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); + target.sendMessage(" §7Do not log out or you will be permanently banned."); } else { - target.sendMessage("§aYou have been unfrozen."); + target.sendMessage("§b ❄ §a§lYOU HAVE BEEN UNFROZEN!"); + target.sendMessage(" §7You are now free to move again."); } } @@ -409,8 +428,8 @@ private void teleportToRandomPlayer(Player moderator) { moderator.teleport(target.getLocation()); moderationManager.getModerator(moderator).setTargetPlayer(target); - InteractiveMessage.text(PREFIX + "Teleported to §d" + target.getName() + " ") - .hoverAndExecute("§e§l[INFO]", "§7Click to view user information for §e" + target.getName() + "§7 (/userinfo)", "/userinfo " + target.getName()) + InteractiveMessage.text(" §eRandomly teleported to §d" + target.getName() + "§e. ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + target.getName() + "§f.", "/userinfo " + target.getName()) .send(moderator); } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java index 3f7242ad..5827afea 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java @@ -20,7 +20,7 @@ public final class EventsRegistry { private ActionbarService actionBarService; public void initializeListeners() { - PluginManager manager = Bukkit.getPluginManager(); + PluginManager pluginManager = Bukkit.getPluginManager(); signInputListener = new SignInputListener(); menuSystem = new MenuSystem(instance); @@ -29,10 +29,10 @@ public void initializeListeners() { actionBarService.registerSection(ModerationHud.createSection(instance.getModerationManager())); - manager.registerEvents(signInputListener, instance); - manager.registerEvents(new MenuListener(menuSystem), instance); - manager.registerEvents(new ModerationListener(instance.getModerationManager()), instance); - manager.registerEvents(new InteractiveChatListener(), instance); + pluginManager.registerEvents(signInputListener, instance); + pluginManager.registerEvents(new MenuListener(menuSystem), instance); + pluginManager.registerEvents(new ModerationListener(instance.getModerationManager()), instance); + pluginManager.registerEvents(new InteractiveChatListener(), instance); } From b9b0cf2c3e665eed327a9946d5e1c7d72775f853 Mon Sep 17 00:00:00 2001 From: Moussa Date: Fri, 4 Sep 2026 23:45:29 +0200 Subject: [PATCH 06/30] refactor(mod): replace manual getters with Lombok annotations and improve target validation --- .../commands/impl/TargetCommand.java | 11 ++++++++ .../moderator/BlightedModerator.java | 24 ++++++----------- .../listeners/ModerationListener.java | 27 +++++++------------ 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java index af1b29b1..858069c3 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java @@ -8,6 +8,7 @@ import org.bukkit.entity.Player; import static fr.moussax.bedrock.text.Messenger.inform; +import static fr.moussax.bedrock.text.Messenger.warn; /** * Moderation command to set or clear the targeted player for the moderation actionbar HUD (/target ). @@ -17,6 +18,11 @@ public final class TargetCommand extends ModerationCommand { @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (!getModerationManager().isInModerationMode(moderator)) { + warn(moderator, "You must be in §dModeration Mode §cto target a player."); + return true; + } + BlightedModerator blightedModerator = getModerationManager().getModerator(moderator); if (arguments.length == 0) { @@ -30,6 +36,11 @@ protected boolean executeModeration(Player moderator, Command command, String la return false; } + if (target.equals(moderator)) { + warn(moderator, "You cannot target yourself."); + return true; + } + blightedModerator.setTargetPlayer(target); InteractiveMessage.text(" §eTargeting §d" + target.getName() + " §ewith §fModeration HUD§e. ") .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + target.getName() + "§f.", "/userinfo " + target.getName()) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java index df62312e..a58c9778 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java @@ -2,6 +2,7 @@ import fr.moussax.blightedMod.BlightedMod; import fr.moussax.blightedMod.utils.PluginPermissions; +import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.entity.Player; @@ -14,9 +15,13 @@ public final class BlightedModerator { + @Getter private final Player player; + @Getter private boolean isInModerationMode; + @Getter private boolean isVanished; + @Getter private Player targetPlayer; private ItemStack[] savedInventory; @@ -33,23 +38,10 @@ public BlightedModerator(Player player) { this.isVanished = false; } - public Player getPlayer() { - return player; - } - - public boolean isInModerationMode() { - return isInModerationMode; - } - - public boolean isVanished() { - return isVanished; - } - - public Player getTargetPlayer() { - return targetPlayer; - } - public void setTargetPlayer(Player targetPlayer) { + if (targetPlayer != null && targetPlayer.equals(player)) { + return; + } this.targetPlayer = targetPlayer; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java index e1287542..66eb90b0 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java @@ -25,15 +25,7 @@ import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.Random; -import java.util.Set; -import java.util.UUID; +import java.util.*; import static fr.moussax.bedrock.text.Messenger.warn; @@ -48,7 +40,6 @@ public ModerationListener(ModerationManager moderationManager) { this.punishmentManager = moderationManager.getPunishmentManager(); } - // ponytail: kept — Spigot target requires AsyncPlayerChatEvent; non-deprecated replacement (AsyncChatEvent) is Paper-only @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerChat(AsyncPlayerChatEvent event) { Player player = event.getPlayer(); @@ -114,10 +105,10 @@ private String buildBanMessage(PunishmentData ban, String title) { String durationText = ban.isPermanent() ? "Permanent" : "Temporary"; return """ §c§l%s - + §7Reason: §f%s §7Duration: §f%s - + §7Appeal on our Discord if you believe this was a mistake.""".formatted(title, ban.reason(), durationText); } @@ -146,7 +137,7 @@ public void onEntityInteract(PlayerInteractEntityEvent event) { event.setCancelled(true); - if (!(event.getRightClicked() instanceof Player target)) return; + if (!(event.getRightClicked() instanceof Player target) || target.equals(player)) return; BlightedModerator moderator = moderationManager.getModerator(player); boolean alreadyTargeted = Objects.equals(moderator.getTargetPlayer(), target); @@ -237,7 +228,7 @@ public void onFrozenPlayerMove(PlayerMoveEvent event) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPrivateMessageSpy(PlayerCommandPreprocessEvent event) { String rawCommandLine = event.getMessage(); - if (rawCommandLine == null || !rawCommandLine.startsWith("/")) { + if (!rawCommandLine.startsWith("/")) { return; } @@ -307,7 +298,9 @@ public void onModeratorPlaceBlock(BlockPlaceEvent event) { if (moderationManager.isInModerationMode(event.getPlayer())) event.setCancelled(true); } - /** Non-entity damage (fall, fire, drown, etc.) against a moderator in moderation mode. */ + /** + * Non-entity damage (fall, fire, drown, etc.) against a moderator in moderation mode. + */ @EventHandler public void onModeratorTakeDamage(EntityDamageEvent event) { if (event.getEntity() instanceof Player player && moderationManager.isInModerationMode(player)) { @@ -327,7 +320,6 @@ public void onModeratorEntityDamage(EntityDamageByEntityEvent event) { Material tool = attacker.getInventory().getItemInMainHand().getType(); if (tool == Material.STICK) { - // ponytail: kept — Allow Anti-KB stick hit and damage processing so Minecraft hurt() pipeline applies knockback if (event.getEntity() instanceof Player victim) { moderationManager.getModerator(attacker).setTargetPlayer(victim); } @@ -366,7 +358,8 @@ private void handleToolInteraction(Player player, Material tool) { openSanctionsMenu(player, target); } } - default -> { } + default -> { + } } } From 3b0295c2033c7d151371dce7bc0817bb0501fa1c Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 08:58:03 +0200 Subject: [PATCH 07/30] refactor(menu): standardize paginated inner grid slots and prevent refresh flickering - centralize 28-slot chest inner grid slots in PaginatedMenu - add getDisplaySlots and getEmptyStateItem hooks to PaginatedMenu - refactor SanctionsMenu, ReportsCenterMenu, and ItemRegistryMenu to use base grid slots - guard title updates in Menu#refresh to avoid re-opening container view on tick - add item stack diffing to skip redundant slot updates --- .../ui/actionbar/ActionbarComposer.java | 37 ++- .../ui/actionbar/ActionbarSection.java | 20 +- .../ui/actionbar/ActionbarService.java | 15 +- .../java/fr/moussax/bedrock/ui/menu/Menu.java | 27 +- .../bedrock/ui/menu/system/MenuSystem.java | 15 +- .../bedrock/ui/menu/types/PaginatedMenu.java | 82 +++--- .../fr/moussax/blightedMod/BlightedMod.java | 16 +- .../blightedMod/commands/impl/BanCommand.java | 50 +--- .../commands/impl/MuteCommand.java | 17 +- .../commands/impl/UserinfoCommand.java | 9 +- .../moderator/ModerationManager.java | 10 +- .../moderator/hud/ModerationHud.java | 17 +- .../listeners/InteractiveChatListener.java | 7 +- .../listeners/ModerationListener.java | 4 +- .../moderator/menus/ReportsCenterMenu.java | 81 +----- .../moderator/menus/SanctionsMenu.java | 248 ++++++++++-------- .../moderator/punishments/DurationParser.java | 1 - .../punishments/PunishmentManager.java | 13 +- .../fr/moussax/blightedSMP/BlightedSMP.java | 6 + .../items/registry/menu/ItemRegistryMenu.java | 57 +--- .../engine/player/BlightedPlayer.java | 11 +- .../blightedSMP/registry/EventsRegistry.java | 1 - 22 files changed, 358 insertions(+), 386 deletions(-) diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java index 10b482de..f382cfcf 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java @@ -106,21 +106,36 @@ public String compile(@NonNull Player player) { } } - return sections.values().stream() + var visibleSections = sections.values().stream() .filter(section -> section.visibility().test(player)) .sorted(Comparator.comparingInt(ActionbarSection::priority)) - .map(section -> { - TimedAlert alert = slotAlerts.get(section.id()); - if (alert != null) { - if (!alert.isExpired()) { - return alert.message(); - } - slotAlerts.remove(section.id()); - } - return section.textSupplier().apply(player); - }) + .toList(); + + // ponytail: kept — exclusive section overrides default actionbar when active + for (ActionbarSection section : visibleSections) { + if (section.exclusive()) { + String text = evaluateSection(section, player); + if (text != null && !text.isEmpty()) { + return text; + } + } + } + + return visibleSections.stream() + .map(section -> evaluateSection(section, player)) .filter(Objects::nonNull) .filter(sectionText -> !sectionText.isEmpty()) .collect(Collectors.joining(separator)); } + + private String evaluateSection(ActionbarSection section, Player player) { + TimedAlert alert = slotAlerts.get(section.id()); + if (alert != null) { + if (!alert.isExpired()) { + return alert.message(); + } + slotAlerts.remove(section.id()); + } + return section.textSupplier().apply(player); + } } diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarSection.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarSection.java index af3232e7..4ce75f71 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarSection.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarSection.java @@ -19,7 +19,8 @@ public record ActionbarSection( @NonNull String id, int priority, @NonNull Function textSupplier, - @NonNull Predicate visibility + @NonNull Predicate visibility, + boolean exclusive ) { /** * Creates an action bar section that is always visible for all players. @@ -30,7 +31,7 @@ public record ActionbarSection( * @return created action bar section */ public static ActionbarSection of(String id, int priority, Function textSupplier) { - return new ActionbarSection(id, priority, textSupplier, player -> true); + return new ActionbarSection(id, priority, textSupplier, player -> true, false); } /** @@ -43,6 +44,19 @@ public static ActionbarSection of(String id, int priority, Function textSupplier, Predicate visibility) { - return new ActionbarSection(id, priority, textSupplier, visibility); + return new ActionbarSection(id, priority, textSupplier, visibility, false); + } + + /** + * Creates an exclusive action bar section that overrides non-exclusive sections when visible. + * + * @param id unique section identifier + * @param priority rendering priority + * @param textSupplier text supplier function + * @param visibility predicate controlling section display + * @return created exclusive action bar section + */ + public static ActionbarSection exclusiveOf(String id, int priority, Function textSupplier, Predicate visibility) { + return new ActionbarSection(id, priority, textSupplier, visibility, true); } } diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarService.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarService.java index 2ca22cbb..aedc7701 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarService.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarService.java @@ -25,6 +25,7 @@ */ public final class ActionbarService implements Listener { + // ponytail: kept — shared instance across plugins avoids actionbar packet collisions @Getter @Setter private static ActionbarService instance; @@ -52,10 +53,14 @@ public ActionbarService(@NonNull Plugin plugin) { * @param periodTicks interval between renders in server ticks */ public void start(long periodTicks) { - if (running) return; + if (running && tickerTask != null && !tickerTask.isCancelled()) return; running = true; - this.tickerTask = Bukkit.getScheduler().runTaskTimer(plugin, this::tickAll, periodTicks, periodTicks); + if (tickerTask != null) { + tickerTask.cancel(); + } + + this.tickerTask = Bukkit.getScheduler().runTaskTimer(plugin, this::tickAll, 0L, periodTicks); } /** @@ -176,9 +181,10 @@ public void renderPlayer(@NonNull Player player) { private void tickAll() { for (Player player : Bukkit.getOnlinePlayers()) { try { + // ponytail: kept — prevent single player evaluation error from cancelling remaining player renders renderPlayer(player); - } catch (Exception exception) { - Log.warn("ActionbarService", "Failed to render for " + player.getName() + ": " + exception.getMessage()); + } catch (Throwable throwable) { + Log.warn("ActionbarService", "Failed to render actionbar for " + player.getName() + ": " + throwable.getMessage()); } } } @@ -192,6 +198,7 @@ private ActionbarComposer getOrCreateComposer(UUID uuid) { } private void sendRawPacket(@NonNull Player player, @NonNull String text) { + if (text == null || text.isEmpty()) return; player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent(text)); } } diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java index fa954e71..351a88b9 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java @@ -715,19 +715,32 @@ public void refresh(@NonNull Player player) { slots.clear(); build(player); - if (player.getOpenInventory().getTopInventory().getHolder() == this) { - player.getOpenInventory().setTitle(this.title); + var openInventory = player.getOpenInventory(); + if (openInventory.getTopInventory().getHolder() == this && !openInventory.getTitle().equals(this.title)) { + openInventory.setTitle(this.title); } + boolean changed = false; for (int slot = 0; slot < size; slot++) { MenuSlot menuSlot = slots.get(slot); - if (menuSlot != null) { - inventory.setItem(slot, menuSlot.item); - } else if (!isInteractable(slot)) { - inventory.setItem(slot, null); + ItemStack newItem = menuSlot != null ? menuSlot.item : null; + ItemStack currentItem = inventory.getItem(slot); + + if (!isSameItem(currentItem, newItem)) { + inventory.setItem(slot, newItem); + changed = true; } } - player.updateInventory(); + + if (changed) { + player.updateInventory(); + } + } + + private boolean isSameItem(@Nullable ItemStack first, @Nullable ItemStack second) { + if (first == null && second == null) return true; + if (first == null || second == null) return false; + return first.getAmount() == second.getAmount() && first.isSimilar(second); } /** diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/system/MenuSystem.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/system/MenuSystem.java index 7e4f6445..c4357a04 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/system/MenuSystem.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/system/MenuSystem.java @@ -47,12 +47,15 @@ public MenuSystem(@NonNull Plugin plugin) { private void tickActiveMenus() { tickCounter++; for (Map.Entry entry : activeMenus.entrySet()) { - if (!(entry.getValue() instanceof TickableMenu tickable)) continue; - if (tickCounter % Math.max(1L, tickable.tickPeriodTicks()) != 0) continue; - - Player player = plugin.getServer().getPlayer(entry.getKey()); - if (player != null && player.isOnline()) { - tickable.onTick(player); + try { + if (!(entry.getValue() instanceof TickableMenu tickable)) continue; + if (tickCounter % Math.max(1L, tickable.tickPeriodTicks()) != 0) continue; + + Player player = plugin.getServer().getPlayer(entry.getKey()); + if (player != null && player.isOnline()) { + tickable.onTick(player); + } + } catch (Throwable _) { } } } diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java index 1e653ae1..b16f1818 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java @@ -3,6 +3,7 @@ import fr.moussax.bedrock.ui.menu.Menu; import fr.moussax.bedrock.ui.menu.interaction.MenuElementPreset; import fr.moussax.bedrock.ui.menu.interaction.MenuItemInteraction; +import lombok.Getter; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; @@ -17,8 +18,16 @@ *

Subclasses provide the total item count, item contents, and optional * click handling through {@link #onItemClick(Player, int, ClickType)}.

*/ +@Getter public abstract class PaginatedMenu extends Menu { + public static final int[] INNER_GRID_SLOTS = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34, + 37, 38, 39, 40, 41, 42, 43 + }; + protected int currentPage = 0; protected int totalItems = 0; @@ -50,30 +59,33 @@ protected PaginatedMenu(String title, int size) { protected abstract ItemStack getItem(@NonNull Player player, int index); /** - * Returns the 0-based index of the current page. + * Returns the target inventory slot indices for displaying paginated items. + * + *

If {@code null}, items are placed sequentially starting at slot 0.

* - * @return current page index (0-based) + * @return slot index array, or {@code null} for default sequential placement */ - public int getCurrentPage() { - return currentPage; + protected int[] getDisplaySlots() { + return null; } /** - * Returns the 1-based number of the current page. + * Returns an item to display when the menu has no items, or {@code null}. * - * @return current page number (1-based) + * @param player player viewing the menu + * @return empty state item, or {@code null} if none */ - public int getCurrentPageNumber() { - return currentPage + 1; + protected ItemStack getEmptyStateItem(@NonNull Player player) { + return null; } /** - * Returns the total item count evaluated during the last build. + * Returns the 1-based number of the current page. * - * @return total item count + * @return current page number (1-based) */ - public int getTotalItems() { - return totalItems; + public int getCurrentPageNumber() { + return currentPage + 1; } /** @@ -95,6 +107,10 @@ public int getTotalPages() { * @return number of items displayed per page */ protected int getItemsPerPage() { + int[] displaySlots = getDisplaySlots(); + if (displaySlots != null) { + return displaySlots.length; + } return size - 9; } @@ -109,46 +125,52 @@ protected int getItemsPerPage() { @Override public void build(@NonNull Player viewer) { totalItems = Math.max(0, getTotalItems(viewer)); + clearInventory(); + + ItemStack emptyItem = getEmptyStateItem(viewer); + if (totalItems == 0) { + if (emptyItem != null) { + setItem(22, emptyItem, MenuItemInteraction.ANY_CLICK, (_, _) -> { }); + } + setCloseButton(49); + return; + } int itemsPerPage = getItemsPerPage(); - int maxPage = Math.max(0, (totalItems - 1) / itemsPerPage); + int maxPage = (totalItems - 1) / itemsPerPage; currentPage = Math.min(currentPage, maxPage); int startIndex = currentPage * itemsPerPage; int endIndex = Math.min(startIndex + itemsPerPage, totalItems); - int slot = 0; + int[] displaySlots = getDisplaySlots(); + int slotIndex = 0; for (int i = startIndex; i < endIndex; i++) { final int index = i; + int slot = displaySlots != null ? displaySlots[slotIndex++] : i - startIndex; setItem( - slot++, + slot, getItem(viewer, index), MenuItemInteraction.ANY_CLICK, (player, click) -> onItemClick(player, index, click) ); } - // Previous page if (currentPage > 0) { - setBackButton(size - 9, - (player, type) -> { - currentPage--; - refresh(player); - } - ); + setBackButton(48, (player, _) -> { + currentPage--; + refresh(player); + }); } - // Next page if (endIndex < totalItems) { - setItem(size - 1, MenuElementPreset.NEXT_BUTTON, - (player, type) -> { - currentPage++; - refresh(player); - } - ); + setItem(50, MenuElementPreset.NEXT_BUTTON, (player, _) -> { + currentPage++; + refresh(player); + }); } - setCloseButton(size - 5); + setCloseButton(49); } /** diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java index 9d5ae19e..ab04833f 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java @@ -7,30 +7,22 @@ import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; import fr.moussax.blightedMod.registry.CommandsRegistry; import fr.moussax.blightedMod.registry.EventsRegistry; +import lombok.Getter; import org.bukkit.plugin.java.JavaPlugin; import java.sql.SQLException; public final class BlightedMod extends JavaPlugin { + @Getter private static BlightedMod instance; + @Getter private PluginDatabase pluginDatabase; + @Getter private ModerationManager moderationManager; private EventsRegistry eventsRegistry; - public static BlightedMod getInstance() { - return instance; - } - - public PluginDatabase getPluginDatabase() { - return pluginDatabase; - } - - public ModerationManager getModerationManager() { - return moderationManager; - } - @Override public void onEnable() { instance = this; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java index 63b3840d..5fd71c8c 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java @@ -9,6 +9,7 @@ import org.bukkit.entity.Player; import java.util.Arrays; +import java.util.Locale; import static fr.moussax.bedrock.text.Messenger.warn; @@ -20,19 +21,12 @@ public final class BanCommand extends ModerationCommand { @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { - if (label.equalsIgnoreCase("unban")) { - return handleUnban(moderator, arguments); - } - - if (label.equalsIgnoreCase("banip")) { - return handleBanIp(moderator, arguments); - } - - if (label.equalsIgnoreCase("unbanip")) { - return handleUnbanIp(moderator, arguments); - } - - return handleBan(moderator, arguments); + return switch (label.toLowerCase(Locale.ROOT)) { + case "unban" -> handleUnban(moderator, arguments); + case "banip" -> handleBanIp(moderator, arguments); + case "unbanip" -> handleUnbanIp(moderator, arguments); + default -> handleBan(moderator, arguments); + }; } private boolean handleBan(Player moderator, String[] arguments) { @@ -63,18 +57,8 @@ private boolean handleBan(Player moderator, String[] arguments) { String reason = arguments.length > reasonStartIndex ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) : "No reason specified"; - String ipAddress = PunishmentManager.getPlayerIp(target); - getPunishmentManager().addPunishment( - target.getUniqueId(), - target.getName(), - PunishmentData.PunishmentType.BAN, - reason, - moderator.getUniqueId(), - moderator.getName(), - expiresAt, - ipAddress - ); + getPunishmentManager().addBan(target, moderator, reason, expiresAt); String durationText = expiresAt != null ? DurationParser.formatDuration(arguments[1]) : "Permanent"; String banMessage = """ @@ -88,7 +72,7 @@ private boolean handleBan(Player moderator, String[] arguments) { target.kickPlayer(banMessage); String durationString = expiresAt != null ? " for §6" + durationText + "§e" : " permanently"; - String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e banned §9" + target.getName() + "§e" + durationString + " for §c" + reason + "§e."; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e banned §d" + target.getName() + "§e" + durationString + " for §c" + reason + "§e."; getModerationManager().broadcastToModerators(notification); return true; @@ -112,7 +96,7 @@ private boolean handleUnban(Player moderator, String[] arguments) { getPunishmentManager().removePunishment(target.getUniqueId(), PunishmentData.PunishmentType.BAN); - String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unbanned §9" + target.getName() + "§e."; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unbanned §d" + target.getName() + "§e."; getModerationManager().broadcastToModerators(notification); return true; @@ -146,18 +130,8 @@ private boolean handleBanIp(Player moderator, String[] arguments) { String reason = arguments.length > reasonStartIndex ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) : "No reason specified"; - String ipAddress = PunishmentManager.getPlayerIp(target); - getPunishmentManager().addPunishment( - target.getUniqueId(), - target.getName(), - PunishmentData.PunishmentType.IP_BAN, - reason, - moderator.getUniqueId(), - moderator.getName(), - expiresAt, - ipAddress - ); + getPunishmentManager().addIpBan(target, moderator, reason, expiresAt); String durationText = expiresAt != null ? DurationParser.formatDuration(arguments[1]) : "Permanent"; String banMessage = """ @@ -170,7 +144,7 @@ private boolean handleBanIp(Player moderator, String[] arguments) { target.kickPlayer(banMessage); - String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e IP banned §9" + target.getName() + "§e for §c" + reason + "§e."; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e IP banned §d" + target.getName() + "§e for §c" + reason + "§e."; getModerationManager().broadcastToModerators(notification); return true; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java index 7678809d..e6d490f1 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java @@ -4,7 +4,6 @@ import fr.moussax.blightedMod.commands.ModerationCommand; import fr.moussax.blightedMod.moderator.punishments.DurationParser; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; -import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; import org.bukkit.command.Command; import org.bukkit.entity.Player; @@ -52,21 +51,11 @@ private boolean handleMute(Player moderator, String[] arguments) { String reason = arguments.length > reasonStartIndex ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) : "No reason specified"; - String ipAddress = PunishmentManager.getPlayerIp(target); - - getPunishmentManager().addPunishment( - target.getUniqueId(), - target.getName(), - PunishmentData.PunishmentType.MUTE, - reason, - moderator.getUniqueId(), - moderator.getName(), - expiresAt, - ipAddress - ); + + getPunishmentManager().addMute(target, moderator, reason, expiresAt); String durationText = expiresAt != null ? "for §6" + DurationParser.formatDuration(arguments[1]) + " " : ""; - String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e muted §d" + target.getName() + " §e" + durationText + "for §c" + reason + "§e."; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e muted §d" + target.getName() + " §e" + durationText + "§efor §c" + reason + "§e."; getModerationManager().broadcastToModerators(notification); String durationString = expiresAt != null ? "for §6" + DurationParser.formatDuration(arguments[1]) + " §7" : ""; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java index 63f3bf09..07e0f381 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java @@ -8,14 +8,13 @@ import org.bukkit.command.Command; import org.bukkit.entity.Player; +import org.bukkit.attribute.Attribute; +import java.util.Objects; import java.util.List; import static fr.moussax.bedrock.text.Messenger.inform; import static fr.moussax.bedrock.text.Messenger.warn; -/** - * Moderation command to view detailed player info (/userinfo ). - */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class UserInfoCommand extends ModerationCommand { @@ -33,8 +32,8 @@ protected boolean executeModeration(Player moderator, Command command, String la String ipAddress = PunishmentManager.getPlayerIp(target); Location location = target.getLocation(); - // ponytail: kept — Spigot 26.2 returns double for getMaxHealth(); direct Bukkit API call used for health percentage. - int healthPercent = (int) Math.round((target.getHealth() / target.getMaxHealth()) * 100.0); + double maxHealth = Objects.requireNonNull(target.getAttribute(Attribute.MAX_HEALTH)).getValue(); + int healthPercent = (int) Math.round((target.getHealth() / maxHealth) * 100.0); List punishments = getPunishmentManager().getAllPunishments(target.getName()); long mutesCount = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.MUTE).count(); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java index acad88c4..c446c082 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java @@ -19,6 +19,7 @@ public enum ChatChannel { STAFF } + @Getter private final PunishmentManager punishmentManager; private final Map moderators = new HashMap<>(); private final Set frozenPlayers = new HashSet<>(); @@ -26,12 +27,9 @@ public enum ChatChannel { private final Map playerChatChannels = new ConcurrentHashMap<>(); private final Set messageInspectEnabled = ConcurrentHashMap.newKeySet(); private final Map lastMessageTargets = new ConcurrentHashMap<>(); + @Getter private int slowmodeDelaySeconds = 0; - public PunishmentManager getPunishmentManager() { - return punishmentManager; - } - private ModerationManager(PunishmentManager punishmentManager) { this.punishmentManager = punishmentManager; } @@ -96,10 +94,6 @@ public void setSlowmodeDelaySeconds(int seconds) { this.slowmodeDelaySeconds = Math.max(0, seconds); } - public int getSlowmodeDelaySeconds() { - return slowmodeDelaySeconds; - } - public boolean isSlowmodeEnabled() { return slowmodeDelaySeconds > 0; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java index 1c342828..58151498 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java @@ -3,9 +3,11 @@ import fr.moussax.bedrock.ui.actionbar.ActionbarSection; import fr.moussax.blightedMod.moderator.BlightedModerator; import fr.moussax.blightedMod.moderator.ModerationManager; +import org.bukkit.attribute.Attribute; import org.bukkit.entity.Player; import java.util.Locale; +import java.util.Objects; public final class ModerationHud { public static final String SECTION_ID = "moderation_hud"; @@ -15,9 +17,9 @@ private ModerationHud() { } public static ActionbarSection createSection(ModerationManager moderationManager) { - return ActionbarSection.of(SECTION_ID, PRIORITY, + return ActionbarSection.exclusiveOf(SECTION_ID, PRIORITY, player -> render(moderationManager, player), - moderationManager::isModerator + moderationManager::isInModerationMode ); } @@ -32,18 +34,17 @@ private static String render(ModerationManager moderationManager, Player player) return null; } - int healthPercent = (int) Math.round((target.getHealth() / target.getHealth()) * 100.0); + double maxHealth = Objects.requireNonNull(target.getAttribute(Attribute.MAX_HEALTH)).getValue(); + int healthPercent = (int) Math.round((target.getHealth() / maxHealth) * 100.0); String distanceText = player.getWorld().equals(target.getWorld()) ? String.format(Locale.ROOT, "%.2f blocks", player.getLocation().distance(target.getLocation())) : "N/A"; String frozenText = moderationManager.isFrozen(target) ? "§b§lYUP!" : "§c§lNOPE!"; - String pingText = target.getPing() < 100 ? "§a" + target.getPing() + "ms" - : target.getPing() < 200 ? "§e" + target.getPing() + "ms" - : target.getPing() < 300 ? "§6" + target.getPing() + "ms" - : target.getPing() < 500 ? "§c" + target.getPing() + "ms" - : "§4" + target.getPing() + "ms"; + int ping = target.getPing(); + String pingColor = ping < 100 ? "§a" : ping < 200 ? "§e" : ping < 300 ? "§6" : ping < 500 ? "§c" : "§4"; + String pingText = pingColor + ping + "ms"; return "§fTarget: §d" + target.getName() + " §fHP: §d" + healthPercent + "%" diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java index 1a590230..d9bc54f3 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java @@ -7,9 +7,6 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.AsyncPlayerChatEvent; -/** - * Handles interactive chat flag rendering for chat reporting via /report chat . - */ public final class InteractiveChatListener implements Listener { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @@ -20,9 +17,7 @@ public void onNormalPlayerChat(AsyncPlayerChatEvent event) { String chatMessage = event.getMessage(); InteractiveMessage interactiveMessage = InteractiveMessage.text(" ") - .hoverAndExecute( - "§c⚑ ", - "§cClick to report §f" + sender.getName(), + .hoverAndExecute("§c⚑ ", "§cClick to report §f" + sender.getName(), "/report chat " + sender.getName() + " " + chatMessage ) .append("§7" + sender.getName() + " §f§l» §7" + chatMessage); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java index 66eb90b0..1dc0d4d4 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java @@ -87,7 +87,9 @@ public void onPlayerLogin(PlayerLoginEvent event) { return; } - String ipAddress = PunishmentManager.getPlayerIp(player); + String ipAddress = event.getAddress().getHostAddress() != null + ? event.getAddress().getHostAddress() + : PunishmentManager.getPlayerIp(player); PunishmentData ipBan = punishmentManager.getActiveIpBan(ipAddress); if (ipBan != null) { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java index 5c1eaf8a..1919136a 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java @@ -1,8 +1,6 @@ package fr.moussax.blightedMod.moderator.menus; import fr.moussax.bedrock.ui.menu.TickableMenu; -import fr.moussax.bedrock.ui.menu.interaction.MenuElementPreset; -import fr.moussax.bedrock.ui.menu.interaction.MenuItemInteraction; import fr.moussax.bedrock.ui.menu.types.PaginatedMenu; import fr.moussax.bedrock.utils.ItemBuilder; import fr.moussax.blightedMod.moderator.ModerationManager; @@ -20,28 +18,12 @@ import static fr.moussax.bedrock.text.Messenger.inform; import static fr.moussax.bedrock.text.Messenger.warn; -/** - * Paginated menu displaying active player reports for staff review. - * Ticks periodically to support real-time updates while open. - */ public final class ReportsCenterMenu extends PaginatedMenu implements TickableMenu { - private static final int[] REPORT_SLOTS = { - 10, 11, 12, 13, 14, 15, 16, - 19, 20, 21, 22, 23, 24, 25, - 28, 29, 30, 31, 32, 33, 34, - 37, 38, 39, 40, 41, 42, 43 - }; - public ReportsCenterMenu() { super("Active Reports", 54); } - @Override - public long tickPeriodTicks() { - return 20L; - } - @Override public void onTick(Player player) { refresh(player); @@ -53,8 +35,16 @@ protected int getTotalItems(@NonNull Player player) { } @Override - protected int getItemsPerPage() { - return REPORT_SLOTS.length; + protected int[] getDisplaySlots() { + return INNER_GRID_SLOTS; + } + + @Override + protected ItemStack getEmptyStateItem(@NonNull Player player) { + return new ItemBuilder(Material.RED_STAINED_GLASS_PANE) + .setDisplayName("§cNo Active Reports") + .addLore("§7There are currently no active reports.") + .toItemStack(); } @Override @@ -87,57 +77,6 @@ protected ItemStack getItem(@NonNull Player player, int index) { return builder.toItemStack(); } - @Override - public void build(@NonNull Player viewer) { - totalItems = Math.max(0, getTotalItems(viewer)); - - int itemsPerPage = getItemsPerPage(); - int maxPage = Math.max(0, (totalItems - 1) / itemsPerPage); - currentPage = Math.min(currentPage, maxPage); - - int startIndex = currentPage * itemsPerPage; - int endIndex = Math.min(startIndex + itemsPerPage, totalItems); - - clearInventory(); - - List reports = ReportManager.getInstance().getActiveReports(); - if (reports.isEmpty()) { - ItemStack noReportsItem = new ItemBuilder(Material.RED_STAINED_GLASS_PANE) - .setDisplayName("§cNo Active Reports") - .addLore("§7There are currently no active reports.") - .toItemStack(); - - setItem(22, noReportsItem, MenuItemInteraction.ANY_CLICK, (player, _) -> { }); - } else { - int slotIndex = 0; - for (int i = startIndex; i < endIndex && slotIndex < REPORT_SLOTS.length; i++, slotIndex++) { - final int index = i; - setItem( - REPORT_SLOTS[slotIndex], - getItem(viewer, index), - MenuItemInteraction.ANY_CLICK, - (player, click) -> onItemClick(player, index, click) - ); - } - } - - if (currentPage > 0) { - setBackButton(48, (player, _) -> { - currentPage--; - refresh(player); - }); - } - - if (endIndex < totalItems) { - setItem(50, MenuElementPreset.NEXT_BUTTON, (player, _) -> { - currentPage++; - refresh(player); - }); - } - - setCloseButton(49); - } - @Override protected void onItemClick(@NonNull Player moderator, int index, @NonNull ClickType clickType) { List reports = ReportManager.getInstance().getActiveReports(); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java index d2968d2c..8d5758bb 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java @@ -1,36 +1,28 @@ package fr.moussax.blightedMod.moderator.menus; import fr.moussax.bedrock.ui.menu.TickableMenu; -import fr.moussax.bedrock.ui.menu.interaction.MenuElementPreset; -import fr.moussax.bedrock.ui.menu.interaction.MenuItemInteraction; import fr.moussax.bedrock.ui.menu.types.PaginatedMenu; import fr.moussax.bedrock.utils.ItemBuilder; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; +import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.SkullMeta; import org.jspecify.annotations.NonNull; import java.text.SimpleDateFormat; +import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Locale; -/** - * Paginated menu displaying historical sanctions for a specific player. - * Features target player head, Hypixel-style hopper filtering, and real-time updates. - */ public final class SanctionsMenu extends PaginatedMenu implements TickableMenu { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); - private static final int[] SANCTION_SLOTS = { - 10, 11, 12, 13, 14, 15, 16, - 19, 20, 21, 22, 23, 24, 25, - 28, 29, 30, 31, 32, 33, 34, - 37, 38, 39, 40, 41, 42, 43 - }; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMM dd, yyyy 'at' hh:mm a", Locale.US); + @Getter public enum SanctionFilter { ALL("All Sanctions"), ACTIVE("Active Only"), @@ -44,14 +36,15 @@ public enum SanctionFilter { this.displayName = displayName; } - public String getDisplayName() { - return displayName; - } - public SanctionFilter next() { SanctionFilter[] values = values(); return values[(ordinal() + 1) % values.length]; } + + public SanctionFilter previous() { + SanctionFilter[] values = values(); + return values[(ordinal() - 1 + values.length) % values.length]; + } } private final String targetName; @@ -59,7 +52,7 @@ public SanctionFilter next() { private SanctionFilter currentFilter = SanctionFilter.ALL; public SanctionsMenu(String targetName, List punishments) { - super(targetName +"'s sanctions", 54); + super(targetName + "'s sanctions", 54); this.targetName = targetName; this.punishments = punishments; } @@ -69,14 +62,18 @@ public void onTick(Player player) { refresh(player); } + private List getFilteredPunishments() { - return punishments.stream().filter(punishment -> switch (currentFilter) { - case ALL -> true; - case ACTIVE -> punishment.active() && !punishment.isExpired(); - case BAN -> punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN; - case MUTE -> punishment.type() == PunishmentData.PunishmentType.MUTE; - case KICK -> punishment.type() == PunishmentData.PunishmentType.KICK; - }).toList(); + return punishments.stream() + .filter(punishment -> switch (currentFilter) { + case ALL -> true; + case ACTIVE -> punishment.active() && !punishment.isExpired(); + case BAN -> punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN; + case MUTE -> punishment.type() == PunishmentData.PunishmentType.MUTE; + case KICK -> punishment.type() == PunishmentData.PunishmentType.KICK; + }) + .sorted(Comparator.comparingInt(PunishmentData::id)) + .toList(); } @Override @@ -84,11 +81,6 @@ protected int getTotalItems(@NonNull Player player) { return getFilteredPunishments().size(); } - @Override - protected int getItemsPerPage() { - return SANCTION_SLOTS.length; - } - @Override protected ItemStack getItem(@NonNull Player player, int index) { List filtered = getFilteredPunishments(); @@ -97,101 +89,130 @@ protected ItemStack getItem(@NonNull Player player, int index) { } PunishmentData punishment = filtered.get(index); + + String typeTitle = switch (punishment.type()) { + case BAN -> "§c§lBAN"; + case IP_BAN -> "§c§lIP BAN"; + case MUTE -> "§e§lMUTE"; + case KICK -> "§f§lKICK"; + }; + Material icon = switch (punishment.type()) { case BAN, IP_BAN -> Material.REDSTONE_BLOCK; case MUTE -> Material.WRITABLE_BOOK; - case KICK -> Material.LEATHER_BOOTS; + case KICK -> Material.COPPER_BOOTS; }; + boolean isActive = punishment.active() && !punishment.isExpired(); + String activeStatus = isActive ? "§a§lACTIVE" : "§c§lEXPIRED"; String dateText = DATE_FORMAT.format(new Date(punishment.createdAt())); - String durationText = punishment.expiresAt() == null ? "Permanent" : DATE_FORMAT.format(new Date(punishment.expiresAt())); - String activeStatus = punishment.active() && !punishment.isExpired() ? "§aACTIVE" : "§cEXPIRED"; - - return new ItemBuilder(icon) - .setDisplayName("§c" + punishment.type().name().replace("_", " ") + " §f| §e#" + punishment.id()) - .addLore("§7Target: §b" + targetName) - .addLore("§7Issued By: §e" + punishment.moderatorName()) - .addLore("§7Reason: §f" + punishment.reason()) - .addLore("§7Issued At: §e" + dateText) - .addLore("§7Expires: §f" + durationText) - .addLore("§7Status: " + activeStatus) - .toItemStack(); - } - - @Override - public void build(@NonNull Player viewer) { - List filtered = getFilteredPunishments(); - totalItems = filtered.size(); - int itemsPerPage = getItemsPerPage(); - int maxPage = Math.max(0, (totalItems - 1) / itemsPerPage); - currentPage = Math.min(currentPage, maxPage); + ItemBuilder builder = new ItemBuilder(icon) + .setDisplayName(typeTitle + " §7(#" + punishment.id() + ")"); - int startIndex = currentPage * itemsPerPage; - int endIndex = Math.min(startIndex + itemsPerPage, totalItems); + if (punishment.type() != PunishmentData.PunishmentType.KICK) { + builder.addLore( + "", + " §7Status: " + activeStatus, + " §7Reason: §f" + punishment.reason(), + "", + " §7Issued By: §e" + punishment.moderatorName(), + " §7Issued At: §f" + dateText + " " + ); - clearInventory(); + String durationText = punishment.isPermanent() + ? "§cPermanent" + : formatDurationSeconds(Math.max(1, (punishment.expiresAt() - punishment.createdAt()) / 1000L)); - renderTargetHead(); + builder.addLore(" §7Duration: §d" + durationText); - if (filtered.isEmpty()) { - ItemStack noSanctionsItem = new ItemBuilder(Material.RED_STAINED_GLASS_PANE) - .setDisplayName("§cNo Sanctions Found") - .addLore("§7No sanctions match the current filter.") - .toItemStack(); + if (punishment.expiresAt() != null) { + builder.addLore(" §7Expires At: §f" + DATE_FORMAT.format(new Date(punishment.expiresAt())) + " "); + } - setItem(22, noSanctionsItem, MenuItemInteraction.ANY_CLICK, (player, _) -> { }); - } else { - int slotIndex = 0; - for (int i = startIndex; i < endIndex && slotIndex < SANCTION_SLOTS.length; i++, slotIndex++) { - final int index = i; - setItem( - SANCTION_SLOTS[slotIndex], - getItem(viewer, index), - MenuItemInteraction.ANY_CLICK, - (player, click) -> onItemClick(player, index, click) - ); + if (punishment.type() == PunishmentData.PunishmentType.IP_BAN && punishment.ipAddress() != null && !punishment.ipAddress().equals("0.0.0.0")) { + builder.addLore(" §7IP Address: §d" + punishment.ipAddress()); } + } else { + builder.addLore( + "", + " §7Reason: §f" + punishment.reason(), + "", + " §7Issued By: §e" + punishment.moderatorName(), + " §7Issued At: §f" + dateText + " " + ); } - if (currentPage > 0) { - setBackButton(48, (player, _) -> { - currentPage--; - refresh(player); - }); - } + builder.addLore(""); + builder.addItemFlag(ItemFlag.HIDE_ATTRIBUTES); + return builder.toItemStack(); + } - if (endIndex < totalItems) { - setItem(50, MenuElementPreset.NEXT_BUTTON, (player, _) -> { - currentPage++; - refresh(player); - }); + private static String formatDurationSeconds(long totalSeconds) { + if (totalSeconds >= 31536000L) { + long years = totalSeconds / 31536000L; + return years + " year" + (years > 1 ? "s" : ""); + } + if (totalSeconds >= 604800L) { + long weeks = totalSeconds / 604800L; + return weeks + " week" + (weeks > 1 ? "s" : ""); + } + if (totalSeconds >= 86400L) { + long days = totalSeconds / 86400L; + return days + " day" + (days > 1 ? "s" : ""); + } + if (totalSeconds >= 3600L) { + long hours = totalSeconds / 3600L; + return hours + " hour" + (hours > 1 ? "s" : ""); } + if (totalSeconds >= 60L) { + long minutes = totalSeconds / 60L; + return minutes + " minute" + (minutes > 1 ? "s" : ""); + } + return totalSeconds + " second" + (totalSeconds > 1 ? "s" : ""); + } + + @Override + protected int[] getDisplaySlots() { + return INNER_GRID_SLOTS; + } + @Override + protected ItemStack getEmptyStateItem(@NonNull Player player) { + return new ItemBuilder(Material.RED_STAINED_GLASS_PANE) + .setDisplayName("§cNo Sanctions Found") + .addLore("§7No sanctions match the current filter.") + .toItemStack(); + } + + @Override + public void build(@NonNull Player viewer) { + super.build(viewer); + renderTargetHead(); renderFilterHopper(); - setCloseButton(49); } private void renderTargetHead() { - ItemStack headStack = new ItemStack(Material.PLAYER_HEAD); - SkullMeta skullMeta = (SkullMeta) headStack.getItemMeta(); - if (skullMeta != null) { - skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(targetName)); - skullMeta.setDisplayName("§d" + targetName); - - long activeBans = punishments.stream().filter(punishment -> (punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN) && punishment.active() && !punishment.isExpired()).count(); - long activeMutes = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.MUTE && punishment.active() && !punishment.isExpired()).count(); - long totalKicks = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.KICK).count(); - - skullMeta.setLore(List.of( - "§7Total Sanctions: §e" + punishments.size(), - "§7Active Bans: §c" + activeBans, - "§7Active Mutes: §d" + activeMutes, - "§7Total Kicks: §f" + totalKicks - )); - headStack.setItemMeta(skullMeta); - } - setItem(4, headStack, MenuItemInteraction.ANY_CLICK, (player, _) -> { }); + long activeBans = punishments.stream().filter(punishment -> (punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN) && punishment.active() && !punishment.isExpired()).count(); + long activeMutes = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.MUTE && punishment.active() && !punishment.isExpired()).count(); + long totalKicks = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.KICK).count(); + + ItemStack headItem = new ItemBuilder(Material.PLAYER_HEAD) + .setSkullOwner(Bukkit.getOfflinePlayer(targetName).getUniqueId()) + .setDisplayName("§d" + targetName) + .addLore( + "", + " §7Total Sanctions: §e" + punishments.size() + " ", + "", + " §7Active Bans: §c" + activeBans, + " §7Active Mutes: §e" + activeMutes, + " §7Total Kicks: §f" + totalKicks, + "" + ) + .addItemFlag(ItemFlag.HIDE_PROFILE) + .toItemStack(); + + setItem(4, headItem); } private void renderFilterHopper() { @@ -207,10 +228,19 @@ private void renderFilterHopper() { .addLore("§bRight-click to go backwards!") .addLore("§eClick to switch filter!"); - setItem(52, hopperBuilder.toItemStack(), MenuItemInteraction.ANY_CLICK, (player, _) -> { - currentFilter = currentFilter.next(); - currentPage = 0; - refresh(player); - }); + setItem( + 53, + hopperBuilder.toItemStack(), + (player, _) -> { + currentFilter = currentFilter.next(); + currentPage = 0; + refresh(player); + }, + (player, _) -> { + currentFilter = currentFilter.previous(); + currentPage = 0; + refresh(player); + } + ); } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java index 701790cb..6751844d 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java @@ -1,6 +1,5 @@ package fr.moussax.blightedMod.moderator.punishments; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java index 31706a95..032e8b7d 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java @@ -227,9 +227,14 @@ WHERE LOWER(player_name) = LOWER(?) } public static String getPlayerIp(Player player) { - InetSocketAddress address = player.getAddress(); - if (address == null || address.getAddress() == null) return "0.0.0.0"; - String host = address.getAddress().getHostAddress(); - return host != null ? host : "0.0.0.0"; + try { + if (player == null) return "0.0.0.0"; + InetSocketAddress address = player.getAddress(); + if (address == null || address.getAddress() == null) return "0.0.0.0"; + String host = address.getAddress().getHostAddress(); + return host != null ? host : "0.0.0.0"; + } catch (Throwable _) { + return "0.0.0.0"; + } } } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/BlightedSMP.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/BlightedSMP.java index 10c2325d..2630c6a8 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/BlightedSMP.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/BlightedSMP.java @@ -13,7 +13,9 @@ import fr.moussax.bedrock.scheduling.PluginContext; import fr.moussax.bedrock.utils.debug.Log; import lombok.Getter; +import fr.moussax.blightedSMP.engine.player.BlightedPlayer; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; import java.sql.SQLException; @@ -58,6 +60,10 @@ public void onEnable() { eventsRegistry.initializeListeners(); eventsRegistry.buildSpawnCache(); + for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { + BlightedPlayer.get(onlinePlayer); + } + BlightedServer.rehydrateEntitiesOnLoadedChunks(this); new BlightedSpawnEngine().runTaskTimer(this, 100L, 20L); } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/registry/menu/ItemRegistryMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/registry/menu/ItemRegistryMenu.java index c34dcf5b..0844d9c0 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/registry/menu/ItemRegistryMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/registry/menu/ItemRegistryMenu.java @@ -24,15 +24,8 @@ import java.util.function.Predicate; public final class ItemRegistryMenu { - private static final int[] CATEGORY_SLOTS = { - 10, 11, 12, 13, 14, 15, 16, - 19, 20, 21, 22, 23, 24, 25, - 28, 29, 30, 31, 32, 33, 34, - 37, 38, 39, 40, 41, 42, 43 - }; - + private static final int[] CATEGORY_SLOTS = PaginatedMenu.INNER_GRID_SLOTS; private static final int SEARCH_SLOT = 41; - private static final int[] ITEM_SLOTS = CATEGORY_SLOTS; private static ItemBuilder hideAllItemFlags(ItemBuilder builder) { return builder.addItemFlag( @@ -152,8 +145,15 @@ protected int getTotalItems(@NonNull Player player) { } @Override - protected int getItemsPerPage() { - return 28; + protected int[] getDisplaySlots() { + return INNER_GRID_SLOTS; + } + + @Override + protected ItemStack getEmptyStateItem(@NonNull Player player) { + return buildMenuItem(new ItemStack(Material.RED_STAINED_GLASS_PANE), + "§cNo Items Found", + List.of("§7No items match the criteria")); } @Override @@ -178,43 +178,10 @@ protected ItemStack getItem(@NonNull Player player, int index) { @Override public void build(@NonNull Player player) { - int totalCount = getTotalItems(player); - int start = currentPage * getItemsPerPage(); - int end = Math.min(start + getItemsPerPage(), totalCount); - - if (blightedItems.isEmpty()) { - setItem(22, buildMenuItem(new ItemStack(Material.RED_STAINED_GLASS_PANE), - "§cNo Items Found", - List.of("§7No items match the criteria")), MenuItemInteraction.ANY_CLICK, (clickingPlayer, _) -> { - }); - } else { - for (int slotIndex = 0, i = start; i < end && slotIndex < ITEM_SLOTS.length; i++, slotIndex++) { - final int itemIndex = i; - setItem(ITEM_SLOTS[slotIndex], getItem(player, itemIndex), MenuItemInteraction.ANY_CLICK, - (clickingPlayer, clickType) -> onItemClick(clickingPlayer, itemIndex, clickType)); - } - } - setNavigation(player, totalCount); - } - - private void setNavigation(Player player, int totalItemsCount) { - if (currentPage > 0) { - setBackButton(48, (clickingPlayer, _) -> { - currentPage--; - refresh(clickingPlayer); - }); - } else if (previousMenu != null) { + super.build(player); + if (currentPage == 0 && previousMenu != null) { setBackButton(48, previousMenu); } - - if ((currentPage + 1) * getItemsPerPage() < totalItemsCount) { - setItem(50, MenuElementPreset.NEXT_BUTTON, (clickingPlayer, _) -> { - currentPage++; - refresh(clickingPlayer); - }); - } - - setCloseButton(49); } @Override diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java index 6577da0a..cb674ffb 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java @@ -13,6 +13,7 @@ import org.bukkit.inventory.ItemStack; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; /** * Server-side domain context for a connected {@link Player}. @@ -23,7 +24,7 @@ */ public final class BlightedPlayer { - private static final Map players = new HashMap<>(); + private static final Map players = new ConcurrentHashMap<>(); private static final double DEFAULT_MAX_MANA = 100; private static final double DEFAULT_MANA_REGEN_RATE = 0.5; @@ -90,8 +91,14 @@ public void tick() { * @param player player whose context to retrieve * @return registered player context, or {@code null} if no context exists */ + // ponytail: kept — explicit get-then-instantiate avoids re-entrant computeIfAbsent mutation in constructor public static BlightedPlayer get(Player player) { - return player != null ? players.get(player.getUniqueId()) : null; + if (player == null || !player.isOnline()) return null; + BlightedPlayer blightedPlayer = players.get(player.getUniqueId()); + if (blightedPlayer == null) { + blightedPlayer = new BlightedPlayer(player); + } + return blightedPlayer; } /** diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/registry/EventsRegistry.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/registry/EventsRegistry.java index 00683f15..61a0d332 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/registry/EventsRegistry.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/registry/EventsRegistry.java @@ -20,7 +20,6 @@ import fr.moussax.bedrock.ui.menu.system.MenuListener; import fr.moussax.bedrock.ui.menu.system.MenuSystem; import fr.moussax.bedrock.ui.sign.SignInputListener; -import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.plugin.PluginManager; From 828284d91959cecf92cf6ec0f1d0e4df8003c466 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:22:35 +0200 Subject: [PATCH 08/30] fix(menu): implement native line wrapping and page auto-splitting splits long book text natively across pages to preserve formatting, colors, and click actions without relying on client-side pagination --- .../fr/moussax/bedrock/ui/book/BookMenu.java | 151 +++++++++++++++++- .../moderator/menus/ReportMenu.java | 113 +++++++++++++ 2 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java index 14ebb177..4299291e 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java @@ -2,6 +2,7 @@ import fr.moussax.bedrock.text.InteractiveMessage; import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -20,7 +21,10 @@ */ public final class BookMenu { - private final List pages = new ArrayList<>(); + private static final int MAXIMUM_CHARACTERS_PER_LINE = 19; + private static final int MAXIMUM_LINES_PER_PAGE = 14; + + private final List rawPages = new ArrayList<>(); private BookMenu() { } @@ -41,7 +45,7 @@ public static BookMenu builder() { * @return this book menu */ public BookMenu addPage(@NonNull InteractiveMessage message) { - this.pages.add(message.build()); + this.rawPages.add(message.build()); return this; } @@ -57,14 +61,15 @@ public BookMenu addPage(@NonNull InteractiveMessage message) { public BookMenu addPage(@NonNull Consumer pageConfigurator) { InteractiveMessage message = InteractiveMessage.text(""); pageConfigurator.accept(message); - this.pages.add(message.build()); + this.rawPages.add(message.build()); return this; } /** * Opens the configured book for a player. * - *

The generated book uses the default BlightedMC title and author.

+ *

The generated book uses the default BlightedMC title and author, + * and automatically paginates content overflowing single-page dimensions.

* * @param player player receiving the book */ @@ -77,11 +82,147 @@ public void open(@NonNull Player player) { meta.setAuthor("BlightedMC"); meta.setGeneration(BookMeta.Generation.ORIGINAL); - for (BaseComponent[] pageComponents : pages) { + List paginatedPages = buildPaginatedPages(); + for (BaseComponent[] pageComponents : paginatedPages) { meta.spigot().addPage(pageComponents); } book.setItemMeta(meta); player.openBook(book); } + + private List buildPaginatedPages() { + List formattedPages = new ArrayList<>(); + + for (BaseComponent[] pageComponents : rawPages) { + List splitPages = splitSinglePageComponents(pageComponents); + formattedPages.addAll(splitPages); + } + + return formattedPages; + } + + private List splitSinglePageComponents(BaseComponent[] pageComponents) { + List flattenedComponents = new ArrayList<>(); + for (BaseComponent baseComponent : pageComponents) { + flattenComponent(baseComponent, flattenedComponents); + } + + List> lines = new ArrayList<>(); + List currentLine = new ArrayList<>(); + int currentLineLength = 0; + + for (TextComponent component : flattenedComponents) { + String componentText = component.getText(); + if (componentText == null || componentText.isEmpty()) { + continue; + } + + String[] paragraphs = componentText.split("\r?\n", -1); + for (int paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) { + if (paragraphIndex > 0) { + lines.add(currentLine); + currentLine = new ArrayList<>(); + currentLineLength = 0; + } + + String paragraphText = paragraphs[paragraphIndex]; + if (paragraphText.isEmpty()) { + continue; + } + + String[] words = paragraphText.split(" ", -1); + for (int wordIndex = 0; wordIndex < words.length; wordIndex++) { + String word = words[wordIndex]; + if (wordIndex > 0 && !currentLine.isEmpty() && words.length > 1) { + word = " " + word; + } + + while (word.length() > MAXIMUM_CHARACTERS_PER_LINE) { + String chunk = word.substring(0, MAXIMUM_CHARACTERS_PER_LINE); + word = word.substring(MAXIMUM_CHARACTERS_PER_LINE); + + TextComponent chunkComponent = duplicateComponentWithText(component, chunk); + if (currentLineLength + chunk.length() > MAXIMUM_CHARACTERS_PER_LINE && !currentLine.isEmpty()) { + lines.add(currentLine); + currentLine = new ArrayList<>(); + currentLineLength = 0; + } + currentLine.add(chunkComponent); + lines.add(currentLine); + currentLine = new ArrayList<>(); + currentLineLength = 0; + } + + if (word.isEmpty()) { + continue; + } + + if (currentLineLength + word.length() > MAXIMUM_CHARACTERS_PER_LINE && !currentLine.isEmpty()) { + lines.add(currentLine); + currentLine = new ArrayList<>(); + currentLineLength = 0; + if (word.startsWith(" ")) { + word = word.substring(1); + } + } + + TextComponent wordComponent = duplicateComponentWithText(component, word); + currentLine.add(wordComponent); + currentLineLength += word.length(); + } + } + } + + if (!currentLine.isEmpty()) { + lines.add(currentLine); + } + + if (lines.isEmpty()) { + return List.of(new BaseComponent[][]{pageComponents}); + } + + List resultPages = new ArrayList<>(); + for (int lineIndex = 0; lineIndex < lines.size(); lineIndex += MAXIMUM_LINES_PER_PAGE) { + int endIndex = Math.min(lineIndex + MAXIMUM_LINES_PER_PAGE, lines.size()); + List> pageLines = lines.subList(lineIndex, endIndex); + + List pageComponentsList = new ArrayList<>(); + for (int lineSubIndex = 0; lineSubIndex < pageLines.size(); lineSubIndex++) { + List line = pageLines.get(lineSubIndex); + pageComponentsList.addAll(line); + if (lineSubIndex < pageLines.size() - 1) { + pageComponentsList.add(new TextComponent("\n")); + } + } + + resultPages.add(pageComponentsList.toArray(new BaseComponent[0])); + } + + return resultPages; + } + + private void flattenComponent(BaseComponent component, List targetList) { + if (component instanceof TextComponent textComponent) { + targetList.add(textComponent); + } + if (component.getExtra() != null) { + for (BaseComponent extraComponent : component.getExtra()) { + flattenComponent(extraComponent, targetList); + } + } + } + + private TextComponent duplicateComponentWithText(TextComponent source, String text) { + TextComponent copy = new TextComponent(text); + copy.setColor(source.getColor()); + copy.setBold(source.isBoldRaw()); + copy.setItalic(source.isItalicRaw()); + copy.setUnderlined(source.isUnderlinedRaw()); + copy.setStrikethrough(source.isStrikethroughRaw()); + copy.setObfuscated(source.isObfuscatedRaw()); + copy.setHoverEvent(source.getHoverEvent()); + copy.setClickEvent(source.getClickEvent()); + return copy; + } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java new file mode 100644 index 00000000..e24363a1 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java @@ -0,0 +1,113 @@ +package fr.moussax.blightedMod.moderator.menus; + +import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.bedrock.ui.menu.Menu; +import fr.moussax.bedrock.utils.ItemBuilder; +import fr.moussax.blightedMod.moderator.ModerationManager; +import fr.moussax.blightedMod.moderator.reports.ReportData; +import fr.moussax.blightedMod.moderator.reports.ReportManager; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NonNull; + +import static fr.moussax.bedrock.text.Messenger.inform; + +public final class ReportMenu extends Menu { + + private record ReportCategory(int slot, Material material, String reason, String line1, String line2) {} + + private static final ReportCategory[] CATEGORIES = { + new ReportCategory(20, Material.WRITABLE_BOOK, "Chat Abuse / Scam", + "§7Using offensive chat, scamming,", "§7or engaging in offensive chat behavior."), + new ReportCategory(21, Material.DIAMOND_SWORD, "Cheating / Hacks", + "§7Using illegal modifications, movement", "§7hacks, or combat advantages."), + new ReportCategory(22, Material.NAME_TAG, "Inappropriate Skin / Name", + "§7Using offensive, inappropriate, or", "§7rule-breaking skins or usernames."), + new ReportCategory(23, Material.CHEST, "Griefing / Theft", + "§7Destroying claims, stealing items,", "§7or damaging another player's property."), + new ReportCategory(24, Material.COMPASS, "Cross-Teaming / Exploits", + "§7Teaming across alliances or abusing", "§7server bugs and game exploits."), + new ReportCategory(30, Material.SCAFFOLDING, "Inappropriate Build", + "§7Building offensive structures, hate", "§7symbols, or rule-breaking builds."), + new ReportCategory(31, Material.CLOCK, "Spam / Chat Flooding", + "§7Flooding chat with repeated messages", "§7or excessive unwanted content."), + new ReportCategory(32, Material.WITHER_ROSE, "Harassment / Toxicity", + "§7Targeted harassment, hate speech,", "§7or persistently toxic behavior."), + }; + + private final String targetName; + private final String attachedMessage; + + public ReportMenu(String targetName) { + this(targetName, "General player report"); + } + + public ReportMenu(String targetName, String attachedMessage) { + super("Report " + targetName, 54); + this.targetName = targetName; + this.attachedMessage = attachedMessage != null ? attachedMessage : "General player report"; + } + + @Override + public void build(@NonNull Player reporter) { + renderTargetHead(); + + for (ReportCategory category : CATEGORIES) { + setItem(category.slot(), createCategoryItem(category.material(), "§c" + category.reason(), category.line1(), category.line2()), + (player, _) -> handleReportSubmit(player, category.reason())); + } + + setCloseButton(49); + } + + private void renderTargetHead() { + ItemStack headItem = new ItemBuilder(Material.PLAYER_HEAD) + .setSkullOwner(Bukkit.getOfflinePlayer(targetName).getUniqueId()) + .setDisplayName("§d" + targetName) + .addLore( + "§8/report " + targetName, "", + " §7This is the player you are ", + " §7reporting to online staff.", + "" + ) + .addItemFlag(ItemFlag.HIDE_PROFILE) + .toItemStack(); + + setItem(4, headItem); + } + + private ItemStack createCategoryItem(Material material, String title, String line1, String line2) { + return new ItemBuilder(material) + .setDisplayName(title) + .addLore(line1, line2, + "", + "§eClick to select." + ) + .addItemFlag(ItemFlag.HIDE_ATTRIBUTES) + .toItemStack(); + } + + private void handleReportSubmit(Player reporter, String categoryReason) { + ReportData report = ReportManager.getInstance().submitReport( + reporter.getName(), + targetName, + categoryReason, + attachedMessage + ); + + inform(reporter, " §a⚑ §7Your report §7has been submitted to online staff."); + + InteractiveMessage notificationMessage = InteractiveMessage.text(" §d§lSTAFF! §f" + reporter.getName() + " §ereported §d" + targetName + " §efor §c" + categoryReason + "§e. ") + .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport " + report.id()) + .append(" ") + .hoverAndExecute("§b[MTP]", "§fClick to teleport to §d" + targetName + "§f.", "/mtp " + targetName) + .append(" ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + targetName + "§f.", "/userinfo " + targetName); + + ModerationManager.getInstance().broadcastToModerators(notificationMessage); + close(); + } +} From 21c2fe2070bcadddd387e9268f5231399e75c787 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:22:40 +0200 Subject: [PATCH 09/30] feat(menu): add direct handled click callbacks to PaginatedMenu exposes onItemLeftClick, onItemRightClick, and onItemShiftClick methods to streamline click event handling --- .../bedrock/ui/menu/types/PaginatedMenu.java | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java index b16f1818..74efb8b0 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java @@ -152,7 +152,16 @@ public void build(@NonNull Player viewer) { slot, getItem(viewer, index), MenuItemInteraction.ANY_CLICK, - (player, click) -> onItemClick(player, index, click) + (player, click) -> { + if (click.isShiftClick()) { + onItemShiftClick(player, index); + } else if (click.isRightClick()) { + onItemRightClick(player, index); + } else if (click.isLeftClick()) { + onItemLeftClick(player, index); + } + onItemClick(player, index, click); + } ); } @@ -173,6 +182,36 @@ public void build(@NonNull Player viewer) { setCloseButton(49); } + /** + * Called when a paginated item is clicked with a left-click. + * + * @param player clicking player + * @param index global item index + */ + protected void onItemLeftClick(@NonNull Player player, int index) { + // override as needed + } + + /** + * Called when a paginated item is clicked with a right-click. + * + * @param player clicking player + * @param index global item index + */ + protected void onItemRightClick(@NonNull Player player, int index) { + // override as needed + } + + /** + * Called when a paginated item is clicked with a shift-click. + * + * @param player clicking player + * @param index global item index + */ + protected void onItemShiftClick(@NonNull Player player, int index) { + // override as needed + } + /** * Called when a paginated item is clicked. * From a28b0a71f08b4c4a827f4bb68ed6f16c49de9fbf Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:22:41 +0200 Subject: [PATCH 10/30] fix(itembuilder): resolve skull owners asynchronously via PlayerProfile stores PlayerProfile directly instead of invoking getOfflinePlayer(String), preventing main-thread blocking I/O on cache misses --- .../fr/moussax/bedrock/utils/ItemBuilder.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/bedrock/src/main/java/fr/moussax/bedrock/utils/ItemBuilder.java b/bedrock/src/main/java/fr/moussax/bedrock/utils/ItemBuilder.java index a5165221..1b5c24f9 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/utils/ItemBuilder.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/utils/ItemBuilder.java @@ -12,6 +12,7 @@ import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.EntitySnapshot; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; import org.bukkit.inventory.*; import org.bukkit.inventory.meta.*; import org.bukkit.inventory.meta.components.*; @@ -54,7 +55,7 @@ public class ItemBuilder { private boolean unstackable; private List bannerPatterns; - private UUID skullOwnerUUID; + private PlayerProfile skullOwnerProfile; private String base64Texture; private Color leatherColor; private Color potionColor; @@ -840,7 +841,23 @@ public ItemBuilder addBannerPatterns(List patterns) { */ public ItemBuilder setSkullOwner(@NonNull UUID playerId) { item.setType(Material.PLAYER_HEAD); - this.skullOwnerUUID = playerId; + this.skullOwnerProfile = Bukkit.createPlayerProfile(playerId); + this.base64Texture = null; + return this; + } + + /** + * Sets the owner of a player head by player name. + * + *

The item type is changed to {@link Material#PLAYER_HEAD}.

+ * + * @param playerName the owner's player name + * @return this builder + */ + public ItemBuilder setSkullOwner(@NonNull String playerName) { + item.setType(Material.PLAYER_HEAD); + Player onlinePlayer = Bukkit.getPlayerExact(playerName); + this.skullOwnerProfile = onlinePlayer != null ? onlinePlayer.getPlayerProfile() : Bukkit.createPlayerProfile(playerName); this.base64Texture = null; return this; } @@ -856,7 +873,7 @@ public ItemBuilder setSkullOwner(@NonNull UUID playerId) { public ItemBuilder setCustomSkullTexture(@NonNull String base64Texture) { item.setType(Material.PLAYER_HEAD); this.base64Texture = base64Texture; - this.skullOwnerUUID = null; + this.skullOwnerProfile = null; return this; } @@ -1004,9 +1021,8 @@ private void applyPotionProperties() { private void applySkullProperties() { if (item.getType() == Material.PLAYER_HEAD && itemMeta instanceof SkullMeta skullMeta) { - if (skullOwnerUUID != null) { - PlayerProfile profile = Bukkit.createPlayerProfile(skullOwnerUUID); - skullMeta.setOwnerProfile(profile); + if (skullOwnerProfile != null) { + skullMeta.setOwnerProfile(skullOwnerProfile); } else if (base64Texture != null) { applyBase64Texture(skullMeta, base64Texture); } From fa092a5b4c56c0bfbd5e2cf820d8141321b5c66f Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:22:42 +0200 Subject: [PATCH 11/30] feat(mod): add formatShortDuration helper to DurationParser provides concise duration formatting for moderation notifications and menu lore --- .../moderator/punishments/DurationParser.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java index 6751844d..666fcdd4 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java @@ -67,4 +67,20 @@ public static String formatDuration(String input) { } return value + " " + unitName; } -} \ No newline at end of file + + public static String formatShortDuration(long seconds) { + if (seconds < 60) { + return seconds + "s"; + } + long minutes = seconds / 60; + if (minutes < 60) { + return minutes + "m"; + } + long hours = minutes / 60; + if (hours < 24) { + return hours + "h"; + } + long days = hours / 24; + return days + "d"; + } +} From 2a47cc92ac4464abbfbbb56cd5c0b8b5b1dcf5b0 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:22:44 +0200 Subject: [PATCH 12/30] refactor(mod): align moderation target notifications with standard format standardizes mute, freeze, and chat moderation messages to match the updated project-wide formatting style --- .../blightedMod/commands/impl/ChatCommand.java | 5 ----- .../commands/impl/FreezeCommand.java | 17 +++++------------ .../blightedMod/commands/impl/MuteCommand.java | 9 ++++++--- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java index 17ddf78d..4ddcec0a 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java @@ -26,11 +26,6 @@ protected boolean executeModeration(Player player, Command command, String label String targetChannel = arguments[0].toLowerCase(); if (targetChannel.equals("staff") || targetChannel.equals("s")) { - if (!getModerationManager().isModerator(player)) { - warn(player, "Only staff members can access the staff chat channel."); - return false; - } - getModerationManager().setChatChannel(player, ModerationManager.ChatChannel.STAFF); inform(player, " §7You are now in the §dSTAFF §7channel."); return true; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java index ab7b0f59..a9ee24af 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java @@ -7,9 +7,6 @@ import static fr.moussax.bedrock.text.Messenger.warn; -/** - * Moderation command to toggle freeze status for a player (/freeze ). - */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class FreezeCommand extends ModerationCommand { @@ -30,21 +27,17 @@ protected boolean executeModeration(Player moderator, Command command, String la return false; } - if (getModerationManager().isModerator(target)) { - warn(moderator, "You cannot freeze another moderator."); - return false; - } - boolean isNowFrozen = getModerationManager().toggleFreeze(target); if (isNowFrozen) { moderator.sendMessage("§b ❄ §eYou froze §d" + target.getName() + "§e."); - target.sendMessage("§b ❄ §c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); - target.sendMessage(" §7Do not log out or you will be permanently banned."); + target.sendMessage(""); + target.sendMessage("§b ❄ §b§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); + target.sendMessage("§c ⚠ Do not log out or you will be permanently banned."); + target.sendMessage(""); } else { moderator.sendMessage("§b ❄ §eYou unfroze §d" + target.getName() + "§e."); - target.sendMessage("§b ❄ §a§lYOU HAVE BEEN UNFROZEN!"); - target.sendMessage(" §7You are now free to move again."); + target.sendMessage("§b ❄ §7You have been unfrozen, you can move again."); } return true; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java index e6d490f1..68bc500b 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java @@ -58,8 +58,11 @@ private boolean handleMute(Player moderator, String[] arguments) { String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e muted §d" + target.getName() + " §e" + durationText + "§efor §c" + reason + "§e."; getModerationManager().broadcastToModerators(notification); - String durationString = expiresAt != null ? "for §6" + DurationParser.formatDuration(arguments[1]) + " §7" : ""; - target.sendMessage(" §c§lMUTED! §7You have been muted " + durationString + "for: §f" + reason); + if (expiresAt != null) { + target.sendMessage(" §c⌚ §cYou are muted for §d" + arguments[1] + " §cfor §b" + reason + "§c."); + } else { + target.sendMessage(" §c⌚ §cYou are muted §cfor §b" + reason + "§c."); + } return true; } @@ -84,7 +87,7 @@ private boolean handleUnmute(Player moderator, String[] arguments) { String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unmuted §d" + target.getName() + "§e."; getModerationManager().broadcastToModerators(notification); - target.sendMessage(" §a§lUNMUTED! §7You are no longer muted."); + target.sendMessage(" §a⚑ §7You are no longer muted."); return true; } } From ffc8d4c9353dd26682056b9df096eaca182b894b Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:22:45 +0200 Subject: [PATCH 13/30] refactor(mod): consolidate teleport commands into single execution pipeline merges teleport and teleport-here handling into executeModeration, eliminating duplicated target validation and interactive message logic --- .../commands/impl/ModTpCommands.java | 48 +++++-------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java index dddbd7f2..0dfdcece 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModTpCommands.java @@ -13,17 +13,10 @@ public final class ModTpCommands extends ModerationCommand { @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { - if (label.equalsIgnoreCase("mtp")) { - return handleTeleport(moderator, arguments); - } else if (label.equalsIgnoreCase("mtphere")) { - return handleTeleportHere(moderator, arguments); - } - return false; - } + boolean isHere = label.equalsIgnoreCase("mtphere"); - private boolean handleTeleport(Player moderator, String[] arguments) { if (arguments.length == 0) { - warn(moderator, "Usage: /mtp "); + warn(moderator, "Usage: /" + (isHere ? "mtphere" : "mtp") + " "); return false; } @@ -33,41 +26,26 @@ private boolean handleTeleport(Player moderator, String[] arguments) { } if (target.equals(moderator)) { - warn(moderator, "You cannot teleport to yourself."); - return false; - } - - moderator.teleport(target.getLocation()); - getModerationManager().getModerator(moderator).setTargetPlayer(target); - - InteractiveMessage.text(" §eTeleported to §d" + target.getName() + "§e. ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + target.getName() + "§f.", "/userinfo " + target.getName()) - .send(moderator); - return true; - } - - private boolean handleTeleportHere(Player moderator, String[] arguments) { - if (arguments.length == 0) { - warn(moderator, "Usage: /mtphere "); + warn(moderator, isHere ? "You cannot teleport yourself to yourself." : "You cannot teleport to yourself."); return false; } - Player target = requireTarget(moderator, arguments[0]); - if (target == null) { - return false; + if (isHere) { + target.teleport(moderator.getLocation()); + } else { + moderator.teleport(target.getLocation()); } - if (target.equals(moderator)) { - warn(moderator, "You cannot teleport yourself to yourself."); - return false; - } - - target.teleport(moderator.getLocation()); getModerationManager().getModerator(moderator).setTargetPlayer(target); - InteractiveMessage.text(" §eTeleported §d" + target.getName() + "§e to you. ") + String textMessage = isHere + ? " §eTeleported §d" + target.getName() + "§e to you. " + : " §eTeleported to §d" + target.getName() + "§e. "; + + InteractiveMessage.text(textMessage) .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + target.getName() + "§f.", "/userinfo " + target.getName()) .send(moderator); + return true; } } From 8e0a6204898012457b0eb8d9fb955eb6b66a6263 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:22:46 +0200 Subject: [PATCH 14/30] refactor(mod): delegate packed ice tool action to freeze command replaces custom freeze logic in click listener with direct command execution to keep freeze logic in a single location --- .../listeners/ModerationListener.java | 59 +++++++------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java index 1dc0d4d4..b766c524 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java @@ -5,6 +5,7 @@ import fr.moussax.blightedMod.moderator.BlightedModerator; import fr.moussax.blightedMod.moderator.ModerationManager; import fr.moussax.blightedMod.moderator.menus.InvSeeMenu; +import fr.moussax.blightedMod.moderator.punishments.DurationParser; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; import org.bukkit.Bukkit; @@ -48,8 +49,13 @@ public void onPlayerChat(AsyncPlayerChatEvent event) { event.setCancelled(true); PunishmentData mute = punishmentManager.getActivePunishment(player.getUniqueId(), PunishmentData.PunishmentType.MUTE); if (mute != null) { - String durationText = mute.isPermanent() ? "permanently" : "temporarily"; - player.sendMessage(" §c§lMUTED! §7You are " + durationText + " muted for: §f" + mute.reason()); + if (mute.isPermanent()) { + player.sendMessage(" §c⌚ §cYou are muted §cfor §b" + mute.reason() + "§c."); + } else { + long remainingSeconds = Math.max(1, (mute.expiresAt() - System.currentTimeMillis()) / 1000L); + String durationText = DurationParser.formatShortDuration(remainingSeconds); + player.sendMessage(" §c⌚ §cYou are muted for §d" + durationText + " §cfor §b" + mute.reason() + "§c."); + } } return; } @@ -64,7 +70,7 @@ public void onPlayerChat(AsyncPlayerChatEvent event) { int remainingSlowmodeSeconds = moderationManager.getRemainingSlowmodeCooldown(player); if (remainingSlowmodeSeconds > 0) { event.setCancelled(true); - player.sendMessage(" §c§lSLOWMODE! §7Please wait §e" + remainingSlowmodeSeconds + "s §7before chatting again."); + player.sendMessage(" §c⌚ §cPlease wait §d" + remainingSlowmodeSeconds + "s §cbefore chatting again."); return; } @@ -207,11 +213,10 @@ public void onServerListPing(ServerListPingEvent event) { Iterator iterator = event.iterator(); while (iterator.hasNext()) { Player player = iterator.next(); - if (moderationManager.isModerator(player)) { - BlightedModerator moderator = moderationManager.getModerator(player); - if (moderator != null && moderator.isVanished()) { - iterator.remove(); - } + if (moderationManager.isModerator(player) + && moderationManager.getModerator(player) instanceof BlightedModerator moderator + && moderator.isVanished()) { + iterator.remove(); } } } @@ -263,13 +268,8 @@ public void onPrivateMessageSpy(PlayerCommandPreprocessEvent event) { } UUID lastTargetId = moderationManager.getLastMessageTarget(sender.getUniqueId()); - String recipientName = "Unknown"; - if (lastTargetId != null) { - Player lastTargetPlayer = Bukkit.getPlayer(lastTargetId); - if (lastTargetPlayer != null) { - recipientName = lastTargetPlayer.getName(); - } - } + Player lastTargetPlayer = lastTargetId != null ? Bukkit.getPlayer(lastTargetId) : null; + String recipientName = lastTargetPlayer != null ? lastTargetPlayer.getName() : "Unknown"; String messageContent = String.join(" ", Arrays.copyOfRange(parts, 1, parts.length)); moderationManager.broadcastSpyMessage(sender.getName(), recipientName, messageContent); @@ -321,13 +321,12 @@ public void onModeratorEntityDamage(EntityDamageByEntityEvent event) { if (!moderationManager.isInModerationMode(attacker)) return; Material tool = attacker.getInventory().getItemInMainHand().getType(); - if (tool == Material.STICK) { - if (event.getEntity() instanceof Player victim) { - moderationManager.getModerator(attacker).setTargetPlayer(victim); - } - } else { - event.setCancelled(true); + if (tool == Material.STICK && event.getEntity() instanceof Player victim) { + moderationManager.getModerator(attacker).setTargetPlayer(victim); + return; } + + event.setCancelled(true); } @EventHandler @@ -345,7 +344,7 @@ private void handleToolInteraction(Player player, Material tool) { case PACKED_ICE -> { Player target = getActiveTarget(player); if (target != null) { - toggleFreeze(player, target); + player.performCommand("freeze " + target.getName()); } } case CHEST -> { @@ -394,22 +393,6 @@ private void openSanctionsMenu(Player moderator, Player target) { moderator.performCommand("sanctions " + target.getName()); } - private void toggleFreeze(Player moderator, Player target) { - moderationManager.getModerator(moderator).setTargetPlayer(target); - boolean isFrozen = moderationManager.toggleFreeze(target); - String actionText = isFrozen ? "froze" : "unfroze"; - - moderator.sendMessage("§b ❄ §eYou " + actionText + " §d" + target.getName() + "§e."); - - if (isFrozen) { - target.sendMessage("§b ❄ §c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); - target.sendMessage(" §7Do not log out or you will be permanently banned."); - } else { - target.sendMessage("§b ❄ §a§lYOU HAVE BEEN UNFROZEN!"); - target.sendMessage(" §7You are now free to move again."); - } - } - private void teleportToRandomPlayer(Player moderator) { List eligiblePlayers = new ArrayList<>(Bukkit.getOnlinePlayers()); eligiblePlayers.remove(moderator); From 27dca969b6c19a7b49c58a08eb68c01fffd0f9b8 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:22:47 +0200 Subject: [PATCH 15/30] refactor(mod): clean report menus and deduplicate submission logic centralizes report notification dispatches, delegates report list teleports to mtp command, and updates menu skull references --- .../commands/impl/CheckReportCommand.java | 16 +-- .../commands/impl/ReportCommand.java | 87 ++---------- .../commands/impl/ReportsCommand.java | 22 ++- .../commands/impl/TestModCommand.java | 102 ++------------ .../listeners/InteractiveChatListener.java | 4 +- .../moderator/menus/InvSeeMenu.java | 10 -- .../moderator/menus/ReportMenu.java | 28 +--- .../moderator/menus/ReportsCenterMenu.java | 127 ++++++++++++------ .../moderator/menus/SanctionsMenu.java | 2 +- .../moderator/reports/ReportManager.java | 20 +++ 10 files changed, 162 insertions(+), 256 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/CheckReportCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/CheckReportCommand.java index 41efd385..8fc558ec 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/CheckReportCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/CheckReportCommand.java @@ -1,7 +1,7 @@ package fr.moussax.blightedMod.commands.impl; -import fr.moussax.bedrock.ui.book.BookMenu; import fr.moussax.blightedMod.commands.ModerationCommand; +import fr.moussax.blightedMod.moderator.menus.ReportsCenterMenu; import fr.moussax.blightedMod.moderator.reports.ReportData; import fr.moussax.blightedMod.moderator.reports.ReportManager; import org.bukkit.command.Command; @@ -32,19 +32,7 @@ protected boolean executeModeration(Player moderator, Command command, String la return true; } - BookMenu bookMenu = BookMenu.builder() - .addPage(page -> page - .append(" §1§lREPORT DETAILS\n") - .append("§0Report ID: §8#" + report.id() + "\n") - .append("§0Target: §c" + report.targetName() + "\n") - .append("§0Reporter: §9" + report.reporterName() + "\n\n") - .append("§0Reason: §8" + report.reason() + "\n\n") - .append("§0Message:\n") - .append("§8\"§7" + report.message() + "§8\"\n\n") - .hoverAndExecute("§c▶ Dismiss Report", "§eClick to dismiss this report", "/reports dismiss " + report.id()) - ); - - bookMenu.open(moderator); + ReportsCenterMenu.openReportBook(moderator, report); return true; } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java index 92584ab9..1fc03cd5 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java @@ -1,22 +1,15 @@ package fr.moussax.blightedMod.commands.impl; import fr.moussax.bedrock.commands.PlayerCommand; -import fr.moussax.bedrock.text.InteractiveMessage; -import fr.moussax.bedrock.ui.book.BookMenu; -import fr.moussax.blightedMod.moderator.ModerationManager; -import fr.moussax.blightedMod.moderator.reports.ReportData; +import fr.moussax.blightedMod.moderator.menus.ReportMenu; import fr.moussax.blightedMod.moderator.reports.ReportManager; import org.bukkit.command.Command; import org.bukkit.entity.Player; import java.util.Arrays; -import static fr.moussax.bedrock.text.Messenger.inform; import static fr.moussax.bedrock.text.Messenger.warn; -/** - * Handles /report command execution for general reports and chat reports with BookMenu interface. - */ public final class ReportCommand extends PlayerCommand { @Override @@ -34,7 +27,12 @@ protected boolean execute(Player player, Command command, String label, String[] ? String.join(" ", Arrays.copyOfRange(arguments, 2, arguments.length)) : "No message content"; - openChatReportBook(player, targetName, chatMessage); + if (player.getName().equalsIgnoreCase(targetName)) { + warn(player, "You cannot report yourself."); + return false; + } + + new ReportMenu(targetName, chatMessage).open(player); return true; } @@ -45,7 +43,7 @@ protected boolean execute(Player player, Command command, String label, String[] ? String.join(" ", Arrays.copyOfRange(arguments, 3, arguments.length)) : "No message content"; - submitChatReport(player, targetName, reason, chatMessage); + submitDirectReport(player, targetName, reason, chatMessage); return true; } @@ -61,76 +59,15 @@ protected boolean execute(Player player, Command command, String label, String[] if (arguments.length > 1) { String reason = String.join(" ", Arrays.copyOfRange(arguments, 1, arguments.length)); - submitGeneralReport(player, target.getName(), reason); + submitDirectReport(player, target.getName(), reason, "General player report"); } else { - openGeneralReportBook(player, target.getName()); + new ReportMenu(target.getName()).open(player); } return true; } - private void openChatReportBook(Player reporter, String targetName, String chatMessage) { - if (reporter.getName().equalsIgnoreCase(targetName)) { - warn(reporter, "You cannot report yourself."); - return; - } - - BookMenu bookMenu = BookMenu.builder() - .addPage(page -> page - .append(" §1§lCHAT REPORT\n") - .append("§0Reporting: §c" + targetName + "\n\n") - .append("§0Message:\n") - .append("§8\"§7" + chatMessage + "§8\"\n\n") - .append("§0Select Reason:\n") - .hoverAndExecute("§c▶ Abuse / Harassment\n", "§eSubmit report for Abuse / Harassment", "/report submit " + targetName + " Abuse/Harassment " + chatMessage) - .hoverAndExecute("§c▶ Spam / Flood\n", "§eSubmit report for Spam / Flood", "/report submit " + targetName + " Spam/Flood " + chatMessage) - .hoverAndExecute("§c▶ Inappropriate Content\n", "§eSubmit report for Inappropriate Content", "/report submit " + targetName + " Inappropriate " + chatMessage) - .hoverAndExecute("§c▶ Advertising / Scam", "§eSubmit report for Advertising / Scam", "/report submit " + targetName + " Advertising/Scam " + chatMessage) - ); - - bookMenu.open(reporter); - } - - private void openGeneralReportBook(Player reporter, String targetName) { - BookMenu bookMenu = BookMenu.builder() - .addPage(page -> page - .append(" §1§lPLAYER REPORT\n") - .append("§0Reporting: §c" + targetName + "\n\n") - .append("§0Select Reason:\n") - .hoverAndExecute("§c▶ Cheating / Hacks\n", "§eSubmit report for Cheating / Hacks", "/report submit " + targetName + " Cheating None") - .hoverAndExecute("§c▶ Toxicity / Abuse\n", "§eSubmit report for Toxicity / Abuse", "/report submit " + targetName + " Toxicity None") - .hoverAndExecute("§c▶ Griefing / Scamming\n", "§eSubmit report for Griefing / Scamming", "/report submit " + targetName + " Griefing None") - .hoverAndExecute("§c▶ Cross-Teaming", "§eSubmit report for Cross-Teaming", "/report submit " + targetName + " Cross-Teaming None") - ); - - bookMenu.open(reporter); - } - - private void submitChatReport(Player reporter, String targetName, String reason, String chatMessage) { - ReportData report = ReportManager.getInstance().submitReport(reporter.getName(), targetName, reason, chatMessage); - - InteractiveMessage notificationMessage = InteractiveMessage.text(" §6§lALERT! §f" + reporter.getName() + " §ereported §d" + targetName + "§e's chat message: §7\"" + chatMessage + "\"§e. ") - .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport " + report.id()) - .append(" ") - .hoverAndExecute("§b[MTP]", "§fClick to teleport to §d" + targetName + "§f.", "/mtp " + targetName) - .append(" ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + targetName + "§f.", "/userinfo " + targetName); - - ModerationManager.getInstance().broadcastToModerators(notificationMessage); - inform(reporter, " §a§lREPORT SENT! §7Your report for §d" + targetName + " §7has been submitted to online staff."); - } - - private void submitGeneralReport(Player reporter, String targetName, String reason) { - ReportData report = ReportManager.getInstance().submitReport(reporter.getName(), targetName, reason, "General player report"); - - InteractiveMessage notificationMessage = InteractiveMessage.text(" §6§lALERT! §d" + targetName + " §ewas reported. ") - .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport " + report.id()) - .append(" ") - .hoverAndExecute("§b[MTP]", "§fClick to teleport to §d" + targetName + "§f.", "/mtp " + targetName) - .append(" ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + targetName + "§f.", "/userinfo " + targetName); - - ModerationManager.getInstance().broadcastToModerators(notificationMessage); - inform(reporter, " §a§lREPORT SENT! §7Your report for §d" + targetName + " §7has been submitted to online staff."); + private void submitDirectReport(Player reporter, String targetName, String reason, String chatMessage) { + ReportManager.getInstance().submitAndNotify(reporter, targetName, reason, chatMessage); } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportsCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportsCommand.java index d25d9f86..5fb39b07 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportsCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportsCommand.java @@ -2,16 +2,32 @@ import fr.moussax.blightedMod.commands.ModerationCommand; import fr.moussax.blightedMod.moderator.menus.ReportsCenterMenu; +import fr.moussax.blightedMod.moderator.reports.ReportManager; import org.bukkit.command.Command; import org.bukkit.entity.Player; -/** - * Moderation command to view active player reports via a paginated menu (/reports). - */ +import static fr.moussax.bedrock.text.Messenger.inform; +import static fr.moussax.bedrock.text.Messenger.warn; + public final class ReportsCommand extends ModerationCommand { @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + if (arguments.length >= 2 && arguments[0].equalsIgnoreCase("dismiss")) { + try { + int reportId = Integer.parseInt(arguments[1]); + boolean dismissed = ReportManager.getInstance().dismissReport(reportId); + if (dismissed) { + inform(moderator, "§eDismissed report §d#" + reportId +"§e."); + } else { + warn(moderator, "Report #" + reportId + " not found or already dismissed."); + } + } catch (NumberFormatException _) { + warn(moderator, "Invalid report ID."); + } + return true; + } + new ReportsCenterMenu().open(moderator); return true; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java index 83c104d4..492a9b77 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java @@ -1,6 +1,5 @@ package fr.moussax.blightedMod.commands.impl; -import fr.moussax.bedrock.text.InteractiveMessage; import fr.moussax.blightedMod.commands.ModerationCommand; import org.bukkit.command.Command; import org.bukkit.entity.Player; @@ -13,99 +12,24 @@ public final class TestModCommand extends ModerationCommand { protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { String category = arguments.length > 0 ? arguments[0].toLowerCase() : "all"; - inform(moderator, "§e=== [BlightedMod Message Formatting Test] ==="); - - if (category.equals("all") || category.equals("mode")) { - inform(moderator, "§8--- Moderation Mode & Vanish ---"); - inform(moderator, " §dModeration Mode §etoggled §aON§e."); - inform(moderator, " §dModeration Mode §etoggled §cOFF§e."); - inform(moderator, " §dVanish §etoggled §aON§e."); - inform(moderator, " §dVanish §etoggled §cOFF§e."); - } - - if (category.equals("all") || category.equals("chat")) { - inform(moderator, "§8--- Staff Chat & SocialSpy ---"); - inform(moderator, " §d§lSTAFF! §9Steve§f§l » §bHello staff team!"); - inform(moderator, " §6§lSPY! §fSteve §d➟ §fAlex §7§l» §dHey check out this secret base!"); - inform(moderator, " §6Social Spy §etoggled §aON§e."); - inform(moderator, " §6Social Spy §etoggled §cOFF§e."); - } - - if (category.equals("all") || category.equals("tp")) { - inform(moderator, "§8--- Teleportation & Targeting ---"); - InteractiveMessage.text(" §eTeleported to §dAlex§e. ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") - .send(moderator); - - InteractiveMessage.text(" §eTeleported §dAlex§e to you. ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") - .send(moderator); - - InteractiveMessage.text(" §eRandomly teleported to §dAlex§e. ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") - .send(moderator); - - InteractiveMessage.text(" §eTargeting §dAlex §ewith §fModeration HUD§e. ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") - .send(moderator); - - inform(moderator, " §eTarget cleared from §fModeration HUD§e."); - inform(moderator, " §d§lSTAFF! §9Steve§e teleported to §dAlex§e."); - } - - if (category.equals("all") || category.equals("reports")) { - inform(moderator, "§8--- Reports & Alerts ---"); - InteractiveMessage.text(" §6§lALERT! §dAlex §ewas reported. ") - .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport ") - .append(" ") - .hoverAndExecute("§b[MTP]", "§fClick to teleport to §dAlex§f.", "/mtp Alex") - .append(" ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") - .send(moderator); - - InteractiveMessage.text(" §6§lALERT! §fSteve §ereported §dAlex&e's chat message: §7\"J'vous baise vous grand mort\"§e. ") - .hoverAndExecute("§b[MTP]", "§fClick to teleport to §dAlex§f.", "/mtp Alex") - .append(" ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §dAlex§f.", "/userinfo Alex") - .send(moderator); - - inform(moderator, " §6§lALERT! §dAlex§e was automatically banned for §fdisconnecting §ewhile being frozen by a moderator."); - } - - if (category.equals("all") || category.equals("sanctions")) { - inform(moderator, "§8--- Punishments & Sanctions ---"); - inform(moderator, " §d§lSTAFF! §9Steve§e banned §9Alex§e for §61d§e for §cFly hacking§e."); - inform(moderator, " §d§lSTAFF! §9Steve§e unbanned §9Alex§e."); - inform(moderator, " §d§lSTAFF! §9Steve§e IP banned §9Alex§e for §cAlting§e."); - inform(moderator, " §d§lSTAFF! §9Steve§e unbanned IP for §dAlex§e."); - inform(moderator, " §d§lSTAFF! §9Steve§e muted §dAlex §efor §61h §efor §cSpamming§e."); - inform(moderator, " §d§lSTAFF! §9Steve§e unmuted §dAlex§e."); - inform(moderator, " §d§lSTAFF! §9Steve§e kicked §dAlex§e for §cGriefin§e."); - inform(moderator, "§b ❄ §eYou froze §dAlex§e."); - inform(moderator, "§b ❄ §eYou unfroze §dAlex§e."); - } - - if (category.equals("all") || category.equals("settings")) { - inform(moderator, "§8--- Chat Settings & User Info ---"); - inform(moderator, " §d§lSTAFF! §9Steve §eset §fChat Slowmode §eto §610s§e."); - inform(moderator, " §d§lSTAFF! §9Steve §ehas disabled §fChat Slowmode§e."); - inform(moderator, " §7You are now in the §dSTAFF §7channel."); - } + inform(moderator, " "); + inform(moderator, " "); if (category.equals("all") || category.equals("target") || category.equals("user") || category.equals("player")) { inform(moderator, "§8--- Target / Player Facing Messages ---"); - inform(moderator, " §c§lMUTED! §7You have been muted for §61h §7for: §fSpamming"); - inform(moderator, " §c§lMUTED! §7You are temporarily muted for: §fSpamming"); - inform(moderator, " §a§lUNMUTED! §7You are no longer muted."); - inform(moderator, "§b ❄ §c§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); - inform(moderator, " §7Do not log out or you will be permanently banned."); - inform(moderator, "§b ❄ §a§lYOU HAVE BEEN UNFROZEN!"); - inform(moderator, " §7You are now free to move again."); - inform(moderator, " §c§lSLOWMODE! §7Please wait §e5s §7before chatting again."); - inform(moderator, " §a§lREPORT SENT! §7Your report for §dAlex §7has been submitted to online staff."); + inform(moderator, " §c⌚ §cYou are muted for §d1h §cfor §bAdvertising Social Media§c."); + inform(moderator, " §c⌚ §cYou are muted for §cfor §bAdvertising Social Media§c."); + inform(moderator, " §a⚑ §7You are no longer muted."); + inform(moderator, " "); + inform(moderator, "§b ❄ §b§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); + inform(moderator, "§c ⚠ Do not log out or you will be permanently banned."); + inform(moderator, " "); + inform(moderator, "§b ❄ §7You have been unfrozen, you can move again."); + inform(moderator, " §c⌚ §cPlease wait §d5s §cbefore chatting again."); } - inform(moderator, "§e=== [End of Formatting Test] ==="); + inform(moderator, " "); + inform(moderator, " "); return true; } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java index d9bc54f3..722e38df 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java @@ -17,10 +17,10 @@ public void onNormalPlayerChat(AsyncPlayerChatEvent event) { String chatMessage = event.getMessage(); InteractiveMessage interactiveMessage = InteractiveMessage.text(" ") - .hoverAndExecute("§c⚑ ", "§cClick to report §f" + sender.getName(), + .hoverAndExecute("§c⚑ ", "§cClick to report §d" + sender.getName() + "§c.", "/report chat " + sender.getName() + " " + chatMessage ) - .append("§7" + sender.getName() + " §f§l» §7" + chatMessage); + .append("§7" + sender.getName() + " §8§l» §f" + chatMessage); for (Player recipient : event.getRecipients()) { interactiveMessage.send(recipient); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java index 5fa022cc..88d8d45a 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java @@ -64,9 +64,6 @@ private void updateContents() { " §7Health: §f" + (int) target.getHealth() + "§c❤", " §7Food: §f" + target.getFoodLevel() + "§c\uD83C\uDF56", " §7Gamemode: §f" + target.getGameMode().name() + " ", - "", - " §7Ping: §d" + target.getPing() + "ms", - " §7IP Address: §d" + formatAddress(target.getAddress()) + " ", "" ) .setSkullOwner(target.getUniqueId()) @@ -95,13 +92,6 @@ private void updateSlot(int menuSlot, ItemStack item, ItemStack placeholder) { } } - private String formatAddress(InetSocketAddress socketAddress) { - if (socketAddress == null || socketAddress.getAddress() == null) { - return "Unknown"; - } - return socketAddress.getAddress().getHostAddress(); - } - private boolean isEmpty(ItemStack item) { return item == null || item.getType() == Material.AIR; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java index e24363a1..7032fea0 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java @@ -30,12 +30,14 @@ private record ReportCategory(int slot, Material material, String reason, String "§7Destroying claims, stealing items,", "§7or damaging another player's property."), new ReportCategory(24, Material.COMPASS, "Cross-Teaming / Exploits", "§7Teaming across alliances or abusing", "§7server bugs and game exploits."), - new ReportCategory(30, Material.SCAFFOLDING, "Inappropriate Build", + new ReportCategory(29, Material.SCAFFOLDING, "Inappropriate Build", "§7Building offensive structures, hate", "§7symbols, or rule-breaking builds."), - new ReportCategory(31, Material.CLOCK, "Spam / Chat Flooding", + new ReportCategory(30, Material.CLOCK, "Spam / Chat Flooding", "§7Flooding chat with repeated messages", "§7or excessive unwanted content."), - new ReportCategory(32, Material.WITHER_ROSE, "Harassment / Toxicity", + new ReportCategory(31, Material.WITHER_ROSE, "Harassment / Toxicity", "§7Targeted harassment, hate speech,", "§7or persistently toxic behavior."), + new ReportCategory(32, Material.PAPER, "Other", + "§7Any rule-breaking behavior not", "§7covered by the categories above."), }; private final String targetName; @@ -65,7 +67,7 @@ public void build(@NonNull Player reporter) { private void renderTargetHead() { ItemStack headItem = new ItemBuilder(Material.PLAYER_HEAD) - .setSkullOwner(Bukkit.getOfflinePlayer(targetName).getUniqueId()) + .setSkullOwner(targetName) .setDisplayName("§d" + targetName) .addLore( "§8/report " + targetName, "", @@ -91,23 +93,7 @@ private ItemStack createCategoryItem(Material material, String title, String lin } private void handleReportSubmit(Player reporter, String categoryReason) { - ReportData report = ReportManager.getInstance().submitReport( - reporter.getName(), - targetName, - categoryReason, - attachedMessage - ); - - inform(reporter, " §a⚑ §7Your report §7has been submitted to online staff."); - - InteractiveMessage notificationMessage = InteractiveMessage.text(" §d§lSTAFF! §f" + reporter.getName() + " §ereported §d" + targetName + " §efor §c" + categoryReason + "§e. ") - .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport " + report.id()) - .append(" ") - .hoverAndExecute("§b[MTP]", "§fClick to teleport to §d" + targetName + "§f.", "/mtp " + targetName) - .append(" ") - .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + targetName + "§f.", "/userinfo " + targetName); - - ModerationManager.getInstance().broadcastToModerators(notificationMessage); + ReportManager.getInstance().submitAndNotify(reporter, targetName, categoryReason, attachedMessage); close(); } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java index 1919136a..422c185a 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java @@ -1,27 +1,31 @@ package fr.moussax.blightedMod.moderator.menus; +import fr.moussax.bedrock.ui.book.BookMenu; import fr.moussax.bedrock.ui.menu.TickableMenu; import fr.moussax.bedrock.ui.menu.types.PaginatedMenu; import fr.moussax.bedrock.utils.ItemBuilder; -import fr.moussax.blightedMod.moderator.ModerationManager; import fr.moussax.blightedMod.moderator.reports.ReportData; import fr.moussax.blightedMod.moderator.reports.ReportManager; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; -import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.jspecify.annotations.NonNull; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; +import java.util.Locale; import static fr.moussax.bedrock.text.Messenger.inform; -import static fr.moussax.bedrock.text.Messenger.warn; public final class ReportsCenterMenu extends PaginatedMenu implements TickableMenu { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMM dd, yyyy 'at' hh:mm a", Locale.US); + public ReportsCenterMenu() { - super("Active Reports", 54); + super("Reports Center", 54); } @Override @@ -43,7 +47,7 @@ protected int[] getDisplaySlots() { protected ItemStack getEmptyStateItem(@NonNull Player player) { return new ItemBuilder(Material.RED_STAINED_GLASS_PANE) .setDisplayName("§cNo Active Reports") - .addLore("§7There are currently no active reports.") + .addLore("§7There are currently no", "§7active reports.") .toItemStack(); } @@ -57,53 +61,94 @@ protected ItemStack getItem(@NonNull Player player, int index) { ReportData report = reports.get(index); long elapsedSeconds = (System.currentTimeMillis() - report.timestamp()) / 1000L; String timeAgo = elapsedSeconds < 60 ? elapsedSeconds + "s ago" : (elapsedSeconds / 60) + "m ago"; - - ItemBuilder builder = new ItemBuilder(Material.PAPER) - .setDisplayName("§eReport: §b" + report.targetName()) - .addLore("§7Report ID: §f#" + report.id()) - .addLore("§7Target: §b" + report.targetName()) - .addLore("§7Reporter: §e" + report.reporterName()) - .addLore("§7Reason: §c" + report.reason()); + String dateText = DATE_FORMAT.format(new Date(report.timestamp())); + + ItemBuilder builder = new ItemBuilder(Material.PLAYER_HEAD) + .setSkullOwner(report.targetName()) + .setDisplayName("§b" + report.targetName() + " §7(#" + report.id() + ")") + .addLore( + "", + " §7Status: §a§lACTIVE", + " §7Reporter: §e" + report.reporterName(), + " §7Reason: §c" + report.reason(), + " §7Submitted: §f" + dateText + " §7(" + timeAgo + ")" + ); if (report.message() != null && !report.message().isBlank()) { - builder.addLore("§7Message: §f\"" + report.message() + "\""); + builder.addLore(" §7Message: §f\"" + report.message() + "\""); } - builder.addLore("§7Submitted: §e" + timeAgo) - .addLore("") - .addLore("§eLeft-Click §7to teleport & target") - .addLore("§cRight-Click §7to dismiss"); + builder.addItemFlag(ItemFlag.HIDE_PROFILE); + builder.addLore( + "", + "§dLeft-click to teleport to target!", + "§bShift-Left-Click to view report details!", + "§cRight-click to dismiss report!" + ); return builder.toItemStack(); } @Override - protected void onItemClick(@NonNull Player moderator, int index, @NonNull ClickType clickType) { - List reports = ReportManager.getInstance().getActiveReports(); - if (index >= reports.size()) { - return; - } + public void build(@NonNull Player viewer) { + super.build(viewer); + renderHeader(); + } - ReportData report = reports.get(index); + private void renderHeader() { + int totalReports = ReportManager.getInstance().getActiveReports().size(); + ItemStack headerItem = new ItemBuilder(Material.WRITABLE_BOOK) + .setDisplayName("§eReports Center") + .addLore( + "", + " §7View and manage every report ", + " §7submitted by players, then act", + " §7on them from this menu.", + "", + " §7Active Reports: §d" + totalReports, + "" + ) + .toItemStack(); + setItem(4, headerItem); + } - if (clickType.isRightClick()) { - ReportManager.getInstance().dismissReport(report.id()); - inform(moderator, "§aDismissed report #" + report.id() + " against §f" + report.targetName()); - refresh(moderator); - return; - } + @Override + protected void onItemLeftClick(@NonNull Player moderator, int index) { + ReportData report = ReportManager.getInstance().getActiveReports().get(index); + close(); + moderator.performCommand("mtp " + report.targetName()); + } - if (clickType.isLeftClick()) { - Player target = Bukkit.getPlayerExact(report.targetName()); - if (target == null) { - warn(moderator, "Target player §4" + report.targetName() + " §cis no longer online."); - return; - } - - moderator.teleport(target.getLocation()); - ModerationManager.getInstance().getModerator(moderator).setTargetPlayer(target); - inform(moderator, "§aTeleported to §f" + target.getName() + " §aand set as HUD target."); - close(); - } + @Override + protected void onItemRightClick(@NonNull Player moderator, int index) { + ReportData report = ReportManager.getInstance().getActiveReports().get(index); + ReportManager.getInstance().dismissReport(report.id()); + inform(moderator, " §eYou dismissed §creport #" + report.id() + " §eagainst §d" + report.targetName() + "§e."); + refresh(moderator); + } + + @Override + protected void onItemShiftClick(@NonNull Player moderator, int index) { + ReportData report = ReportManager.getInstance().getActiveReports().get(index); + openReportBook(moderator, report); + } + + public static void openReportBook(Player moderator, ReportData report) { + String dateText = DATE_FORMAT.format(new Date(report.timestamp())); + BookMenu bookMenu = BookMenu.builder() + .addPage(page -> { + page.append(" §0§lREPORT DETAILS\n\n"); + page.append("§0" + report.reporterName() + " §0reported §6" + report.targetName() + " §0for §c" + report.reason() + "§0.\n\n"); + + if (report.message() != null && !report.message().isBlank() && !report.message().equalsIgnoreCase("General player report") && !report.message().equalsIgnoreCase("None")) { + page.append("§0Chat Message:\n"); + page.append("§8\"§3" + report.message() + "§8\"\n\n"); + } + + page.append("§0Submitted: §8" + dateText + "\n\n"); + page.hoverAndExecute("§4▶ Dismiss Report", "§eClick to dismiss this report", "/reports dismiss " + report.id()); + }); + + bookMenu.open(moderator); } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java index 8d5758bb..8bacca0e 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java @@ -198,7 +198,7 @@ private void renderTargetHead() { long totalKicks = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.KICK).count(); ItemStack headItem = new ItemBuilder(Material.PLAYER_HEAD) - .setSkullOwner(Bukkit.getOfflinePlayer(targetName).getUniqueId()) + .setSkullOwner(targetName) .setDisplayName("§d" + targetName) .addLore( "", diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java index 0495e106..a4148e05 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/reports/ReportManager.java @@ -1,10 +1,16 @@ package fr.moussax.blightedMod.moderator.reports; +import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.blightedMod.moderator.ModerationManager; +import org.bukkit.entity.Player; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import static fr.moussax.bedrock.text.Messenger.inform; + public final class ReportManager { private static final ReportManager INSTANCE = new ReportManager(); @@ -31,6 +37,20 @@ public ReportData submitReport(String reporterName, String targetName, String re return report; } + public void submitAndNotify(Player reporter, String targetName, String reason, String message) { + ReportData report = submitReport(reporter.getName(), targetName, reason, message); + inform(reporter, " §a⚑ §7Your report §7has been submitted to online staff."); + + InteractiveMessage notificationMessage = InteractiveMessage.text(" §d§lSTAFF! §f" + reporter.getName() + " §ereported §d" + targetName + " §efor §c" + reason + "§e. ") + .hoverAndExecute("§6[DETAILS]", "§fClick to view §dreport §fdetails.", "/checkreport " + report.id()) + .append(" ") + .hoverAndExecute("§b[MTP]", "§fClick to teleport to §d" + targetName + "§f.", "/mtp " + targetName) + .append(" ") + .hoverAndExecute("§3[INFO]", "§fClick to view information about §d" + targetName + "§f.", "/userinfo " + targetName); + + ModerationManager.getInstance().broadcastToModerators(notificationMessage); + } + public List getActiveReports() { synchronized (reports) { return new ArrayList<>(reports); From bca86c6cd9275e9cda38bb3f89935822a5b4c601 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:27:50 +0200 Subject: [PATCH 16/30] refactor: code cleanup --- bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java | 1 - .../fr/moussax/blightedMod/commands/impl/BanCommand.java | 3 --- .../fr/moussax/blightedMod/commands/impl/ChatCommand.java | 3 --- .../fr/moussax/blightedMod/commands/impl/ModCommand.java | 3 --- .../moussax/blightedMod/commands/impl/SanctionsCommand.java | 3 --- .../blightedMod/commands/impl/SetSlowmodeCommand.java | 1 - .../fr/moussax/blightedMod/commands/impl/TargetCommand.java | 3 --- .../fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java | 1 - .../fr/moussax/blightedMod/moderator/menus/ReportMenu.java | 6 ------ .../blightedMod/moderator/menus/ReportsCenterMenu.java | 1 - .../moussax/blightedMod/moderator/menus/SanctionsMenu.java | 1 - .../engine/items/registry/menu/ItemRegistryMenu.java | 1 - 12 files changed, 27 deletions(-) diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java index 351a88b9..d2e52bf8 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java @@ -7,7 +7,6 @@ import fr.moussax.bedrock.ui.menu.types.InteractiveMenu; import fr.moussax.bedrock.utils.ItemBuilder; import lombok.Getter; -import lombok.Setter; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java index 5fd71c8c..a2401ae6 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java @@ -13,9 +13,6 @@ import static fr.moussax.bedrock.text.Messenger.warn; -/** - * Moderation command to ban (/ban, /banip) and unban (/unban, /unbanip) players. - */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class BanCommand extends ModerationCommand { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java index 4ddcec0a..c2625d58 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ChatCommand.java @@ -9,9 +9,6 @@ import static fr.moussax.bedrock.text.Messenger.inform; import static fr.moussax.bedrock.text.Messenger.warn; -/** - * Command to switch active chat channel (/chat staff, /chat all). - */ @CommandArgument(position = 0, suggestions = {"$channels"}) public final class ChatCommand extends ModerationCommand { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModCommand.java index 664c84d0..efea66e9 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ModCommand.java @@ -5,9 +5,6 @@ import org.bukkit.command.Command; import org.bukkit.entity.Player; -/** - * Moderation command to toggle moderation mode (/mod) or broadcast a staff announcement (/mod ). - */ public final class ModCommand extends ModerationCommand { @Override diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java index 36754593..427e1708 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SanctionsCommand.java @@ -11,9 +11,6 @@ import static fr.moussax.bedrock.text.Messenger.warn; -/** - * Moderation command to view all sanctions for a player (/sanctions ). - */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class SanctionsCommand extends ModerationCommand { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java index b4124ed6..6d9dc4b5 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java @@ -2,7 +2,6 @@ import fr.moussax.blightedMod.commands.ModerationCommand; import fr.moussax.blightedMod.moderator.punishments.DurationParser; -import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.entity.Player; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java index 858069c3..46b4c12f 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TargetCommand.java @@ -10,9 +10,6 @@ import static fr.moussax.bedrock.text.Messenger.inform; import static fr.moussax.bedrock.text.Messenger.warn; -/** - * Moderation command to set or clear the targeted player for the moderation actionbar HUD (/target ). - */ @CommandArgument(position = 0, suggestions = {"$players"}) public final class TargetCommand extends ModerationCommand { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java index 88d8d45a..7ef34495 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java @@ -10,7 +10,6 @@ import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; -import java.net.InetSocketAddress; import java.util.Objects; public final class InvSeeMenu extends Menu implements TickableMenu { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java index 7032fea0..88dfd63f 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportMenu.java @@ -1,20 +1,14 @@ package fr.moussax.blightedMod.moderator.menus; -import fr.moussax.bedrock.text.InteractiveMessage; import fr.moussax.bedrock.ui.menu.Menu; import fr.moussax.bedrock.utils.ItemBuilder; -import fr.moussax.blightedMod.moderator.ModerationManager; -import fr.moussax.blightedMod.moderator.reports.ReportData; import fr.moussax.blightedMod.moderator.reports.ReportManager; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.jspecify.annotations.NonNull; -import static fr.moussax.bedrock.text.Messenger.inform; - public final class ReportMenu extends Menu { private record ReportCategory(int slot, Material material, String reason, String line1, String line2) {} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java index 422c185a..bba334a4 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java @@ -6,7 +6,6 @@ import fr.moussax.bedrock.utils.ItemBuilder; import fr.moussax.blightedMod.moderator.reports.ReportData; import fr.moussax.blightedMod.moderator.reports.ReportManager; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemFlag; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java index 8bacca0e..5b38373c 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java @@ -5,7 +5,6 @@ import fr.moussax.bedrock.utils.ItemBuilder; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; import lombok.Getter; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemFlag; diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/registry/menu/ItemRegistryMenu.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/registry/menu/ItemRegistryMenu.java index 0844d9c0..10a810a0 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/registry/menu/ItemRegistryMenu.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/items/registry/menu/ItemRegistryMenu.java @@ -6,7 +6,6 @@ import fr.moussax.bedrock.text.Formatter; import fr.moussax.bedrock.ui.menu.Menu; import fr.moussax.bedrock.ui.menu.types.PaginatedMenu; -import fr.moussax.bedrock.ui.menu.interaction.MenuElementPreset; import fr.moussax.bedrock.ui.menu.interaction.MenuItemInteraction; import fr.moussax.bedrock.ui.sign.SignInputMenu; import fr.moussax.bedrock.utils.ItemBuilder; From 6b84582b0346e3dd0cf75abca9d2158dce1fb90e Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:28:04 +0200 Subject: [PATCH 17/30] feat(hud): add dynamic health color indicator --- .../fr/moussax/blightedMod/moderator/hud/ModerationHud.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java index 58151498..ef6607b7 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/hud/ModerationHud.java @@ -36,6 +36,7 @@ private static String render(ModerationManager moderationManager, Player player) double maxHealth = Objects.requireNonNull(target.getAttribute(Attribute.MAX_HEALTH)).getValue(); int healthPercent = (int) Math.round((target.getHealth() / maxHealth) * 100.0); + String healthColor = healthPercent >= 60 ? "§a" : healthPercent >= 30 ? "§e" : "§c"; String distanceText = player.getWorld().equals(target.getWorld()) ? String.format(Locale.ROOT, "%.2f blocks", player.getLocation().distance(target.getLocation())) @@ -47,7 +48,7 @@ private static String render(ModerationManager moderationManager, Player player) String pingText = pingColor + ping + "ms"; return "§fTarget: §d" + target.getName() - + " §fHP: §d" + healthPercent + "%" + + " §fHP: " + healthColor + healthPercent + "%" + " §fDistance: §d" + distanceText + " §fPing: " + pingText + " §fFrozen: " + frozenText; From 3abacbbcc150550e12d0dbb7d86ee1c202038c7e Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:42:59 +0200 Subject: [PATCH 18/30] fix(menu): preserve text formatting and space tokens during book pagination converts legacy color codes via TextComponent.fromLegacy to retain explicit styling across line and page breaks, preserves leading spaces for title centering, and prevents recursive leaf-component collection stack overflows --- .../fr/moussax/bedrock/ui/book/BookMenu.java | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java index 4299291e..bae39c0f 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java @@ -103,16 +103,13 @@ private List buildPaginatedPages() { } private List splitSinglePageComponents(BaseComponent[] pageComponents) { - List flattenedComponents = new ArrayList<>(); - for (BaseComponent baseComponent : pageComponents) { - flattenComponent(baseComponent, flattenedComponents); - } + List leafComponents = flattenToLeafComponents(pageComponents); List> lines = new ArrayList<>(); List currentLine = new ArrayList<>(); int currentLineLength = 0; - for (TextComponent component : flattenedComponents) { + for (TextComponent component : leafComponents) { String componentText = component.getText(); if (componentText == null || componentText.isEmpty()) { continue; @@ -134,7 +131,17 @@ private List splitSinglePageComponents(BaseComponent[] pageComp String[] words = paragraphText.split(" ", -1); for (int wordIndex = 0; wordIndex < words.length; wordIndex++) { String word = words[wordIndex]; - if (wordIndex > 0 && !currentLine.isEmpty() && words.length > 1) { + + if (word.isEmpty()) { + if (!currentLine.isEmpty() || wordIndex < words.length - 1) { + TextComponent spaceComponent = duplicateComponentWithText(component, " "); + currentLine.add(spaceComponent); + currentLineLength += 1; + } + continue; + } + + if (wordIndex > 0 && !currentLine.isEmpty() && !words[wordIndex - 1].isEmpty()) { word = " " + word; } @@ -142,22 +149,16 @@ private List splitSinglePageComponents(BaseComponent[] pageComp String chunk = word.substring(0, MAXIMUM_CHARACTERS_PER_LINE); word = word.substring(MAXIMUM_CHARACTERS_PER_LINE); - TextComponent chunkComponent = duplicateComponentWithText(component, chunk); if (currentLineLength + chunk.length() > MAXIMUM_CHARACTERS_PER_LINE && !currentLine.isEmpty()) { lines.add(currentLine); currentLine = new ArrayList<>(); - currentLineLength = 0; } - currentLine.add(chunkComponent); + currentLine.add(duplicateComponentWithText(component, chunk)); lines.add(currentLine); currentLine = new ArrayList<>(); currentLineLength = 0; } - if (word.isEmpty()) { - continue; - } - if (currentLineLength + word.length() > MAXIMUM_CHARACTERS_PER_LINE && !currentLine.isEmpty()) { lines.add(currentLine); currentLine = new ArrayList<>(); @@ -202,13 +203,60 @@ private List splitSinglePageComponents(BaseComponent[] pageComp return resultPages; } - private void flattenComponent(BaseComponent component, List targetList) { + private List flattenToLeafComponents(BaseComponent[] components) { + List leafComponents = new ArrayList<>(); + for (BaseComponent component : components) { + collectLeafComponents(component, leafComponents); + } + return leafComponents; + } + + private void collectLeafComponents(BaseComponent component, List targetList) { + if (component instanceof TextComponent textComponent) { + processTextComponent(textComponent, targetList); + } + + if (component.getExtra() == null) { + return; + } + + for (BaseComponent extraComponent : component.getExtra()) { + if (extraComponent.getHoverEvent() == null) { + extraComponent.setHoverEvent(component.getHoverEvent()); + } + if (extraComponent.getClickEvent() == null) { + extraComponent.setClickEvent(component.getClickEvent()); + } + collectLeafComponents(extraComponent, targetList); + } + } + + private void processTextComponent(TextComponent textComponent, List targetList) { + String text = textComponent.getText(); + if (text == null || text.isEmpty()) { + return; + } + + BaseComponent legacyComponent = TextComponent.fromLegacy(text); + addParsedLeafComponents(legacyComponent, textComponent, targetList); + } + + private void addParsedLeafComponents(BaseComponent component, TextComponent parent, List targetList) { if (component instanceof TextComponent textComponent) { - targetList.add(textComponent); + if (textComponent.getHoverEvent() == null) { + textComponent.setHoverEvent(parent.getHoverEvent()); + } + if (textComponent.getClickEvent() == null) { + textComponent.setClickEvent(parent.getClickEvent()); + } + if (textComponent.getText() != null && !textComponent.getText().isEmpty()) { + targetList.add(textComponent); + } } + if (component.getExtra() != null) { - for (BaseComponent extraComponent : component.getExtra()) { - flattenComponent(extraComponent, targetList); + for (BaseComponent extra : component.getExtra()) { + addParsedLeafComponents(extra, parent, targetList); } } } From 0b3bef05035dac38ddacbfd9a7ef95f9ffbf4caa Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 11:43:01 +0200 Subject: [PATCH 19/30] refactor(mod): trim long report messages in menu lore with ellipsis truncates inventory lore report messages longer than 40 characters to fit GUI dimensions while full text remains viewable in the book menu --- .../blightedMod/moderator/menus/ReportsCenterMenu.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java index bba334a4..3688d82a 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java @@ -74,7 +74,11 @@ protected ItemStack getItem(@NonNull Player player, int index) { ); if (report.message() != null && !report.message().isBlank()) { - builder.addLore(" §7Message: §f\"" + report.message() + "\""); + String displayMessage = report.message(); + if (displayMessage.length() > 40) { + displayMessage = displayMessage.substring(0, 37) + "..."; + } + builder.addLore(" §7Message: §f\"" + displayMessage + "\""); } builder.addItemFlag(ItemFlag.HIDE_PROFILE); @@ -140,7 +144,7 @@ public static void openReportBook(Player moderator, ReportData report) { page.append("§0" + report.reporterName() + " §0reported §6" + report.targetName() + " §0for §c" + report.reason() + "§0.\n\n"); if (report.message() != null && !report.message().isBlank() && !report.message().equalsIgnoreCase("General player report") && !report.message().equalsIgnoreCase("None")) { - page.append("§0Chat Message:\n"); + page.append("§0Message:\n"); page.append("§8\"§3" + report.message() + "§8\"\n\n"); } From 275c4f452aad55f8f3da423098f18fecf11979a7 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 12:26:54 +0200 Subject: [PATCH 20/30] refactor(mod): standardize userinfo, invsee, and gems command output - format /gems help with syntax highlighting and single-line usage warnings - add offline player support, status indicator, playtime, and first connection to /userinfo - update /invsee head lore with dimension name, coordinates, xp, and MODERATOR status --- .../commands/impl/UserinfoCommand.java | 74 +++++++++++++------ .../moderator/BlightedModerator.java | 1 - .../moderator/ModerationTools.java | 7 -- .../moderator/menus/InvSeeMenu.java | 37 +++++++++- .../commands/impl/GemsCommand.java | 48 +++++++++--- 5 files changed, 125 insertions(+), 42 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java index 07e0f381..6f3b66f1 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/UserinfoCommand.java @@ -4,20 +4,25 @@ import fr.moussax.blightedMod.commands.ModerationCommand; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; -import org.bukkit.Location; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; import org.bukkit.command.Command; import org.bukkit.entity.Player; -import org.bukkit.attribute.Attribute; -import java.util.Objects; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Locale; -import static fr.moussax.bedrock.text.Messenger.inform; import static fr.moussax.bedrock.text.Messenger.warn; @CommandArgument(position = 0, suggestions = {"$players"}) public final class UserInfoCommand extends ModerationCommand { + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH); + @Override protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { if (arguments.length == 0) { @@ -25,33 +30,60 @@ protected boolean executeModeration(Player moderator, Command command, String la return false; } - Player target = requireTarget(moderator, arguments[0]); - if (target == null) { + OfflinePlayer target = Bukkit.getOfflinePlayer(arguments[0]); + if (!target.hasPlayedBefore() && !target.isOnline()) { + warn(moderator, "Unable to find player §4" + arguments[0]); return false; } - String ipAddress = PunishmentManager.getPlayerIp(target); - Location location = target.getLocation(); - double maxHealth = Objects.requireNonNull(target.getAttribute(Attribute.MAX_HEALTH)).getValue(); - int healthPercent = (int) Math.round((target.getHealth() / maxHealth) * 100.0); + String targetName = target.getName() != null ? target.getName() : arguments[0]; + boolean isOnline = target.isOnline(); + String statusDot = isOnline ? "§a●" : "§c●"; + + Player targetPlayer = target.getPlayer(); + List punishments = getPunishmentManager().getAllPunishments(targetName); + String ipAddress = targetPlayer != null ? PunishmentManager.getPlayerIp(targetPlayer) : resolveOfflineIp(punishments); - List punishments = getPunishmentManager().getAllPunishments(target.getName()); long mutesCount = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.MUTE).count(); long bansCount = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.BAN || punishment.type() == PunishmentData.PunishmentType.IP_BAN).count(); long kicksCount = punishments.stream().filter(punishment -> punishment.type() == PunishmentData.PunishmentType.KICK).count(); - boolean isFrozen = getModerationManager().isFrozen(target); - boolean isMuted = getPunishmentManager().isMuted(target.getUniqueId()); - boolean isVanished = getModerationManager().getModerator(target).isVanished(); + String firstConnection = formatFirstPlayed(target.getFirstPlayed()); + String totalPlaytime = formatPlaytime(target.getStatistic(Statistic.PLAY_ONE_MINUTE)); - inform(moderator, " §eUser info for §d" + target.getName() + "§7:"); - moderator.sendMessage(" §f- §7UUID: §f" + target.getUniqueId()); - moderator.sendMessage(" §f- §7IP Address: §f" + ipAddress); - moderator.sendMessage(" §f- §7State: §a" + healthPercent + "% HP §f| §e" + target.getFoodLevel() + " Food §f| §b" + target.getGameMode().name() + " §f| §dLvl " + target.getLevel()); - moderator.sendMessage(" §f- §7Location: §f" + location.getWorld().getName() + " (" + location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ() + ")"); - moderator.sendMessage(" §f- §7Status: §fFrozen: " + (isFrozen ? "§aYES" : "§cNO") + " §f| Muted: " + (isMuted ? "§aYES" : "§cNO") + " §f| Vanished: " + (isVanished ? "§aYES" : "§cNO")); - moderator.sendMessage(" §f- §7History: §f" + mutesCount + " Mutes | " + bansCount + " Bans | " + kicksCount + " Kicks"); + moderator.sendMessage(" "); + moderator.sendMessage(" "); + moderator.sendMessage(" §e§lINFO§f | §f" + targetName + " " + statusDot); + moderator.sendMessage(" "); + moderator.sendMessage("§7 • §fIP Address: §e" + ipAddress); + moderator.sendMessage("§7 • §fFirst Connection: §e" + firstConnection); + moderator.sendMessage("§7 • §fTotal Playtime: §d" + totalPlaytime); + moderator.sendMessage("§7 • §fSanctions: §c" + mutesCount + "§f Mutes §7| §c" + bansCount + "§f Bans §7| §c" + kicksCount + "§f Kicks"); + moderator.sendMessage(" "); return true; } + + private String resolveOfflineIp(List punishments) { + return punishments.stream() + .map(PunishmentData::ipAddress) + .filter(ipAddress -> ipAddress != null && !ipAddress.isBlank() && !ipAddress.equals("0.0.0.0")) + .findFirst() + .orElse("Unknown"); + } + + private String formatFirstPlayed(long firstPlayedTimestamp) { + if (firstPlayedTimestamp <= 0) { + return "Unknown"; + } + Instant instant = Instant.ofEpochMilli(firstPlayedTimestamp); + return DATE_FORMATTER.format(instant.atZone(ZoneId.systemDefault())); + } + + private String formatPlaytime(int totalTicks) { + long totalSeconds = totalTicks / 20L; + long hours = totalSeconds / 3600L; + long minutes = (totalSeconds % 3600L) / 60L; + return hours + "h and " + minutes + "m"; + } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java index a58c9778..ed317abc 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java @@ -117,7 +117,6 @@ public void giveModerationTools() { inventory.setItem(1, ModerationTools.getReportViewer()); inventory.setItem(2, ModerationTools.getSanctionsInspector()); inventory.setItem(3, ModerationTools.getKnockbackStick()); - inventory.setItem(4, ModerationTools.getModerationMenu()); inventory.setItem(6, ModerationTools.getFreezer()); inventory.setItem(7, ModerationTools.getInventoryInspector()); inventory.setItem(8, ModerationTools.getVanishTool(isVanished)); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java index b4a5a598..383aa06c 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationTools.java @@ -39,13 +39,6 @@ public static ItemStack getKnockbackStick() { .toItemStack(); } - public static ItemStack getModerationMenu() { - return new ItemBuilder(Material.NETHER_STAR) - .setDisplayName("§dModeration Menu §7(Right Click)") - .addLore("§7Access to extra tools and settings.") - .toItemStack(); - } - public static ItemStack getFreezer() { return new ItemBuilder(Material.PACKED_ICE) .setDisplayName("§dFreeze Target §7(Right Click)") diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java index 7ef34495..8d4c165f 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/InvSeeMenu.java @@ -4,7 +4,10 @@ import fr.moussax.bedrock.ui.menu.TickableMenu; import fr.moussax.bedrock.ui.menu.interaction.MenuElementPreset; import fr.moussax.bedrock.utils.ItemBuilder; +import fr.moussax.blightedMod.moderator.ModerationManager; import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.attribute.Attribute; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemFlag; @@ -57,12 +60,29 @@ public void onTick(Player player) { } private void updateContents() { + boolean isInModerationMode = ModerationManager.getInstance().isInModerationMode(target); + String gameModeText = isInModerationMode ? "§dMODERATOR" : "§f" + target.getGameMode().name(); + + double maxHealth = Objects.requireNonNull(target.getAttribute(Attribute.MAX_HEALTH)).getValue(); + int currentHealth = (int) Math.round(target.getHealth()); + int maxHp = (int) Math.round(maxHealth); + + int level = target.getLevel(); + int xpPercent = Math.round(target.getExp() * 100.0f); + + int ping = target.getPing(); + String dimensionName = formatDimension(target.getWorld().getEnvironment()); + ItemStack playerInformation = new ItemBuilder(Material.PLAYER_HEAD) .setDisplayName("§d" + target.getName()) - .addLore( "", - " §7Health: §f" + (int) target.getHealth() + "§c❤", - " §7Food: §f" + target.getFoodLevel() + "§c\uD83C\uDF56", - " §7Gamemode: §f" + target.getGameMode().name() + " ", + .addLore("", + " §7Health: §f" + currentHealth + "§7/§f" + maxHp + " §c❤", + " §7Food: §f" + target.getFoodLevel() + "§7/20 §c\uD83C\uDF56", + " §7XP: §fLevel " + level + " §8(" + xpPercent + "%)", + " §7Gamemode: " + gameModeText, + " §7Dimension: §f" + dimensionName, + " §7Location: §f" + target.getLocation().getBlockX() + ", " + target.getLocation().getBlockY() + ", " + target.getLocation().getBlockZ() + " ", + " §7Ping: §d" + ping + "ms", "" ) .setSkullOwner(target.getUniqueId()) @@ -82,6 +102,15 @@ private void updateContents() { } } + private String formatDimension(World.Environment environment) { + return switch (environment) { + case NORMAL -> "OVERWORLD"; + case NETHER -> "NETHER"; + case THE_END -> "THE END"; + default -> environment.name(); + }; + } + private void updateSlot(int menuSlot, ItemStack item, ItemStack placeholder) { ItemStack displayItem = isEmpty(item) ? placeholder : item; ItemStack currentItem = getInventory().getItem(menuSlot); diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/GemsCommand.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/GemsCommand.java index da2985f5..c97d92be 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/GemsCommand.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/GemsCommand.java @@ -20,15 +20,7 @@ public final class GemsCommand extends AdminCommand { @Override protected boolean executeAdmin(Player player, Command command, String label, String[] args) { if (args.length == 0 || args[0].equalsIgnoreCase("help")) { - inform(player, "§e§lGEMS CURRENCY §7- Subcommands:"); - inform(player, " §f • §e/gems add [amount] §f» §7Give gems to a player."); - inform(player, " §f • §e/gems remove [amount] §f» §7Take gems from a player."); - inform(player, " §f • §e/gems set [amount] §f» §7Set gems for a player."); - inform(player, " §f • §e/gems reset §f» §7Reset gems for a player."); - inform(player, " §f • §e/gems giveall [amount] §f» §7Give gems to everyone."); - inform(player, " §f • §e/gems resetall §f» §7Reset everyone's balance."); - inform(player, " §f • §e/gems help §f» §7Prints this help message."); - + sendHelpMenu(player); return false; } @@ -46,6 +38,44 @@ protected boolean executeAdmin(Player player, Command command, String label, Str }; } + private void sendHelpMenu(Player player) { + player.sendMessage(" "); + player.sendMessage(" "); + player.sendMessage(" §e§lGEMS CURRENCY§f | §7Subcommands"); + player.sendMessage(" "); + player.sendMessage("§f • " + formatSyntax("gems add [amount]") + " §f§l» §7Give gems to a player."); + player.sendMessage("§f • " + formatSyntax("gems remove [amount]") + " §f§l» §7Take gems from a player."); + player.sendMessage("§f • " + formatSyntax("gems set [amount]") + " §f§l» §7Set gems for a player."); + player.sendMessage("§f • " + formatSyntax("gems reset ") + " §f§l» §7Reset gems for a player."); + player.sendMessage("§f • " + formatSyntax("gems giveall [amount]") + " §f§l» §7Give gems to everyone."); + player.sendMessage("§f • " + formatSyntax("gems resetall") + " §f§l» §7Reset everyone's balance."); + player.sendMessage("§f • " + formatSyntax("gems help") + " §f§l» §7Prints this help message."); + player.sendMessage(" "); + } + + private String formatSyntax(String syntax) { + StringBuilder builder = new StringBuilder("§e/"); + String[] arguments = syntax.strip().split("\\s+"); + + for (int index = 0; index < arguments.length; index++) { + if (index > 0) { + builder.append(' '); + } + builder.append(formatToken(arguments[index])); + } + return builder.toString(); + } + + private String formatToken(String token) { + if (token.length() > 1 && token.charAt(0) == '<' && token.charAt(token.length() - 1) == '>') { + return "§e<§f" + token.substring(1, token.length() - 1) + "§e>"; + } + if (token.length() > 1 && token.charAt(0) == '[' && token.charAt(token.length() - 1) == ']') { + return "§e[§f" + token.substring(1, token.length() - 1) + "§e]"; + } + return "§f" + token; + } + private boolean handleModify(Player sender, String[] args, boolean add) { if (args.length < 3) { warn(sender, "Usage: /gems " + (add ? "add" : "remove") + " [amount]"); From 2db100a20a51bee50f1ed5caf83d3955898cce55 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 12:43:13 +0200 Subject: [PATCH 21/30] feat(mod): add target glow, freeze overlay, and moderation chat styling - apply client-side pink glow to selected target players for moderators - retain powder snow freeze overlay and position lock without damage - cancel hunger drain for players active in moderation mode - color normal chat messages blue when sent while in moderation mode --- .../fr/moussax/blightedMod/BlightedMod.java | 2 +- .../commands/impl/FreezeCommand.java | 2 + .../moderator/BlightedModerator.java | 17 +++- .../moderator/ModerationGlowHelper.java | 81 +++++++++++++++++++ .../moderator/ModerationManager.java | 22 +++++ .../listeners/InteractiveChatListener.java | 10 ++- .../listeners/ModerationListener.java | 35 ++++++-- .../blightedMod/registry/EventsRegistry.java | 2 +- 8 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationGlowHelper.java diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java index ab04833f..9acda84e 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/BlightedMod.java @@ -43,9 +43,9 @@ public void onEnable() { PunishmentManager punishmentManager = new PunishmentManager(pluginDatabase.getConnection()); moderationManager = ModerationManager.init(punishmentManager); + moderationManager.startFreezeTask(this); CommandsRegistry.registerCommands(this); - eventsRegistry = new EventsRegistry(); eventsRegistry.initializeListeners(); } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java index a9ee24af..f32d73a0 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/FreezeCommand.java @@ -30,12 +30,14 @@ protected boolean executeModeration(Player moderator, Command command, String la boolean isNowFrozen = getModerationManager().toggleFreeze(target); if (isNowFrozen) { + target.setFreezeTicks(target.getMaxFreezeTicks()); moderator.sendMessage("§b ❄ §eYou froze §d" + target.getName() + "§e."); target.sendMessage(""); target.sendMessage("§b ❄ §b§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); target.sendMessage("§c ⚠ Do not log out or you will be permanently banned."); target.sendMessage(""); } else { + target.setFreezeTicks(0); moderator.sendMessage("§b ❄ §eYou unfroze §d" + target.getName() + "§e."); target.sendMessage("§b ❄ §7You have been unfrozen, you can move again."); } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java index ed317abc..09a9b017 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java @@ -38,11 +38,17 @@ public BlightedModerator(Player player) { this.isVanished = false; } - public void setTargetPlayer(Player targetPlayer) { - if (targetPlayer != null && targetPlayer.equals(player)) { + public void setTargetPlayer(Player newTargetPlayer) { + if (newTargetPlayer != null && newTargetPlayer.equals(player)) { return; } - this.targetPlayer = targetPlayer; + if (this.targetPlayer != null && !this.targetPlayer.equals(newTargetPlayer)) { + ModerationGlowHelper.removePinkGlow(player, this.targetPlayer); + } + this.targetPlayer = newTargetPlayer; + if (newTargetPlayer != null) { + ModerationGlowHelper.applyPinkGlow(player, newTargetPlayer); + } } public void onEnable() { @@ -57,11 +63,14 @@ public void onEnable() { } public void onDisable() { + if (this.targetPlayer != null) { + ModerationGlowHelper.removePinkGlow(player, this.targetPlayer); + this.targetPlayer = null; + } player.getInventory().clear(); restoreState(); setVanished(false, true); this.isInModerationMode = false; - this.targetPlayer = null; player.sendMessage(" §dModeration Mode §etoggled §cOFF§e."); } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationGlowHelper.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationGlowHelper.java new file mode 100644 index 00000000..bf37773c --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationGlowHelper.java @@ -0,0 +1,81 @@ +package fr.moussax.blightedMod.moderator; + +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.scoreboard.Team; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class ModerationGlowHelper { + + private static final String GLOW_TEAM_NAME = "mod_glow_pink"; + + private ModerationGlowHelper() { + } + + public static void applyPinkGlow(Player moderator, Player target) { + if (moderator == null || target == null || !moderator.isOnline() || !target.isOnline()) { + return; + } + + Scoreboard scoreboard = moderator.getScoreboard(); + if (scoreboard.equals(Objects.requireNonNull(Bukkit.getScoreboardManager()).getMainScoreboard())) { + scoreboard = Bukkit.getScoreboardManager().getNewScoreboard(); + moderator.setScoreboard(scoreboard); + } + + Team team = scoreboard.getTeam(GLOW_TEAM_NAME); + if (team == null) { + team = scoreboard.registerNewTeam(GLOW_TEAM_NAME); + team.setColor(ChatColor.LIGHT_PURPLE); + } + + if (!team.hasEntry(target.getName())) { + team.addEntry(target.getName()); + } + + sendGlowPacket(moderator, target, true); + } + + public static void removePinkGlow(Player moderator, Player target) { + if (moderator == null || target == null || !moderator.isOnline()) { + return; + } + + Scoreboard scoreboard = moderator.getScoreboard(); + Team team = scoreboard.getTeam(GLOW_TEAM_NAME); + if (team != null && team.hasEntry(target.getName())) { + team.removeEntry(target.getName()); + } + + if (target.isOnline()) { + sendGlowPacket(moderator, target, false); + } + } + + private static void sendGlowPacket(Player moderator, Player target, boolean glowing) { + try { + CraftPlayer craftTarget = (CraftPlayer) target; + CraftPlayer craftModerator = (CraftPlayer) moderator; + + byte existingFlags = craftTarget.getHandle().getEntityData().get(new EntityDataAccessor<>(0, EntityDataSerializers.BYTE)); + byte updatedFlags = glowing ? (byte) (existingFlags | 0x40) : (byte) (existingFlags & ~0x40); + + List> dataValues = new ArrayList<>(); + dataValues.add(SynchedEntityData.DataValue.create(new EntityDataAccessor<>(0, EntityDataSerializers.BYTE), updatedFlags)); + + ClientboundSetEntityDataPacket packet = new ClientboundSetEntityDataPacket(target.getEntityId(), dataValues); + craftModerator.getHandle().connection.send(packet); + } catch (Throwable _) { + } + } +} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java index c446c082..ce396c5e 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java @@ -5,6 +5,7 @@ import fr.moussax.blightedMod.utils.PluginPermissions; import lombok.Getter; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.entity.Player; import java.util.*; @@ -23,6 +24,7 @@ public enum ChatChannel { private final PunishmentManager punishmentManager; private final Map moderators = new HashMap<>(); private final Set frozenPlayers = new HashSet<>(); + private final Map frozenLocations = new ConcurrentHashMap<>(); private final Map lastChatTimestamps = new ConcurrentHashMap<>(); private final Map playerChatChannels = new ConcurrentHashMap<>(); private final Set messageInspectEnabled = ConcurrentHashMap.newKeySet(); @@ -80,16 +82,35 @@ public boolean isFrozen(Player player) { return frozenPlayers.contains(player.getUniqueId()); } + public Location getFrozenLocation(Player player) { + return frozenLocations.get(player.getUniqueId()); + } + public boolean toggleFreeze(Player player) { UUID playerId = player.getUniqueId(); if (frozenPlayers.contains(playerId)) { frozenPlayers.remove(playerId); + frozenLocations.remove(playerId); + player.setFreezeTicks(0); return false; } frozenPlayers.add(playerId); + frozenLocations.put(playerId, player.getLocation()); + player.setFreezeTicks(139); return true; } + public void startFreezeTask(org.bukkit.plugin.Plugin plugin) { + Bukkit.getScheduler().runTaskTimer(plugin, () -> { + for (UUID frozenId : frozenPlayers) { + Player player = Bukkit.getPlayer(frozenId); + if (player != null && player.isOnline()) { + player.setFreezeTicks(139); + } + } + }, 0L, 5L); + } + public void setSlowmodeDelaySeconds(int seconds) { this.slowmodeDelaySeconds = Math.max(0, seconds); } @@ -149,6 +170,7 @@ public void handlePlayerQuit(Player player) { } moderators.remove(playerId); frozenPlayers.remove(playerId); + frozenLocations.remove(playerId); lastChatTimestamps.remove(playerId); playerChatChannels.remove(playerId); messageInspectEnabled.remove(playerId); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java index 722e38df..237699c2 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java @@ -1,6 +1,7 @@ package fr.moussax.blightedMod.moderator.listeners; import fr.moussax.bedrock.text.InteractiveMessage; +import fr.moussax.blightedMod.moderator.ModerationManager; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -9,18 +10,25 @@ public final class InteractiveChatListener implements Listener { + private final ModerationManager moderationManager; + + public InteractiveChatListener(ModerationManager moderationManager) { + this.moderationManager = moderationManager; + } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onNormalPlayerChat(AsyncPlayerChatEvent event) { event.setCancelled(true); Player sender = event.getPlayer(); String chatMessage = event.getMessage(); + String messageColor = moderationManager.isInModerationMode(sender) ? "§b" : "§f"; InteractiveMessage interactiveMessage = InteractiveMessage.text(" ") .hoverAndExecute("§c⚑ ", "§cClick to report §d" + sender.getName() + "§c.", "/report chat " + sender.getName() + " " + chatMessage ) - .append("§7" + sender.getName() + " §8§l» §f" + chatMessage); + .append("§7" + sender.getName() + " §8§l» " + messageColor + chatMessage); for (Player recipient : event.getRecipients()) { interactiveMessage.send(recipient); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java index b766c524..5acfee72 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java @@ -20,6 +20,7 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.*; import org.bukkit.event.server.ServerListPingEvent; @@ -226,10 +227,23 @@ public void onFrozenPlayerMove(PlayerMoveEvent event) { Player player = event.getPlayer(); if (moderationManager.isModerator(player) || !moderationManager.isFrozen(player)) return; - boolean movedHorizontally = event.getFrom().getX() != Objects.requireNonNull(event.getTo()).getX() - || event.getFrom().getZ() != event.getTo().getZ(); + org.bukkit.Location frozenLocation = moderationManager.getFrozenLocation(player); + if (frozenLocation == null) return; - if (movedHorizontally) event.setTo(event.getFrom()); + org.bukkit.Location to = event.getTo(); + if (to == null) return; + + double deltaX = to.getX() - frozenLocation.getX(); + double deltaY = to.getY() - frozenLocation.getY(); + double deltaZ = to.getZ() - frozenLocation.getZ(); + double distanceSquared = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ; + + if (distanceSquared > 0.05) { + org.bukkit.Location returnLocation = frozenLocation.clone(); + returnLocation.setYaw(to.getYaw()); + returnLocation.setPitch(to.getPitch()); + event.setTo(returnLocation); + } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @@ -301,12 +315,14 @@ public void onModeratorPlaceBlock(BlockPlaceEvent event) { } /** - * Non-entity damage (fall, fire, drown, etc.) against a moderator in moderation mode. + * Non-entity damage (fall, fire, drown, freeze, etc.) against a moderator in moderation mode or a frozen player. */ @EventHandler public void onModeratorTakeDamage(EntityDamageEvent event) { - if (event.getEntity() instanceof Player player && moderationManager.isInModerationMode(player)) { - event.setCancelled(true); + if (event.getEntity() instanceof Player player) { + if (moderationManager.isInModerationMode(player) || moderationManager.isFrozen(player)) { + event.setCancelled(true); + } } } @@ -336,6 +352,13 @@ public void onModeratorInventoryClick(InventoryClickEvent event) { } } + @EventHandler + public void onModeratorFoodLevelChange(FoodLevelChangeEvent event) { + if (event.getEntity() instanceof Player player && moderationManager.isInModerationMode(player)) { + event.setCancelled(true); + } + } + private void handleToolInteraction(Player player, Material tool) { switch (tool) { case ENDER_EYE -> teleportToRandomPlayer(player); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java index 5827afea..e0b8282f 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/EventsRegistry.java @@ -32,7 +32,7 @@ public void initializeListeners() { pluginManager.registerEvents(signInputListener, instance); pluginManager.registerEvents(new MenuListener(menuSystem), instance); pluginManager.registerEvents(new ModerationListener(instance.getModerationManager()), instance); - pluginManager.registerEvents(new InteractiveChatListener(), instance); + pluginManager.registerEvents(new InteractiveChatListener(instance.getModerationManager()), instance); } From 5b0dddf10c4f8e6eec2d58b841f0d8104ee34ff6 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 13:01:52 +0200 Subject: [PATCH 22/30] fix(bedrock): resolve thread safety in BlightedPlayer and dynamic slots in PaginatedMenu - make PaginatedMenu navigation control slots dynamic based on inventory size - use computeIfAbsent in BlightedPlayer for thread-safe instance lookup - single-pass ActionbarComposer section evaluation and var cleanup --- .../ui/actionbar/ActionbarComposer.java | 25 ++++++++----------- .../java/fr/moussax/bedrock/ui/menu/Menu.java | 3 ++- .../bedrock/ui/menu/types/PaginatedMenu.java | 15 +++++++---- .../engine/player/BlightedPlayer.java | 9 +------ .../engine/player/PlayerListener.java | 2 +- 5 files changed, 25 insertions(+), 29 deletions(-) diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java index f382cfcf..3b92578d 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java @@ -5,12 +5,12 @@ import org.jspecify.annotations.NonNull; import java.time.Duration; +import java.util.ArrayList; import java.util.Comparator; +import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.PriorityQueue; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; /** * Assembles and formats per-player action bar text from active sections and temporary alerts. @@ -106,26 +106,23 @@ public String compile(@NonNull Player player) { } } - var visibleSections = sections.values().stream() + List visibleSections = sections.values().stream() .filter(section -> section.visibility().test(player)) .sorted(Comparator.comparingInt(ActionbarSection::priority)) .toList(); - // ponytail: kept — exclusive section overrides default actionbar when active + List evaluatedTexts = new ArrayList<>(visibleSections.size()); for (ActionbarSection section : visibleSections) { - if (section.exclusive()) { - String text = evaluateSection(section, player); - if (text != null && !text.isEmpty()) { - return text; - } + String text = evaluateSection(section, player); + if (section.exclusive() && text != null && !text.isEmpty()) { + return text; + } + if (text != null && !text.isEmpty()) { + evaluatedTexts.add(text); } } - return visibleSections.stream() - .map(section -> evaluateSection(section, player)) - .filter(Objects::nonNull) - .filter(sectionText -> !sectionText.isEmpty()) - .collect(Collectors.joining(separator)); + return String.join(separator, evaluatedTexts); } private String evaluateSection(ActionbarSection section, Player player) { diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java index d2e52bf8..3234e8c4 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java @@ -13,6 +13,7 @@ import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemStack; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -714,7 +715,7 @@ public void refresh(@NonNull Player player) { slots.clear(); build(player); - var openInventory = player.getOpenInventory(); + InventoryView openInventory = player.getOpenInventory(); if (openInventory.getTopInventory().getHolder() == this && !openInventory.getTitle().equals(this.title)) { openInventory.setTitle(this.title); } diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java index 74efb8b0..53b444fd 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java @@ -127,12 +127,17 @@ public void build(@NonNull Player viewer) { totalItems = Math.max(0, getTotalItems(viewer)); clearInventory(); + int closeSlot = size - 5; + int backSlot = size - 6; + int nextSlot = size - 4; + ItemStack emptyItem = getEmptyStateItem(viewer); if (totalItems == 0) { if (emptyItem != null) { - setItem(22, emptyItem, MenuItemInteraction.ANY_CLICK, (_, _) -> { }); + int emptySlot = size >= 27 ? 22 : size / 2; + setItem(emptySlot, emptyItem, MenuItemInteraction.ANY_CLICK, (_, _) -> { }); } - setCloseButton(49); + setCloseButton(closeSlot); return; } @@ -166,20 +171,20 @@ public void build(@NonNull Player viewer) { } if (currentPage > 0) { - setBackButton(48, (player, _) -> { + setBackButton(backSlot, (player, _) -> { currentPage--; refresh(player); }); } if (endIndex < totalItems) { - setItem(50, MenuElementPreset.NEXT_BUTTON, (player, _) -> { + setItem(nextSlot, MenuElementPreset.NEXT_BUTTON, (player, _) -> { currentPage++; refresh(player); }); } - setCloseButton(49); + setCloseButton(closeSlot); } /** diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java index cb674ffb..3eca843a 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java @@ -73,8 +73,6 @@ public BlightedPlayer(Player player) { setCurrentMana(dataHandler.getSavedMana()); this.forgeFuel = dataHandler.getSavedForgeFuel(); - players.put(playerId, this); - ArmorManager.updatePlayerArmor(this); } @@ -91,14 +89,9 @@ public void tick() { * @param player player whose context to retrieve * @return registered player context, or {@code null} if no context exists */ - // ponytail: kept — explicit get-then-instantiate avoids re-entrant computeIfAbsent mutation in constructor public static BlightedPlayer get(Player player) { if (player == null || !player.isOnline()) return null; - BlightedPlayer blightedPlayer = players.get(player.getUniqueId()); - if (blightedPlayer == null) { - blightedPlayer = new BlightedPlayer(player); - } - return blightedPlayer; + return players.computeIfAbsent(player.getUniqueId(), _ -> new BlightedPlayer(player)); } /** diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/PlayerListener.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/PlayerListener.java index 08551d46..2a15077e 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/PlayerListener.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/PlayerListener.java @@ -20,7 +20,7 @@ public final class PlayerListener implements Listener { @EventHandler public void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - new BlightedPlayer(event.getPlayer()); + BlightedPlayer.get(event.getPlayer()); event.setJoinMessage(" §f" + event.getPlayer().getName() + " §7joined the SMP."); if (!player.hasPlayedBefore()) { From 9fade520919be6d5ef458a9123febf35ddcb2ed9 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 13:01:54 +0200 Subject: [PATCH 23/30] feat(mod): add console support for kick, ban, and mute commands - allow server console to execute kick, ban, unban, and mute commands - record console punishments under CONSOLE_UUID - remove dead testmod command and registration --- .../bedrock/commands/PlayerCommand.java | 17 +++- .../commands/ModerationCommand.java | 79 +++++++++++++++---- .../commands/impl/TestModCommand.java | 35 -------- .../blightedMod/database/PluginDatabase.java | 18 +---- .../punishments/PunishmentManager.java | 18 +++-- .../registry/CommandsRegistry.java | 3 - blighted-mod/src/main/resources/plugin.yml | 3 - 7 files changed, 93 insertions(+), 80 deletions(-) delete mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java diff --git a/bedrock/src/main/java/fr/moussax/bedrock/commands/PlayerCommand.java b/bedrock/src/main/java/fr/moussax/bedrock/commands/PlayerCommand.java index f57b289c..140b19e1 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/commands/PlayerCommand.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/commands/PlayerCommand.java @@ -63,11 +63,11 @@ protected abstract boolean execute( * *

If no matching player is found, a warning is sent to the command sender. * - * @param sender the player requesting the lookup + * @param sender the command sender requesting the lookup * @param name the exact username to resolve * @return the matching online player, or {@code null} if no player exists */ - protected final @Nullable Player requireTarget(Player sender, String name) { + protected final @Nullable Player requireTarget(CommandSender sender, String name) { Player target = Bukkit.getPlayerExact(name); if (target == null) { @@ -76,4 +76,17 @@ protected abstract boolean execute( return target; } + + /** + * Resolves an online player by their exact username. + * + *

If no matching player is found, a warning is sent to the command sender. + * + * @param sender the player requesting the lookup + * @param name the exact username to resolve + * @return the matching online player, or {@code null} if no player exists + */ + protected final @Nullable Player requireTarget(Player sender, String name) { + return requireTarget((CommandSender) sender, name); + } } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/ModerationCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/ModerationCommand.java index 4ab1d580..5d5bcf8e 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/ModerationCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/ModerationCommand.java @@ -1,31 +1,43 @@ package fr.moussax.blightedMod.commands; -import fr.moussax.bedrock.commands.PlayerCommand; import fr.moussax.blightedMod.moderator.ModerationManager; import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; +import org.bukkit.Bukkit; import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import static fr.moussax.bedrock.text.Messenger.warn; /** * Base abstract class for all moderation commands in BlightedMod. - * Validates that the sender is a qualified moderator before delegating execution. + * Validates that the sender is a qualified moderator or console before delegating execution. */ -public abstract class ModerationCommand extends PlayerCommand { +public abstract class ModerationCommand implements CommandExecutor { @Override - protected final boolean execute(Player player, Command command, String label, String[] arguments) { - if (!getModerationManager().isModerator(player)) { - warn(player, "You do not have permission to execute moderation commands."); - return true; + public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] arguments) { + if (sender instanceof Player player) { + if (!getModerationManager().isModerator(player)) { + warn(player, "You must be MODERATOR or higher to use this command."); + return true; + } + return executeModeration(player, command, label, arguments); } - return executeModeration(player, command, label, arguments); + if (isConsoleAllowed()) { + return executeModeration(sender, command, label, arguments); + } + + warn(sender, "This command can only be executed by in-game moderators."); + return true; } /** - * Executes the moderation command logic. + * Executes the moderation command logic for in-game moderators. * * @param moderator validated player executing the moderation command * @param command command executed @@ -33,12 +45,49 @@ protected final boolean execute(Player player, Command command, String label, St * @param arguments command arguments * @return {@code true} if handled; otherwise {@code false} */ - protected abstract boolean executeModeration( - Player moderator, - Command command, - String label, - String[] arguments - ); + protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + return executeModeration((CommandSender) moderator, command, label, arguments); + } + + /** + * Executes the moderation command logic for any command sender (Player or Console). + * + * @param moderator validated command sender executing the moderation command + * @param command command executed + * @param label alias used to invoke the command + * @param arguments command arguments + * @return {@code true} if handled; otherwise {@code false} + */ + protected boolean executeModeration(CommandSender moderator, Command command, String label, String[] arguments) { + if (moderator instanceof Player player) { + return executeModeration(player, command, label, arguments); + } + warn(moderator, "This command can only be executed by in-game moderators."); + return true; + } + + /** + * Returns whether this moderation command can be executed from the server console. + * + * @return {@code true} if console execution is allowed; otherwise {@code false} + */ + protected boolean isConsoleAllowed() { + return false; + } + + protected final @Nullable Player requireTarget(CommandSender sender, String name) { + Player target = Bukkit.getPlayerExact(name); + + if (target == null) { + warn(sender, "Unable to find player §4" + name); + } + + return target; + } + + protected final @Nullable Player requireTarget(Player sender, String name) { + return requireTarget((CommandSender) sender, name); + } protected ModerationManager getModerationManager() { return ModerationManager.getInstance(); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java deleted file mode 100644 index 492a9b77..00000000 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/TestModCommand.java +++ /dev/null @@ -1,35 +0,0 @@ -package fr.moussax.blightedMod.commands.impl; - -import fr.moussax.blightedMod.commands.ModerationCommand; -import org.bukkit.command.Command; -import org.bukkit.entity.Player; - -import static fr.moussax.bedrock.text.Messenger.inform; - -public final class TestModCommand extends ModerationCommand { - - @Override - protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { - String category = arguments.length > 0 ? arguments[0].toLowerCase() : "all"; - - inform(moderator, " "); - inform(moderator, " "); - - if (category.equals("all") || category.equals("target") || category.equals("user") || category.equals("player")) { - inform(moderator, "§8--- Target / Player Facing Messages ---"); - inform(moderator, " §c⌚ §cYou are muted for §d1h §cfor §bAdvertising Social Media§c."); - inform(moderator, " §c⌚ §cYou are muted for §cfor §bAdvertising Social Media§c."); - inform(moderator, " §a⚑ §7You are no longer muted."); - inform(moderator, " "); - inform(moderator, "§b ❄ §b§lYOU HAVE BEEN FROZEN BY A MODERATOR!"); - inform(moderator, "§c ⚠ Do not log out or you will be permanently banned."); - inform(moderator, " "); - inform(moderator, "§b ❄ §7You have been unfrozen, you can move again."); - inform(moderator, " §c⌚ §cPlease wait §d5s §cbefore chatting again."); - } - - inform(moderator, " "); - inform(moderator, " "); - return true; - } -} diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PluginDatabase.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PluginDatabase.java index 46e71200..f222bef0 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PluginDatabase.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/database/PluginDatabase.java @@ -1,36 +1,22 @@ package fr.moussax.blightedMod.database; import fr.moussax.bedrock.utils.debug.Log; +import lombok.Getter; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; -/** - * Manages SQLite database connection and schema initialization for BlightedMod. - */ +@Getter public final class PluginDatabase { private final Connection connection; - /** - * Creates a database manager at the specified file path and initializes the schema. - * - * @param path absolute file path to the SQLite database file - * @throws SQLException if a database access error occurs during connection or initialization - */ public PluginDatabase(String path) throws SQLException { this.connection = DriverManager.getConnection("jdbc:sqlite:" + path); initializeSchema(); } - public Connection getConnection() { - return connection; - } - - /** - * Closes the active database connection if currently open. - */ public void closeConnection() { if (connection == null) return; synchronized (connection) { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java index 032e8b7d..a1a16888 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java @@ -145,39 +145,45 @@ public boolean isIpBanned(String ipAddress) { return getActiveIpBan(ipAddress) != null; } - public void addBan(Player target, Player moderator, String reason, Long expiresAt) { + public static final UUID CONSOLE_UUID = UUID.nameUUIDFromBytes("CONSOLE".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + + public static UUID getModeratorUuid(org.bukkit.command.CommandSender sender) { + return sender instanceof Player player ? player.getUniqueId() : CONSOLE_UUID; + } + + public void addBan(Player target, org.bukkit.command.CommandSender moderator, String reason, Long expiresAt) { addPunishment( target.getUniqueId(), target.getName(), PunishmentData.PunishmentType.BAN, reason, - moderator.getUniqueId(), + getModeratorUuid(moderator), moderator.getName(), expiresAt, getPlayerIp(target) ); } - public void addMute(Player target, Player moderator, String reason, Long expiresAt) { + public void addMute(Player target, org.bukkit.command.CommandSender moderator, String reason, Long expiresAt) { addPunishment( target.getUniqueId(), target.getName(), PunishmentData.PunishmentType.MUTE, reason, - moderator.getUniqueId(), + getModeratorUuid(moderator), moderator.getName(), expiresAt, getPlayerIp(target) ); } - public void addIpBan(Player target, Player moderator, String reason, Long expiresAt) { + public void addIpBan(Player target, org.bukkit.command.CommandSender moderator, String reason, Long expiresAt) { addPunishment( target.getUniqueId(), target.getName(), PunishmentData.PunishmentType.IP_BAN, reason, - moderator.getUniqueId(), + getModeratorUuid(moderator), moderator.getName(), expiresAt, getPlayerIp(target) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java index ec77cf5b..1d368a9f 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/registry/CommandsRegistry.java @@ -17,7 +17,6 @@ import fr.moussax.blightedMod.commands.impl.SetSlowmodeCommand; import fr.moussax.blightedMod.commands.impl.SocialSpyCommand; import fr.moussax.blightedMod.commands.impl.TargetCommand; -import fr.moussax.blightedMod.commands.impl.TestModCommand; import fr.moussax.blightedMod.commands.impl.UserInfoCommand; import fr.moussax.blightedMod.commands.impl.VanishCommand; import org.bukkit.Bukkit; @@ -71,8 +70,6 @@ public static void registerCommands(JavaPlugin plugin) { registrar.register("msginspect", socialSpyCommand); registrar.register("spy", socialSpyCommand); registrar.register("socialspy", socialSpyCommand); - - registrar.register("testmod", new TestModCommand()); } diff --git a/blighted-mod/src/main/resources/plugin.yml b/blighted-mod/src/main/resources/plugin.yml index a6b18984..ea659872 100644 --- a/blighted-mod/src/main/resources/plugin.yml +++ b/blighted-mod/src/main/resources/plugin.yml @@ -77,7 +77,4 @@ commands: msginspect: description: Toggle private message inspection (SocialSpy). aliases: [spy, socialspy] - permission: blightedmod.moderator - testmod: - description: Test and review all moderator chat message formatting. permission: blightedmod.moderator \ No newline at end of file From c4786a62d57cf58eab03e4dc67c18775ebcfe577 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 13:01:56 +0200 Subject: [PATCH 24/30] refactor(mod): extract PunishmentArguments record for duration and reason parsing - create PunishmentArguments record to parse optional duration and reason - deduplicate argument handling across ban, banip, and mute commands --- .../blightedMod/commands/impl/BanCommand.java | 40 +++++++++---------- .../commands/impl/KickCommand.java | 17 +++++--- .../commands/impl/MuteCommand.java | 26 ++++++------ .../punishments/PunishmentArguments.java | 31 ++++++++++++++ 4 files changed, 76 insertions(+), 38 deletions(-) create mode 100644 blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentArguments.java diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java index a2401ae6..24f38e08 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java @@ -3,12 +3,13 @@ import fr.moussax.bedrock.commands.CommandArgument; import fr.moussax.blightedMod.commands.ModerationCommand; import fr.moussax.blightedMod.moderator.punishments.DurationParser; +import fr.moussax.blightedMod.moderator.punishments.PunishmentArguments; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import java.util.Arrays; import java.util.Locale; import static fr.moussax.bedrock.text.Messenger.warn; @@ -17,7 +18,12 @@ public final class BanCommand extends ModerationCommand { @Override - protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + protected boolean isConsoleAllowed() { + return true; + } + + @Override + protected boolean executeModeration(CommandSender moderator, Command command, String label, String[] arguments) { return switch (label.toLowerCase(Locale.ROOT)) { case "unban" -> handleUnban(moderator, arguments); case "banip" -> handleBanIp(moderator, arguments); @@ -26,7 +32,7 @@ protected boolean executeModeration(Player moderator, Command command, String la }; } - private boolean handleBan(Player moderator, String[] arguments) { + private boolean handleBan(CommandSender moderator, String[] arguments) { if (arguments.length < 1) { warn(moderator, "Usage: /ban [duration] [reason]"); moderator.sendMessage("§7Duration format: 1d, 3w, 1m, 1y (omit for permanent)"); @@ -43,17 +49,14 @@ private boolean handleBan(Player moderator, String[] arguments) { return false; } - if (getModerationManager().isModerator(target)) { + if (moderator instanceof Player && getModerationManager().isModerator(target)) { warn(moderator, "You cannot ban another moderator."); return false; } - Long expiresAt = arguments.length > 1 ? DurationParser.parseDuration(arguments[1]) : null; - int reasonStartIndex = expiresAt != null ? 2 : 1; - - String reason = arguments.length > reasonStartIndex - ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) - : "No reason specified"; + PunishmentArguments punishmentArguments = PunishmentArguments.parse(arguments, 1); + Long expiresAt = punishmentArguments.expiresAt(); + String reason = punishmentArguments.reason(); getPunishmentManager().addBan(target, moderator, reason, expiresAt); @@ -75,7 +78,7 @@ private boolean handleBan(Player moderator, String[] arguments) { return true; } - private boolean handleUnban(Player moderator, String[] arguments) { + private boolean handleUnban(CommandSender moderator, String[] arguments) { if (arguments.length < 1) { warn(moderator, "Usage: /unban "); return false; @@ -99,7 +102,7 @@ private boolean handleUnban(Player moderator, String[] arguments) { return true; } - private boolean handleBanIp(Player moderator, String[] arguments) { + private boolean handleBanIp(CommandSender moderator, String[] arguments) { if (arguments.length < 1) { warn(moderator, "Usage: /banip [duration] [reason]"); moderator.sendMessage("§7Duration format: 1d, 3w, 1m, 1y (omit for permanent)"); @@ -116,17 +119,14 @@ private boolean handleBanIp(Player moderator, String[] arguments) { return false; } - if (getModerationManager().isModerator(target)) { + if (moderator instanceof Player && getModerationManager().isModerator(target)) { warn(moderator, "You cannot IP ban another moderator."); return false; } - Long expiresAt = arguments.length > 1 ? DurationParser.parseDuration(arguments[1]) : null; - int reasonStartIndex = expiresAt != null ? 2 : 1; - - String reason = arguments.length > reasonStartIndex - ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) - : "No reason specified"; + PunishmentArguments punishmentArguments = PunishmentArguments.parse(arguments, 1); + Long expiresAt = punishmentArguments.expiresAt(); + String reason = punishmentArguments.reason(); getPunishmentManager().addIpBan(target, moderator, reason, expiresAt); @@ -147,7 +147,7 @@ private boolean handleBanIp(Player moderator, String[] arguments) { return true; } - private boolean handleUnbanIp(Player moderator, String[] arguments) { + private boolean handleUnbanIp(CommandSender moderator, String[] arguments) { if (arguments.length < 1) { warn(moderator, "Usage: /unbanip "); return false; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java index 334dc97e..e22e5c6b 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/KickCommand.java @@ -5,6 +5,7 @@ import fr.moussax.blightedMod.moderator.punishments.PunishmentData; import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import java.util.Arrays; @@ -15,7 +16,12 @@ public final class KickCommand extends ModerationCommand { @Override - protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + protected boolean isConsoleAllowed() { + return true; + } + + @Override + protected boolean executeModeration(CommandSender moderator, Command command, String label, String[] arguments) { if (arguments.length < 1) { warn(moderator, "Usage: /kick [reason]"); return false; @@ -31,7 +37,7 @@ protected boolean executeModeration(Player moderator, Command command, String la return false; } - if (getModerationManager().isModerator(target)) { + if (moderator instanceof Player && getModerationManager().isModerator(target)) { warn(moderator, "You cannot kick another moderator."); return false; } @@ -46,7 +52,7 @@ protected boolean executeModeration(Player moderator, Command command, String la target.getName(), PunishmentData.PunishmentType.KICK, reason, - moderator.getUniqueId(), + PunishmentManager.getModeratorUuid(moderator), moderator.getName(), null, ipAddress @@ -54,10 +60,9 @@ protected boolean executeModeration(Player moderator, Command command, String la String kickMessage = """ §cYou are kicked from this server! - + §7Reason: §f%s - - §7If you believe this was a mistake, please appeal on our Discord.""".formatted(reason); + """.formatted(reason); target.kickPlayer(kickMessage); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java index 68bc500b..fb2ae10a 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/MuteCommand.java @@ -3,19 +3,24 @@ import fr.moussax.bedrock.commands.CommandArgument; import fr.moussax.blightedMod.commands.ModerationCommand; import fr.moussax.blightedMod.moderator.punishments.DurationParser; +import fr.moussax.blightedMod.moderator.punishments.PunishmentArguments; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import java.util.Arrays; - import static fr.moussax.bedrock.text.Messenger.warn; @CommandArgument(position = 0, suggestions = {"$players"}) public final class MuteCommand extends ModerationCommand { @Override - protected boolean executeModeration(Player moderator, Command command, String label, String[] arguments) { + protected boolean isConsoleAllowed() { + return true; + } + + @Override + protected boolean executeModeration(CommandSender moderator, Command command, String label, String[] arguments) { if (label.equalsIgnoreCase("unmute")) { return handleUnmute(moderator, arguments); } @@ -23,7 +28,7 @@ protected boolean executeModeration(Player moderator, Command command, String la return handleMute(moderator, arguments); } - private boolean handleMute(Player moderator, String[] arguments) { + private boolean handleMute(CommandSender moderator, String[] arguments) { if (arguments.length < 1) { warn(moderator, "Usage: /mute [duration] [reason]"); moderator.sendMessage("§7Duration format: 1d, 3w, 1m, 1y (omit for permanent)"); @@ -40,17 +45,14 @@ private boolean handleMute(Player moderator, String[] arguments) { return false; } - if (getModerationManager().isModerator(target)) { + if (moderator instanceof Player && getModerationManager().isModerator(target)) { warn(moderator, "You cannot mute another moderator."); return false; } - Long expiresAt = arguments.length > 1 ? DurationParser.parseDuration(arguments[1]) : null; - int reasonStartIndex = expiresAt != null ? 2 : 1; - - String reason = arguments.length > reasonStartIndex - ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) - : "No reason specified"; + PunishmentArguments punishmentArguments = PunishmentArguments.parse(arguments, 1); + Long expiresAt = punishmentArguments.expiresAt(); + String reason = punishmentArguments.reason(); getPunishmentManager().addMute(target, moderator, reason, expiresAt); @@ -67,7 +69,7 @@ private boolean handleMute(Player moderator, String[] arguments) { return true; } - private boolean handleUnmute(Player moderator, String[] arguments) { + private boolean handleUnmute(CommandSender moderator, String[] arguments) { if (arguments.length < 1) { warn(moderator, "Usage: /unmute "); return false; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentArguments.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentArguments.java new file mode 100644 index 00000000..57e36407 --- /dev/null +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentArguments.java @@ -0,0 +1,31 @@ +package fr.moussax.blightedMod.moderator.punishments; + +import java.util.Arrays; +import org.jspecify.annotations.Nullable; + +/** + * Encapsulates parsed optional expiration timestamp and reason from punishment command arguments. + * + * @param expiresAt expiration timestamp in milliseconds, or {@code null} if permanent + * @param reason punishment reason specified, or "No reason specified" if omitted + */ +public record PunishmentArguments(@Nullable Long expiresAt, String reason) { + + /** + * Parses expiration duration and reason from command arguments starting at a specified index. + * + * @param arguments raw command arguments + * @param startIndex index after target player argument (typically 1) + * @return parsed punishment arguments context + */ + public static PunishmentArguments parse(String[] arguments, int startIndex) { + Long expiresAt = arguments.length > startIndex ? DurationParser.parseDuration(arguments[startIndex]) : null; + int reasonStartIndex = expiresAt != null ? startIndex + 1 : startIndex; + + String reason = arguments.length > reasonStartIndex + ? String.join(" ", Arrays.copyOfRange(arguments, reasonStartIndex, arguments.length)) + : "No reason specified"; + + return new PunishmentArguments(expiresAt, reason); + } +} From 438b7f2a9962b0348a9bee13f63fa4af33d2ccdf Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 13:04:45 +0200 Subject: [PATCH 25/30] chore: update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index de3e13e8..565cc10d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A Spigot 26.2 plugin providing a high-difficulty survival experience inspired by - **`bedrock`**: Core framework module handling UI menus, action bars, announcements, scheduling, and utility systems. - **`blighted-smp`**: Primary SMP plugin module containing custom items, mobs, abilities, recipes, and gameplay mechanics. +- **`blighted-mod`**: Moderation plugin attaching to the SMP server. ## Requirements From 797f92f6b8c39c0be4619c325d4e995169a61386 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 13:22:56 +0200 Subject: [PATCH 26/30] fix: remove event cancellation --- .../moderator/listeners/InteractiveChatListener.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java index 237699c2..73f586cf 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/InteractiveChatListener.java @@ -18,8 +18,6 @@ public InteractiveChatListener(ModerationManager moderationManager) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onNormalPlayerChat(AsyncPlayerChatEvent event) { - event.setCancelled(true); - Player sender = event.getPlayer(); String chatMessage = event.getMessage(); String messageColor = moderationManager.isInModerationMode(sender) ? "§b" : "§f"; @@ -33,5 +31,6 @@ public void onNormalPlayerChat(AsyncPlayerChatEvent event) { for (Player recipient : event.getRecipients()) { interactiveMessage.send(recipient); } + event.getRecipients().clear(); } } From c81bbe09681ed67dfd30e79c001b2b6a679e1878 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 13:52:03 +0200 Subject: [PATCH 27/30] chore: update code peer code review --- .../commands/impl/GemsCommand.java | 14 +++++++------- .../engine/player/BlightedPlayer.java | 7 +++++-- pom.xml | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/GemsCommand.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/GemsCommand.java index c97d92be..84327520 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/GemsCommand.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/commands/impl/GemsCommand.java @@ -43,11 +43,11 @@ private void sendHelpMenu(Player player) { player.sendMessage(" "); player.sendMessage(" §e§lGEMS CURRENCY§f | §7Subcommands"); player.sendMessage(" "); - player.sendMessage("§f • " + formatSyntax("gems add [amount]") + " §f§l» §7Give gems to a player."); - player.sendMessage("§f • " + formatSyntax("gems remove [amount]") + " §f§l» §7Take gems from a player."); - player.sendMessage("§f • " + formatSyntax("gems set [amount]") + " §f§l» §7Set gems for a player."); + player.sendMessage("§f • " + formatSyntax("gems add ") + " §f§l» §7Give gems to a player."); + player.sendMessage("§f • " + formatSyntax("gems remove ") + " §f§l» §7Take gems from a player."); + player.sendMessage("§f • " + formatSyntax("gems set ") + " §f§l» §7Set gems for a player."); player.sendMessage("§f • " + formatSyntax("gems reset ") + " §f§l» §7Reset gems for a player."); - player.sendMessage("§f • " + formatSyntax("gems giveall [amount]") + " §f§l» §7Give gems to everyone."); + player.sendMessage("§f • " + formatSyntax("gems giveall ") + " §f§l» §7Give gems to everyone."); player.sendMessage("§f • " + formatSyntax("gems resetall") + " §f§l» §7Reset everyone's balance."); player.sendMessage("§f • " + formatSyntax("gems help") + " §f§l» §7Prints this help message."); player.sendMessage(" "); @@ -78,7 +78,7 @@ private String formatToken(String token) { private boolean handleModify(Player sender, String[] args, boolean add) { if (args.length < 3) { - warn(sender, "Usage: /gems " + (add ? "add" : "remove") + " [amount]"); + warn(sender, "Usage: /gems " + (add ? "add" : "remove") + " "); return false; } @@ -104,7 +104,7 @@ private boolean handleModify(Player sender, String[] args, boolean add) { private boolean handleSet(Player sender, String[] args) { if (args.length < 3) { - warn(sender, "Usage: /gems set [amount]"); + warn(sender, "Usage: /gems set "); return false; } @@ -148,7 +148,7 @@ private boolean handleResetAll(Player sender) { private boolean handleGiveAll(Player sender, String[] args) { if (args.length < 2) { - warn(sender, "Usage: /gems giveall [amount]"); + warn(sender, "Usage: /gems giveall "); return false; } diff --git a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java index 3eca843a..ec333410 100644 --- a/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java +++ b/blighted-smp/src/main/java/fr/moussax/blightedSMP/engine/player/BlightedPlayer.java @@ -55,11 +55,14 @@ public final class BlightedPlayer { private int forgeFuel; /** - * Constructs and registers a player context for an online Bukkit player. + * Constructs a player context for an online Bukkit player. + * + *

This constructor is private; instances are instantiated exclusively through + * {@link #get(Player)} to ensure registration in the active players map.

* * @param player player represented by this context */ - public BlightedPlayer(Player player) { + private BlightedPlayer(Player player) { this.player = player; this.playerId = player.getUniqueId(); this.dataHandler = new PlayerDataHandler(playerId, player.getName()); diff --git a/pom.xml b/pom.xml index ed28b61d..a9911c3c 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,8 @@ UTF-8 26.2-R0.1-SNAPSHOT 1.18.46 + 3.45.3.0 + 5.10.2
@@ -55,6 +57,23 @@ ${lombok.version} provided + + org.xerial + sqlite-jdbc + ${sqlite-jdbc.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + From a5044584dbaed1598f0b9df3687a7adb7ded90e0 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 14:02:41 +0200 Subject: [PATCH 28/30] refactor(bedrock): code improvements from code review --- bedrock/pom.xml | 10 ++ .../ui/actionbar/ActionbarComposer.java | 30 ++++-- .../ui/actionbar/ActionbarSection.java | 15 ++- .../ui/actionbar/ActionbarService.java | 5 +- .../fr/moussax/bedrock/ui/book/BookMenu.java | 2 +- .../java/fr/moussax/bedrock/ui/menu/Menu.java | 6 +- .../bedrock/ui/menu/system/MenuSystem.java | 4 +- .../bedrock/ui/menu/types/PaginatedMenu.java | 13 ++- .../ui/actionbar/ActionbarComposerTest.java | 98 +++++++++++++++++++ 9 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 bedrock/src/test/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposerTest.java diff --git a/bedrock/pom.xml b/bedrock/pom.xml index 8bff1ce4..8ce34cc9 100644 --- a/bedrock/pom.xml +++ b/bedrock/pom.xml @@ -28,6 +28,16 @@ org.projectlombok lombok + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java index 3b92578d..0d5a8a81 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposer.java @@ -91,8 +91,10 @@ public void clearAlerts() { /** * Compiles the formatted action bar message string for a given player. * - *

If an active modal alert exists, its message is returned immediately. Otherwise, - * visible sections are sorted by priority and joined with the configured separator.

+ *

If an active modal alert exists, its message is returned immediately. Otherwise, if any + * visible exclusive sections produce non-empty text, the exclusive section with the highest priority + * is returned. If no exclusive section produces content, visible normal sections are sorted by + * rendering priority (lowest first) and joined with the configured separator.

* * @param player player for whom to compile action bar content * @return compiled action bar text, or an empty string if no content is visible @@ -108,15 +110,29 @@ public String compile(@NonNull Player player) { List visibleSections = sections.values().stream() .filter(section -> section.visibility().test(player)) - .sorted(Comparator.comparingInt(ActionbarSection::priority)) .toList(); - List evaluatedTexts = new ArrayList<>(visibleSections.size()); - for (ActionbarSection section : visibleSections) { - String text = evaluateSection(section, player); - if (section.exclusive() && text != null && !text.isEmpty()) { + List exclusiveSections = visibleSections.stream() + .filter(ActionbarSection::exclusive) + .sorted(Comparator.comparingInt(ActionbarSection::priority).reversed() + .thenComparing(ActionbarSection::id)) + .toList(); + + for (ActionbarSection exclusiveSection : exclusiveSections) { + String text = evaluateSection(exclusiveSection, player); + if (text != null && !text.isEmpty()) { return text; } + } + + List normalSections = visibleSections.stream() + .filter(section -> !section.exclusive()) + .sorted(Comparator.comparingInt(ActionbarSection::priority)) + .toList(); + + List evaluatedTexts = new ArrayList<>(normalSections.size()); + for (ActionbarSection section : normalSections) { + String text = evaluateSection(section, player); if (text != null && !text.isEmpty()) { evaluatedTexts.add(text); } diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarSection.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarSection.java index 4ce75f71..e77d18ad 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarSection.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarSection.java @@ -10,10 +10,15 @@ /** * Represents a modular content section rendered within a player's action bar. * + *

For normal (non-exclusive) sections, priority determines left-to-right rendering position + * (lower values render further to the left). For exclusive sections, priority defines override precedence + * (higher values take precedence over lower priority exclusive sections when multiple exclusive sections produce non-empty text).

+ * * @param id unique identifier for this section - * @param priority rendering priority; lower values render further to the left + * @param priority rendering priority for normal sections or precedence for exclusive sections * @param textSupplier function producing the display text for a player, or {@code null} if empty * @param visibility predicate determining whether this section should render for a player + * @param exclusive whether this section overrides non-exclusive sections when visible with non-empty text */ public record ActionbarSection( @NonNull String id, @@ -22,6 +27,14 @@ public record ActionbarSection( @NonNull Predicate visibility, boolean exclusive ) { + public ActionbarSection( + @NonNull String id, + int priority, + @NonNull Function textSupplier, + @NonNull Predicate visibility + ) { + this(id, priority, textSupplier, visibility, false); + } /** * Creates an action bar section that is always visible for all players. * diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarService.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarService.java index aedc7701..fab62c23 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarService.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/actionbar/ActionbarService.java @@ -181,10 +181,9 @@ public void renderPlayer(@NonNull Player player) { private void tickAll() { for (Player player : Bukkit.getOnlinePlayers()) { try { - // ponytail: kept — prevent single player evaluation error from cancelling remaining player renders renderPlayer(player); - } catch (Throwable throwable) { - Log.warn("ActionbarService", "Failed to render actionbar for " + player.getName() + ": " + throwable.getMessage()); + } catch (Exception exception) { + Log.warn("ActionbarService", "Failed to render actionbar for " + player.getName() + ": " + exception.getMessage()); } } } diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java index bae39c0f..6c90f182 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/book/BookMenu.java @@ -263,7 +263,7 @@ private void addParsedLeafComponents(BaseComponent component, TextComponent pare private TextComponent duplicateComponentWithText(TextComponent source, String text) { TextComponent copy = new TextComponent(text); - copy.setColor(source.getColor()); + copy.setColor(source.getColorRaw()); copy.setBold(source.isBoldRaw()); copy.setItalic(source.isItalicRaw()); copy.setUnderlined(source.isUnderlinedRaw()); diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java index 3234e8c4..5a0559a6 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/Menu.java @@ -723,7 +723,11 @@ public void refresh(@NonNull Player player) { boolean changed = false; for (int slot = 0; slot < size; slot++) { MenuSlot menuSlot = slots.get(slot); - ItemStack newItem = menuSlot != null ? menuSlot.item : null; + if (menuSlot == null) { + continue; + } + + ItemStack newItem = menuSlot.item; ItemStack currentItem = inventory.getItem(slot); if (!isSameItem(currentItem, newItem)) { diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/system/MenuSystem.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/system/MenuSystem.java index c4357a04..9b7aff8e 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/system/MenuSystem.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/system/MenuSystem.java @@ -2,6 +2,7 @@ import fr.moussax.bedrock.ui.menu.Menu; import fr.moussax.bedrock.ui.menu.TickableMenu; +import fr.moussax.bedrock.utils.debug.Log; import lombok.Getter; import lombok.Setter; import org.bukkit.entity.Player; @@ -55,7 +56,8 @@ private void tickActiveMenus() { if (player != null && player.isOnline()) { tickable.onTick(player); } - } catch (Throwable _) { + } catch (Exception exception) { + Log.warn("MenuSystem", "Failed to tick menu for player " + entry.getKey() + ": " + exception.getMessage()); } } } diff --git a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java index 53b444fd..9661dca2 100644 --- a/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java +++ b/bedrock/src/main/java/fr/moussax/bedrock/ui/menu/types/PaginatedMenu.java @@ -18,7 +18,6 @@ *

Subclasses provide the total item count, item contents, and optional * click handling through {@link #onItemClick(Player, int, ClickType)}.

*/ -@Getter public abstract class PaginatedMenu extends Menu { public static final int[] INNER_GRID_SLOTS = { @@ -28,9 +27,19 @@ public abstract class PaginatedMenu extends Menu { 37, 38, 39, 40, 41, 42, 43 }; + @Getter protected int currentPage = 0; protected int totalItems = 0; + /** + * Returns the total number of items stored for the current page rendering. + * + * @return stored total item count + */ + public int getTotalItemsCount() { + return totalItems; + } + /** * Creates a paginated menu. * @@ -125,7 +134,7 @@ protected int getItemsPerPage() { @Override public void build(@NonNull Player viewer) { totalItems = Math.max(0, getTotalItems(viewer)); - clearInventory(); + slots.clear(); int closeSlot = size - 5; int backSlot = size - 6; diff --git a/bedrock/src/test/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposerTest.java b/bedrock/src/test/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposerTest.java new file mode 100644 index 00000000..9c8d7b97 --- /dev/null +++ b/bedrock/src/test/java/fr/moussax/bedrock/ui/actionbar/ActionbarComposerTest.java @@ -0,0 +1,98 @@ +package fr.moussax.bedrock.ui.actionbar; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ActionbarComposerTest { + + @Test + @DisplayName("Expects the highest priority exclusive section to win over lower priority and normal sections") + void testHighestPriorityExclusiveWins() { + ActionbarComposer composer = new ActionbarComposer(); + + ActionbarSection lowPriorityExclusive = ActionbarSection.exclusiveOf( + "low_exclusive", + 10, + _ -> "Low Priority Exclusive", + _ -> true + ); + + ActionbarSection highPriorityExclusive = ActionbarSection.exclusiveOf( + "high_exclusive", + 100, + _ -> "High Priority Exclusive", + _ -> true + ); + + ActionbarSection normalSection = ActionbarSection.of( + "normal", + 0, + _ -> "Normal Text", + _ -> true + ); + + composer.registerSection(lowPriorityExclusive); + composer.registerSection(highPriorityExclusive); + composer.registerSection(normalSection); + + String result = composer.compile(null); + + assertEquals("High Priority Exclusive", result); + } + + @Test + @DisplayName("Expects an empty exclusive section to fall back to the next available exclusive section") + void testEmptyExclusiveFallsBack() { + ActionbarComposer composer = new ActionbarComposer(); + + ActionbarSection emptyHighExclusive = ActionbarSection.exclusiveOf( + "empty_high", + 200, + _ -> "", + _ -> true + ); + + ActionbarSection activeLowExclusive = ActionbarSection.exclusiveOf( + "active_low", + 50, + _ -> "Active Low Exclusive", + _ -> true + ); + + composer.registerSection(emptyHighExclusive); + composer.registerSection(activeLowExclusive); + + String result = composer.compile(null); + + assertEquals("Active Low Exclusive", result); + } + + @Test + @DisplayName("Expects normal sections to render in ascending priority order when no exclusive section is active") + void testNormalSectionsFollowPriority() { + ActionbarComposer composer = new ActionbarComposer(); + + ActionbarSection firstNormal = ActionbarSection.of( + "first", + 10, + _ -> "First Normal", + _ -> true + ); + + ActionbarSection secondNormal = ActionbarSection.of( + "second", + 20, + _ -> "Second Normal", + _ -> true + ); + + composer.registerSection(secondNormal); + composer.registerSection(firstNormal); + + String result = composer.compile(null); + + assertEquals("First Normal Second Normal", result); + } +} From ab0e0be74a646e05a87743b609f9a9ebf65ea0ab Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 14:05:20 +0200 Subject: [PATCH 29/30] refactor(bedrock): code improvements from code review --- blighted-mod/pom.xml | 4 ++++ .../commands/impl/ReportCommand.java | 13 ++++++++-- .../commands/impl/SetSlowmodeCommand.java | 7 +++++- .../moderator/BlightedModerator.java | 23 ++++++++++++++---- .../moderator/ModerationGlowHelper.java | 24 ++++++++++++++++--- .../moderator/ModerationManager.java | 6 ++++- .../listeners/ModerationListener.java | 12 ++++++---- .../moderator/menus/ReportsCenterMenu.java | 18 +++++++++++--- .../moderator/menus/SanctionsMenu.java | 22 +++++++++++------ .../moderator/punishments/DurationParser.java | 2 +- .../punishments/PunishmentManager.java | 20 ++++++++++++++-- 11 files changed, 122 insertions(+), 29 deletions(-) diff --git a/blighted-mod/pom.xml b/blighted-mod/pom.xml index d9185ba5..d0b6a1c4 100644 --- a/blighted-mod/pom.xml +++ b/blighted-mod/pom.xml @@ -33,6 +33,10 @@ org.projectlombok lombok + + org.xerial + sqlite-jdbc + diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java index 1fc03cd5..0971e069 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/ReportCommand.java @@ -37,13 +37,22 @@ protected boolean execute(Player player, Command command, String label, String[] } if (subAction.equalsIgnoreCase("submit") && arguments.length >= 3) { - String targetName = arguments[1]; + Player target = requireTarget(player, arguments[1]); + if (target == null) { + return false; + } + + if (target.equals(player)) { + warn(player, "You cannot report yourself."); + return false; + } + String reason = arguments[2]; String chatMessage = arguments.length > 3 ? String.join(" ", Arrays.copyOfRange(arguments, 3, arguments.length)) : "No message content"; - submitDirectReport(player, targetName, reason, chatMessage); + submitDirectReport(player, target.getName(), reason, chatMessage); return true; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java index 6d9dc4b5..02f6cd17 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/SetSlowmodeCommand.java @@ -31,7 +31,12 @@ protected boolean executeModeration(Player moderator, Command command, String la } Long secondsDuration = DurationParser.parseDurationSeconds(input); - if (secondsDuration == null || secondsDuration <= 0) { + if (secondsDuration == null) { + warn(moderator, "Invalid duration format. Usage: /slowmode "); + return true; + } + + if (secondsDuration <= 0) { getModerationManager().setSlowmodeDelaySeconds(0); getModerationManager().broadcastToModerators(" §d§lSTAFF! §9" + moderator.getName() + " §ehas disabled §fChat Slowmode§e."); return true; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java index 09a9b017..c4c640fb 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/BlightedModerator.java @@ -31,6 +31,11 @@ public final class BlightedModerator { private float savedExperience; private int savedLevel; private Collection savedPotionEffects; + private double savedHealth; + private int savedFoodLevel; + private boolean savedInvulnerable; + private boolean savedAllowFlight; + private boolean savedFlying; public BlightedModerator(Player player) { this.player = player; @@ -84,6 +89,11 @@ private void saveState() { this.savedExperience = player.getExp(); this.savedLevel = player.getLevel(); this.savedPotionEffects = player.getActivePotionEffects(); + this.savedHealth = player.getHealth(); + this.savedFoodLevel = player.getFoodLevel(); + this.savedInvulnerable = player.isInvulnerable(); + this.savedAllowFlight = player.getAllowFlight(); + this.savedFlying = player.isFlying(); } private void restoreState() { @@ -93,15 +103,20 @@ private void restoreState() { inventory.setItemInOffHand(savedOffHand); player.setGameMode(savedGameMode); + player.setHealth(savedHealth); + player.setFoodLevel(savedFoodLevel); player.setExp(savedExperience); player.setLevel(savedLevel); player.getActivePotionEffects().forEach(effect -> player.removePotionEffect(effect.getType())); player.addPotionEffects(savedPotionEffects); - - player.setInvulnerable(false); - player.setAllowFlight(false); - player.setFlying(false); + player.setInvulnerable(savedInvulnerable); + player.setAllowFlight(savedAllowFlight); + if (savedAllowFlight) { + player.setFlying(savedFlying); + } else { + player.setFlying(false); + } } private void clearPlayerState() { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationGlowHelper.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationGlowHelper.java index bf37773c..cae0bb9b 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationGlowHelper.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationGlowHelper.java @@ -11,13 +11,18 @@ import org.bukkit.scoreboard.Scoreboard; import org.bukkit.scoreboard.Team; +import org.bukkit.scoreboard.ScoreboardManager; + import java.util.ArrayList; import java.util.List; -import java.util.Objects; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; public final class ModerationGlowHelper { private static final String GLOW_TEAM_NAME = "mod_glow_pink"; + private static final Map ORIGINAL_SCOREBOARDS = new ConcurrentHashMap<>(); private ModerationGlowHelper() { } @@ -27,9 +32,15 @@ public static void applyPinkGlow(Player moderator, Player target) { return; } + ScoreboardManager manager = Bukkit.getScoreboardManager(); + if (manager == null) { + return; + } + Scoreboard scoreboard = moderator.getScoreboard(); - if (scoreboard.equals(Objects.requireNonNull(Bukkit.getScoreboardManager()).getMainScoreboard())) { - scoreboard = Bukkit.getScoreboardManager().getNewScoreboard(); + if (scoreboard.equals(manager.getMainScoreboard())) { + ORIGINAL_SCOREBOARDS.putIfAbsent(moderator.getUniqueId(), scoreboard); + scoreboard = manager.getNewScoreboard(); moderator.setScoreboard(scoreboard); } @@ -60,6 +71,13 @@ public static void removePinkGlow(Player moderator, Player target) { if (target.isOnline()) { sendGlowPacket(moderator, target, false); } + + if (team == null || team.getEntries().isEmpty()) { + Scoreboard originalScoreboard = ORIGINAL_SCOREBOARDS.remove(moderator.getUniqueId()); + if (originalScoreboard != null) { + moderator.setScoreboard(originalScoreboard); + } + } } private static void sendGlowPacket(Player moderator, Player target, boolean glowing) { diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java index ce396c5e..129d137b 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/ModerationManager.java @@ -22,7 +22,7 @@ public enum ChatChannel { @Getter private final PunishmentManager punishmentManager; - private final Map moderators = new HashMap<>(); + private final Map moderators = new ConcurrentHashMap<>(); private final Set frozenPlayers = new HashSet<>(); private final Map frozenLocations = new ConcurrentHashMap<>(); private final Map lastChatTimestamps = new ConcurrentHashMap<>(); @@ -55,6 +55,10 @@ public BlightedModerator getModerator(Player player) { return moderators.computeIfAbsent(player.getUniqueId(), _ -> new BlightedModerator(player)); } + public BlightedModerator getModeratorIfPresent(Player player) { + return moderators.get(player.getUniqueId()); + } + public boolean isModerator(Player player) { return player.hasPermission(PluginPermissions.MODERATOR.toString()); } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java index 5acfee72..cc52ed13 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/listeners/ModerationListener.java @@ -196,7 +196,7 @@ public void onPlayerQuit(PlayerQuitEvent event) { quittingPlayer.getName(), PunishmentData.PunishmentType.BAN, reason, - quittingPlayer.getUniqueId(), + PunishmentManager.CONSOLE_UUID, "CONSOLE", null, ipAddress @@ -214,10 +214,11 @@ public void onServerListPing(ServerListPingEvent event) { Iterator iterator = event.iterator(); while (iterator.hasNext()) { Player player = iterator.next(); - if (moderationManager.isModerator(player) - && moderationManager.getModerator(player) instanceof BlightedModerator moderator - && moderator.isVanished()) { - iterator.remove(); + if (moderationManager.isModerator(player)) { + BlightedModerator moderator = moderationManager.getModeratorIfPresent(player); + if (moderator != null && moderator.isVanished()) { + iterator.remove(); + } } } } @@ -339,6 +340,7 @@ public void onModeratorEntityDamage(EntityDamageByEntityEvent event) { Material tool = attacker.getInventory().getItemInMainHand().getType(); if (tool == Material.STICK && event.getEntity() instanceof Player victim) { moderationManager.getModerator(attacker).setTargetPlayer(victim); + event.setDamage(0); return; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java index 3688d82a..23f4b561 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/ReportsCenterMenu.java @@ -117,14 +117,22 @@ private void renderHeader() { @Override protected void onItemLeftClick(@NonNull Player moderator, int index) { - ReportData report = ReportManager.getInstance().getActiveReports().get(index); + List reports = ReportManager.getInstance().getActiveReports(); + if (index < 0 || index >= reports.size()) { + return; + } + ReportData report = reports.get(index); close(); moderator.performCommand("mtp " + report.targetName()); } @Override protected void onItemRightClick(@NonNull Player moderator, int index) { - ReportData report = ReportManager.getInstance().getActiveReports().get(index); + List reports = ReportManager.getInstance().getActiveReports(); + if (index < 0 || index >= reports.size()) { + return; + } + ReportData report = reports.get(index); ReportManager.getInstance().dismissReport(report.id()); inform(moderator, " §eYou dismissed §creport #" + report.id() + " §eagainst §d" + report.targetName() + "§e."); refresh(moderator); @@ -132,7 +140,11 @@ protected void onItemRightClick(@NonNull Player moderator, int index) { @Override protected void onItemShiftClick(@NonNull Player moderator, int index) { - ReportData report = ReportManager.getInstance().getActiveReports().get(index); + List reports = ReportManager.getInstance().getActiveReports(); + if (index < 0 || index >= reports.size()) { + return; + } + ReportData report = reports.get(index); openReportBook(moderator, report); } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java index 5b38373c..92e28f6f 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/menus/SanctionsMenu.java @@ -49,6 +49,7 @@ public SanctionFilter previous() { private final String targetName; private final List punishments; private SanctionFilter currentFilter = SanctionFilter.ALL; + private List cachedFilteredPunishments; public SanctionsMenu(String targetName, List punishments) { super(targetName + "'s sanctions", 54); @@ -61,9 +62,8 @@ public void onTick(Player player) { refresh(player); } - - private List getFilteredPunishments() { - return punishments.stream() + private void updateFilteredPunishmentsCache() { + this.cachedFilteredPunishments = punishments.stream() .filter(punishment -> switch (currentFilter) { case ALL -> true; case ACTIVE -> punishment.active() && !punishment.isExpired(); @@ -77,17 +77,22 @@ private List getFilteredPunishments() { @Override protected int getTotalItems(@NonNull Player player) { - return getFilteredPunishments().size(); + if (cachedFilteredPunishments == null) { + updateFilteredPunishmentsCache(); + } + return cachedFilteredPunishments.size(); } @Override protected ItemStack getItem(@NonNull Player player, int index) { - List filtered = getFilteredPunishments(); - if (index >= filtered.size()) { + if (cachedFilteredPunishments == null) { + updateFilteredPunishmentsCache(); + } + if (index >= cachedFilteredPunishments.size()) { return new ItemStack(Material.AIR); } - PunishmentData punishment = filtered.get(index); + PunishmentData punishment = cachedFilteredPunishments.get(index); String typeTitle = switch (punishment.type()) { case BAN -> "§c§lBAN"; @@ -186,6 +191,7 @@ protected ItemStack getEmptyStateItem(@NonNull Player player) { @Override public void build(@NonNull Player viewer) { + updateFilteredPunishmentsCache(); super.build(viewer); renderTargetHead(); renderFilterHopper(); @@ -233,11 +239,13 @@ private void renderFilterHopper() { (player, _) -> { currentFilter = currentFilter.next(); currentPage = 0; + updateFilteredPunishmentsCache(); refresh(player); }, (player, _) -> { currentFilter = currentFilter.previous(); currentPage = 0; + updateFilteredPunishmentsCache(); refresh(player); } ); diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java index 666fcdd4..966538fe 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/DurationParser.java @@ -61,7 +61,7 @@ public static String formatDuration(String input) { default -> ""; }; - int parsedAmount = Integer.parseInt(value); + long parsedAmount = Long.parseLong(value); if (parsedAmount > 1) { unitName += "s"; } diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java index a1a16888..55d66954 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java @@ -76,6 +76,22 @@ public void removeIpPunishment(String ipAddress) { } } + private void deactivatePunishment(int id) { + String query = """ + UPDATE punishments SET is_active = 0 + WHERE id = ? AND is_active = 1 + """; + + synchronized (connection) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setInt(1, id); + statement.executeUpdate(); + } catch (SQLException exception) { + throw new RuntimeException("Failed to deactivate punishment", exception); + } + } + } + public PunishmentData getActivePunishment(UUID playerUuid, PunishmentData.PunishmentType type) { String query = """ SELECT * FROM punishments @@ -99,7 +115,7 @@ public PunishmentData getActivePunishment(UUID playerUuid, PunishmentData.Punish } if (punishment.isExpired()) { - removePunishment(playerUuid, type); + deactivatePunishment(punishment.id()); return null; } return punishment; @@ -127,7 +143,7 @@ public PunishmentData getActiveIpBan(String ipAddress) { } if (punishment.isExpired()) { - removeIpPunishment(ipAddress); + deactivatePunishment(punishment.id()); return null; } return punishment; From a70761d7972ea9889a1291f81c96338252aadca9 Mon Sep 17 00:00:00 2001 From: Moussa Date: Sat, 5 Sep 2026 14:19:28 +0200 Subject: [PATCH 30/30] refactor(bedrock): code improvements from code review --- .../blightedMod/commands/impl/BanCommand.java | 38 +++++++++++-------- .../punishments/PunishmentManager.java | 38 +++++++++++++++++++ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java index 24f38e08..913e0835 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/commands/impl/BanCommand.java @@ -5,12 +5,12 @@ import fr.moussax.blightedMod.moderator.punishments.DurationParser; import fr.moussax.blightedMod.moderator.punishments.PunishmentArguments; import fr.moussax.blightedMod.moderator.punishments.PunishmentData; -import fr.moussax.blightedMod.moderator.punishments.PunishmentManager; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import java.util.Locale; +import java.util.UUID; import static fr.moussax.bedrock.text.Messenger.warn; @@ -84,19 +84,17 @@ private boolean handleUnban(CommandSender moderator, String[] arguments) { return false; } - Player target = requireTarget(moderator, arguments[0]); - if (target == null) { - return false; - } + String targetName = arguments[0]; + UUID targetId = getPunishmentManager().resolveBannedUuidByName(targetName); - if (!getPunishmentManager().isBanned(target.getUniqueId())) { - warn(moderator, target.getName() + " is not banned."); + if (targetId == null) { + warn(moderator, targetName + " is not banned."); return false; } - getPunishmentManager().removePunishment(target.getUniqueId(), PunishmentData.PunishmentType.BAN); + getPunishmentManager().removePunishment(targetId, PunishmentData.PunishmentType.BAN); - String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unbanned §d" + target.getName() + "§e."; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unbanned §d" + targetName + "§e."; getModerationManager().broadcastToModerators(notification); return true; @@ -153,21 +151,29 @@ private boolean handleUnbanIp(CommandSender moderator, String[] arguments) { return false; } - Player target = requireTarget(moderator, arguments[0]); - if (target == null) { - return false; + String input = arguments[0]; + String ipAddress = null; + String targetName = input; + + if (getPunishmentManager().isIpBanned(input)) { + ipAddress = input; + } else { + for (PunishmentData punishment : getPunishmentManager().getAllPunishments(targetName)) { + if (punishment.ipAddress() != null && !punishment.ipAddress().isEmpty() && !"0.0.0.0".equals(punishment.ipAddress())) { + ipAddress = punishment.ipAddress(); + break; + } + } } - String ipAddress = PunishmentManager.getPlayerIp(target); - - if (!getPunishmentManager().isIpBanned(ipAddress)) { + if (ipAddress == null || !getPunishmentManager().isIpBanned(ipAddress)) { warn(moderator, "This IP is not banned."); return false; } getPunishmentManager().removeIpPunishment(ipAddress); - String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unbanned IP for §d" + target.getName() + "§e."; + String notification = " §d§lSTAFF! §9" + moderator.getName() + "§e unbanned IP for §d" + targetName + "§e."; getModerationManager().broadcastToModerators(notification); return true; diff --git a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java index 55d66954..b5db1752 100644 --- a/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java +++ b/blighted-mod/src/main/java/fr/moussax/blightedMod/moderator/punishments/PunishmentManager.java @@ -149,6 +149,44 @@ public PunishmentData getActiveIpBan(String ipAddress) { return punishment; } + /** + * Resolves the UUID of an actively banned player by name, using persisted punishment + * data instead of a live Mojang lookup. + * + *

Used to unban players who are offline — {@code requireTarget} only resolves + * online players, but a banned player is disconnected by the time {@code /unban} + * is run against them.

+ * + * @param playerName the banned player's name + * @return the player's UUID, or {@code null} if no active ban matches + */ + public UUID resolveBannedUuidByName(String playerName) { + String query = """ + SELECT * FROM punishments + WHERE LOWER(player_name) = LOWER(?) AND punishment_type = 'BAN' AND is_active = 1 + ORDER BY created_at DESC LIMIT 1 + """; + + PunishmentData punishment; + synchronized (connection) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, playerName); + try (ResultSet resultSet = statement.executeQuery()) { + if (!resultSet.next()) return null; + punishment = mapResultSet(resultSet); + } + } catch (SQLException exception) { + throw new RuntimeException("Failed to resolve banned player", exception); + } + } + + if (punishment.isExpired()) { + deactivatePunishment(punishment.id()); + return null; + } + return punishment.playerUuid(); + } + public boolean isMuted(UUID playerUuid) { return getActivePunishment(playerUuid, PunishmentData.PunishmentType.MUTE) != null; }