diff --git a/README.md b/README.md index 4e7180a..f10bfa8 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,17 @@ original location, placed back into survival mode, and their original inventory - `/spawn` - Enter admin mode at the current world's spawn point - `/spawn ` - Enter admin mode at the provided world's spawn point + +### Report System + +- `/report ` — Players send a report with their current location and a timestamp. Admins receive a broadcast where the coordinates are clickable to spectate in admin mode. +- `/reports [page]` — View open reports (5 per page). Each entry shows player, coords, and world. Hovering shows the reason, time, and the report ID. The coords are clickable to spectate in admin mode, and an inline `[Resolve]` button lets admins resolve the report. +- `/reports resolve ` — Resolve by ID (or click the inline `[Resolve]` button). + +Notes: +- Cooldown: players must wait 5 minutes between `/report` submissions. +- Permission: `admintoolbox.reports` is required to view/resolve reports. + #### Navigation - `/back` - Move to previous location in teleport history @@ -91,6 +102,7 @@ screen sharing or live-streaming gameplay. | `admintoolbox.reveal` | `/reveal` | Reveal while spectating in admin mode | | `admintoolbox.yell` | `/yell` | Show titles to other players | | `admintoolbox.freeze` | `/freeze` | Freeze and unfreeze players | +| `admintoolbox.reports` | `/reports` | View and manage player reports | | `admintoolbox.spawn` | `/spawn` | Spectate at current world spawn | | `admintoolbox.spawn.all` | `/spawn [world]` | Spectate at all world spawns | | `admintoolbox.broadcast.receive` | | Receive alerts about other admins' actions | diff --git a/src/main/java/org/modernbeta/admintoolbox/AdminToolboxPlugin.java b/src/main/java/org/modernbeta/admintoolbox/AdminToolboxPlugin.java index 65e375f..a4d8767 100644 --- a/src/main/java/org/modernbeta/admintoolbox/AdminToolboxPlugin.java +++ b/src/main/java/org/modernbeta/admintoolbox/AdminToolboxPlugin.java @@ -10,6 +10,7 @@ import org.bukkit.plugin.java.JavaPlugin; import org.modernbeta.admintoolbox.commands.*; import org.modernbeta.admintoolbox.managers.FreezeManager; +import org.modernbeta.admintoolbox.managers.ReportManager; import org.modernbeta.admintoolbox.managers.admin.AdminManager; import javax.annotation.Nullable; @@ -24,6 +25,7 @@ public class AdminToolboxPlugin extends JavaPlugin { AdminManager adminManager; FreezeManager freezeManager; + ReportManager reportManager; PermissionAudience broadcastAudience; @@ -35,6 +37,10 @@ public class AdminToolboxPlugin extends JavaPlugin { private static final String ADMIN_STATE_CONFIG_FILENAME = "admin-state.yml"; + private File reportsConfigFile; + private FileConfiguration reportsConfig; + private static final String REPORTS_CONFIG_FILENAME = "reports.yml"; + private static final String BROADCAST_AUDIENCE_PERMISSION = "admintoolbox.broadcast.receive"; public static final String BROADCAST_EXEMPT_PERMISSION = "admintoolbox.broadcast.exempt"; @@ -47,14 +53,19 @@ public void onEnable() { this.broadcastAudience = new PermissionAudience(BROADCAST_AUDIENCE_PERMISSION); + createAdminStateConfig(); + this.adminStateConfig = getAdminStateConfig(); + + createReportsConfig(); + this.reportsConfig = getReportsConfig(); + + this.reportManager = new ReportManager(); + { RegisteredServiceProvider provider = Bukkit.getServicesManager().getRegistration(LuckPerms.class); if (provider != null) this.luckPermsAPI = provider.getProvider(); } - createAdminStateConfig(); - this.adminStateConfig = getAdminStateConfig(); - getServer().getPluginManager().registerEvents(adminManager, this); getServer().getPluginManager().registerEvents(freezeManager, this); @@ -68,6 +79,8 @@ public void onEnable() { getCommand("yell").setExecutor(new YellCommand()); getCommand("spawn").setExecutor(new SpawnCommand()); getCommand("streamermode").setExecutor(new StreamerModeCommand()); + getCommand("report").setExecutor(new ReportCommand()); + getCommand("reports").setExecutor(new ReportsCommand()); initializeConfig(); @@ -89,6 +102,41 @@ private void createAdminStateConfig() { this.adminStateConfig = YamlConfiguration.loadConfiguration(adminStateConfigFile); } + private void createReportsConfig() { + this.reportsConfigFile = new File(getDataFolder(), REPORTS_CONFIG_FILENAME); + if (!this.reportsConfigFile.exists()) { + this.reportsConfigFile.getParentFile().mkdirs(); + if (getResource(REPORTS_CONFIG_FILENAME) != null) { + saveResource(REPORTS_CONFIG_FILENAME, false); + } else { + try { + this.reportsConfigFile.createNewFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + this.reportsConfig = YamlConfiguration.loadConfiguration(reportsConfigFile); + + ConfigurationSection existing = this.reportsConfig.getConfigurationSection("reports"); + ConfigurationSection fromAdmin = this.adminStateConfig.getConfigurationSection("reports"); + if ((existing == null || existing.getKeys(false).isEmpty()) && fromAdmin != null) { + ConfigurationSection dest = this.reportsConfig.createSection("reports"); + for (String key : fromAdmin.getKeys(false)) { + ConfigurationSection child = fromAdmin.getConfigurationSection(key); + if (child != null) { + dest.createSection(key, child.getValues(true)); + } + } + try { + this.reportsConfig.save(reportsConfigFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + public FileConfiguration getAdminStateConfig() { // TODO: this re-reads the file from file system every time, should not be needed // but we have run into some desynced state somehow. Figure out why! @@ -109,6 +157,23 @@ public void saveAdminStateConfig() { } } + public FileConfiguration getReportsConfig() { + try { + this.reportsConfig.load(reportsConfigFile); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this.reportsConfig; + } + + public void saveReportsConfig() { + try { + this.reportsConfig.save(reportsConfigFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + public static AdminToolboxPlugin getInstance() { return instance; } @@ -121,6 +186,10 @@ public FreezeManager getFreezeManager() { return freezeManager; } + public ReportManager getReportManager() { + return reportManager; + } + public PermissionAudience getAdminAudience() { return broadcastAudience; } diff --git a/src/main/java/org/modernbeta/admintoolbox/commands/ReportCommand.java b/src/main/java/org/modernbeta/admintoolbox/commands/ReportCommand.java new file mode 100644 index 0000000..dc71b9f --- /dev/null +++ b/src/main/java/org/modernbeta/admintoolbox/commands/ReportCommand.java @@ -0,0 +1,118 @@ +package org.modernbeta.admintoolbox.commands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.modernbeta.admintoolbox.AdminToolboxPlugin; +import org.modernbeta.admintoolbox.models.Report; +import org.modernbeta.admintoolbox.utils.LocationUtils; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +public class ReportCommand implements CommandExecutor, TabCompleter { + private final AdminToolboxPlugin plugin = AdminToolboxPlugin.getInstance(); + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final long COOLDOWN_MINUTES = 5; + private final Map cooldowns = new HashMap<>(); + + private static final List REPORT_SUGGESTIONS = List.of( + "griefing", + "stealing", + "bug" + ); + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player player)) { + sender.sendRichMessage("You must be a player to use this command."); + return true; + } + + if (args.length == 0) { + player.sendRichMessage("Please provide a reason for your report. Usage: /report "); + return true; + } + + LocalDateTime lastReport = cooldowns.get(player.getUniqueId()); + if (lastReport != null) { + Duration timeSince = Duration.between(lastReport, LocalDateTime.now()); + if (timeSince.toMinutes() < COOLDOWN_MINUTES) { + long remainingSeconds = COOLDOWN_MINUTES * 60 - timeSince.getSeconds(); + long minutes = remainingSeconds / 60; + long seconds = remainingSeconds % 60; + player.sendRichMessage("You must wait