diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c336766..11ad4b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ "**" ] + branches: [ "main" ] pull_request: branches: [ "**" ] diff --git a/pom.xml b/pom.xml index e14ca17..f92de2d 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ 1.8 + 5.15.2 UTF-8 @@ -56,6 +57,10 @@ org.apache.maven.plugins maven-resources-plugin 3.3.1 + + ${project.build.sourceEncoding} + ${project.build.sourceEncoding} + process-resources @@ -74,6 +79,14 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar --enable-native-access=ALL-UNNAMED -Xshare:off + + @@ -85,10 +98,6 @@ - - papermc-repo - https://papermc.io/repo/repository/maven-public/ - sonatype https://oss.sonatype.org/content/groups/public/ @@ -169,5 +178,29 @@ 2.11.6 provided + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.slf4j + slf4j-nop + 1.7.36 + test + + + org.xerial + sqlite-jdbc + 3.49.1.0 + test + diff --git a/src/main/java/dev/noah/perplayerkit/KitManager.java b/src/main/java/dev/noah/perplayerkit/KitManager.java index 745afe3..85e58aa 100644 --- a/src/main/java/dev/noah/perplayerkit/KitManager.java +++ b/src/main/java/dev/noah/perplayerkit/KitManager.java @@ -31,11 +31,12 @@ import java.io.IOException; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class KitManager { private static KitManager instance; private final PerPlayerKit plugin; - private final HashMap kitByKitIDMap; + private final Map kitByKitIDMap; private final HashMap lastKitUsedByPlayer; private final List publicKitList; @@ -43,7 +44,7 @@ public KitManager(PerPlayerKit plugin) { this.plugin = plugin; lastKitUsedByPlayer = new HashMap<>(); publicKitList = new ArrayList<>(); - kitByKitIDMap = new HashMap<>(); + kitByKitIDMap = new ConcurrentHashMap<>(); instance = this; } @@ -58,6 +59,15 @@ public ItemStack[] getItemStackArrayById(String id) { return kitByKitIDMap.get(id); } + private void cacheKit(String id, ItemStack[] kit) { + if (kit == null) { + kitByKitIDMap.remove(id); + return; + } + + kitByKitIDMap.put(id, kit); + } + public List getPublicKitList() { return publicKitList; } @@ -104,7 +114,7 @@ public boolean savekit(UUID uuid, int slot, ItemStack[] kit) { } } - kitByKitIDMap.put(IDUtil.getPlayerKitId(uuid, slot), kit); + cacheKit(IDUtil.getPlayerKitId(uuid, slot), kit); player.sendMessage(ChatColor.GREEN + "Kit " + slot + " saved!"); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> savePlayerKitToDB(uuid, slot)); @@ -149,7 +159,7 @@ public boolean savePublicKit(Player player, String publickit, ItemStack[] kit) { } } - kitByKitIDMap.put(IDUtil.getPublicKitId(publickit), kit); + cacheKit(IDUtil.getPublicKitId(publickit), kit); player.sendMessage(ChatColor.GREEN + "Public Kit " + publickit + " saved!"); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> savePublicKitToDB(publickit)); @@ -192,7 +202,7 @@ public boolean savePublicKit(String id, ItemStack[] kit) { } } - kitByKitIDMap.put(IDUtil.getPublicKitId(id), kit); + cacheKit(IDUtil.getPublicKitId(id), kit); return true; } return false; @@ -212,7 +222,7 @@ public boolean saveEC(UUID uuid, int slot, ItemStack[] kit) { } if (notEmpty) { - kitByKitIDMap.put(IDUtil.getECId(uuid, slot), kit); + cacheKit(IDUtil.getECId(uuid, slot), kit); player.sendMessage(ChatColor.GREEN + "Enderchest " + slot + " saved!"); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> saveEnderchestToDB(uuid, slot)); return true; @@ -237,7 +247,7 @@ public boolean saveECSilent(UUID uuid, int slot, ItemStack[] kit) { return false; } - kitByKitIDMap.put(IDUtil.getECId(uuid, slot), kit); + cacheKit(IDUtil.getECId(uuid, slot), kit); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> saveEnderchestToDB(uuid, slot)); return true; } @@ -269,7 +279,7 @@ public boolean savekit(UUID uuid, int slot, ItemStack[] kit, boolean silent) { kit[39] = null; } - kitByKitIDMap.put(IDUtil.getPlayerKitId(uuid, slot), ItemFilter.get().filterItemStack(kit)); + cacheKit(IDUtil.getPlayerKitId(uuid, slot), ItemFilter.get().filterItemStack(kit)); Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> savePlayerKitToDB(uuid, slot)); return true; } else { @@ -436,7 +446,8 @@ public void loadPlayerDataFromDB(UUID uuid) { if (!data.equalsIgnoreCase("error")) { try { ItemStack[] kit = Serializer.itemStackArrayFromBase64(data); - kitByKitIDMap.put(IDUtil.getPlayerKitId(uuid, slot), ItemFilter.get().filterItemStack(Serializer.itemStackArrayFromBase64(data))); + cacheKit(IDUtil.getPlayerKitId(uuid, slot), + ItemFilter.get().filterItemStack(kit)); } catch (IOException ignored) { } } @@ -446,7 +457,8 @@ public void loadPlayerDataFromDB(UUID uuid) { if (!data.equalsIgnoreCase("error")) { try { ItemStack[] kit = Serializer.itemStackArrayFromBase64(data); - kitByKitIDMap.put(IDUtil.getECId(uuid, slot), ItemFilter.get().filterItemStack(Serializer.itemStackArrayFromBase64(data))); + cacheKit(IDUtil.getECId(uuid, slot), + ItemFilter.get().filterItemStack(kit)); } catch (IOException ignored) { } } @@ -486,7 +498,7 @@ public void loadPublicKitFromDB(String id) { if (!data.equalsIgnoreCase("error")) { try { ItemStack[] kit = Serializer.itemStackArrayFromBase64(data); - kitByKitIDMap.put(IDUtil.getPublicKitId(id), ItemFilter.get().filterItemStack(kit)); + cacheKit(IDUtil.getPublicKitId(id), ItemFilter.get().filterItemStack(kit)); } catch (IOException ignored) { plugin.getLogger().info("Error loading public kit " + id); } @@ -544,4 +556,4 @@ private void applyKitLoadEffects(Player player, boolean isEnderChest) { } } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/noah/perplayerkit/PerPlayerKit.java b/src/main/java/dev/noah/perplayerkit/PerPlayerKit.java index 2742fd0..73a08e3 100644 --- a/src/main/java/dev/noah/perplayerkit/PerPlayerKit.java +++ b/src/main/java/dev/noah/perplayerkit/PerPlayerKit.java @@ -18,11 +18,27 @@ */ package dev.noah.perplayerkit; -import dev.noah.perplayerkit.commands.*; -import dev.noah.perplayerkit.commands.extracommands.HealCommand; -import dev.noah.perplayerkit.commands.extracommands.RepairCommand; -import dev.noah.perplayerkit.commands.tabcompleters.ECSlotTabCompleter; -import dev.noah.perplayerkit.commands.tabcompleters.KitSlotTabCompleter; +import dev.noah.perplayerkit.commands.admin.AboutCommandListener; +import dev.noah.perplayerkit.commands.admin.KitRoomCommand; +import dev.noah.perplayerkit.commands.admin.PerPlayerKitCommand; +import dev.noah.perplayerkit.commands.admin.SavePublicKitCommand; +import dev.noah.perplayerkit.commands.completion.ECSlotTabCompleter; +import dev.noah.perplayerkit.commands.completion.KitSlotTabCompleter; +import dev.noah.perplayerkit.commands.features.HealCommand; +import dev.noah.perplayerkit.commands.features.RegearCommand; +import dev.noah.perplayerkit.commands.features.RepairCommand; +import dev.noah.perplayerkit.commands.inspect.InspectEcCommand; +import dev.noah.perplayerkit.commands.inspect.InspectKitCommand; +import dev.noah.perplayerkit.commands.kits.DeleteKitCommand; +import dev.noah.perplayerkit.commands.kits.EnderchestCommand; +import dev.noah.perplayerkit.commands.kits.MainMenuCommand; +import dev.noah.perplayerkit.commands.kits.PublicKitCommand; +import dev.noah.perplayerkit.commands.kits.SwapKitCommand; +import dev.noah.perplayerkit.commands.share.CopyKitCommand; +import dev.noah.perplayerkit.commands.share.ShareECKitCommand; +import dev.noah.perplayerkit.commands.share.ShareKitCommand; +import dev.noah.perplayerkit.commands.shortcuts.ShortECCommand; +import dev.noah.perplayerkit.commands.shortcuts.ShortKitCommand; import dev.noah.perplayerkit.listeners.*; import dev.noah.perplayerkit.listeners.antiexploit.CommandListener; import dev.noah.perplayerkit.listeners.antiexploit.ShulkerDropItemsListener; diff --git a/src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java deleted file mode 100644 index 9c0cebe..0000000 --- a/src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2022-2025 Noah Ross - * - * This file is part of PerPlayerKit. - * - * PerPlayerKit is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for - * more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with PerPlayerKit. If not, see . - */ -package dev.noah.perplayerkit.commands; - -import com.google.common.primitives.Ints; -import dev.noah.perplayerkit.KitManager; -import org.bukkit.ChatColor; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import dev.noah.perplayerkit.util.SoundManager; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -public class DeleteKitCommand implements CommandExecutor { - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (sender instanceof Player player) { - UUID uuid = player.getUniqueId(); - - if (args.length == 1) { - Integer slot = Ints.tryParse(args[0]); - KitManager kitManager = KitManager.get(); - if (slot == null) { - player.sendMessage(ChatColor.RED + "Usage: /deletekit "); - player.sendMessage(ChatColor.RED + "Select a real number"); - SoundManager.playFailure(player); - return true; - } - - if (kitManager.hasKit(uuid, slot)) { - - if (kitManager.deleteKit(uuid, slot)) { - player.sendMessage(ChatColor.GREEN + "Kit " + slot + " deleted!"); - SoundManager.playSuccess(player); - } else { - player.sendMessage(ChatColor.RED + "Kit deletion failed!"); - SoundManager.playFailure(player); - } - - } else { - player.sendMessage(ChatColor.RED + "Kit " + slot + " doesnt exist!"); - SoundManager.playFailure(player); - } - - - } else { - player.sendMessage(ChatColor.RED + "Usage: /deletekit "); - SoundManager.playFailure(player); - } - } else { - sender.sendMessage(ChatColor.RED + "Only Players can use this!"); - if (sender instanceof Player s) SoundManager.playFailure(s); - - } - - - return true; - } -} diff --git a/src/main/java/dev/noah/perplayerkit/commands/InspectCommandUtil.java b/src/main/java/dev/noah/perplayerkit/commands/InspectCommandUtil.java deleted file mode 100644 index b9c7bf4..0000000 --- a/src/main/java/dev/noah/perplayerkit/commands/InspectCommandUtil.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2022-2025 Noah Ross - * - * This file is part of PerPlayerKit. - * - * PerPlayerKit is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for - * more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with PerPlayerKit. If not, see . - */ -package dev.noah.perplayerkit.commands; - -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; -import dev.noah.perplayerkit.util.BroadcastManager; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -public class InspectCommandUtil { - public static final int MIN_SLOT = 1; - public static final int MAX_SLOT = 9; - public static final MiniMessage mm = MiniMessage.miniMessage(); - public static final Component ERROR_PREFIX = mm.deserialize("Error: "); - - private InspectCommandUtil() { - // Utility class - } - - /** - * Attempts to resolve a player identifier (name or UUID) to a UUID asynchronously. - * This method first tries to parse as UUID, then checks online players synchronously, - * and finally searches offline players asynchronously. - * - * @param identifier Player name or UUID string - * @return CompletableFuture containing UUID if found, null otherwise - */ - public static CompletableFuture resolvePlayerIdentifierAsync(String identifier) { - // First try to parse as UUID - try { - UUID uuid = UUID.fromString(identifier); - return CompletableFuture.completedFuture(uuid); - } catch (IllegalArgumentException ignored) { - // Not a UUID, continue - } - - // Try to find online player (this is fast and safe to do synchronously) - Player onlinePlayer = Bukkit.getPlayerExact(identifier); - if (onlinePlayer != null) { - return CompletableFuture.completedFuture(onlinePlayer.getUniqueId()); - } - - // Look up UUID via Mojang API (avoids the very slow Bukkit.getOfflinePlayers() scan) - return CompletableFuture.supplyAsync(() -> { - try { - OkHttpClient client = new OkHttpClient(); - Request request = new Request.Builder() - .url("https://api.mojang.com/users/profiles/minecraft/" + identifier) - .build(); - Response response = client.newCall(request).execute(); - if (response.isSuccessful() && response.body() != null) { - String body = response.body().string(); - // Parse the "id" field: {"id":"","name":""} - int idStart = body.indexOf("\"id\":\"") + 6; - int idEnd = body.indexOf("\"", idStart); - if (idStart > 5 && idEnd > idStart) { - String raw = body.substring(idStart, idEnd); - // Insert dashes into the 32-char UUID string - String formatted = raw.substring(0, 8) + "-" - + raw.substring(8, 12) + "-" - + raw.substring(12, 16) + "-" - + raw.substring(16, 20) + "-" - + raw.substring(20); - return UUID.fromString(formatted); - } - } - } catch (IOException ignored) { - // Fall through to offline UUID computation - } - // Fallback for offline/cracked-mode servers: compute deterministic offline UUID - return UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)); - }); - } - - /** - * Gets a player's name from their UUID, falling back to UUID string if name is not available. - * - * @param uuid Player UUID - * @return Player name or UUID string - */ - public static String getPlayerName(@NotNull UUID uuid) { - Player onlinePlayer = Bukkit.getPlayer(uuid); - if (onlinePlayer != null) { - return onlinePlayer.getName(); - } - - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); - String name = offlinePlayer.getName(); - return name != null ? name : uuid.toString(); - } - - /** - * Shows command usage message to the player. - * - * @param player Player to send message to - * @param commandName Name of the command (e.g., "inspectkit" or "inspectec") - */ - public static void showUsage(@NotNull Player player, @NotNull String commandName) { - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("Usage: /" + commandName + " "))); - } -} diff --git a/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java b/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java deleted file mode 100644 index e4ac35f..0000000 --- a/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2022-2025 Noah Ross - * - * This file is part of PerPlayerKit. - * - * PerPlayerKit is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for - * more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with PerPlayerKit. If not, see . - */ -package dev.noah.perplayerkit.commands; - -import dev.noah.perplayerkit.KitManager; -import dev.noah.perplayerkit.gui.GUI; -import dev.noah.perplayerkit.util.BroadcastManager; -import dev.noah.perplayerkit.util.SoundManager; - -import org.bukkit.Bukkit; -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.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static dev.noah.perplayerkit.commands.InspectCommandUtil.*; - -public class InspectEcCommand implements CommandExecutor, TabCompleter { - private final Plugin plugin; - - public InspectEcCommand(Plugin plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player player)) { - sender.sendMessage(ERROR_PREFIX.append( - mm.deserialize("This command can only be executed by players.")).toString()); - return true; - } - - if (!player.hasPermission("perplayerkit.inspect")) { - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("You don't have permission to use this command."))); - SoundManager.playFailure(player); - return true; - } - - if (args.length < 2) { - showUsage(player, "inspectec"); - return true; - } - - // Parse slot number - int slot; - try { - slot = Integer.parseInt(args[1]); - if (slot < MIN_SLOT || slot > MAX_SLOT) { - throw new NumberFormatException(); - } - } catch (NumberFormatException e) { - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("Slot must be a number between " + - MIN_SLOT + " and " + MAX_SLOT + "."))); - SoundManager.playFailure(player); - return true; - } - - // Resolve player identifier asynchronously - CompletableFuture future = resolvePlayerIdentifierAsync(args[0]) - .thenCompose(targetUuid -> { - if (targetUuid == null) { - // Player not found - schedule error message on main thread - Bukkit.getScheduler().runTask(plugin, () -> { - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("Could not find a player with that name or UUID."))); - SoundManager.playFailure(player); - }); - return CompletableFuture.completedFuture(null); - } - - // Check if player is online first - Player targetPlayer = Bukkit.getPlayer(targetUuid); - - // Load player data asynchronously - return CompletableFuture.runAsync(() -> { - if (targetPlayer == null) { - // Only load from DB if player is offline - KitManager.get().loadPlayerDataFromDB(targetUuid); - } - }).thenRun(() -> { - // Run on the main thread after data is loaded - Bukkit.getScheduler().runTask(plugin, () -> { - if (KitManager.get().hasEC(targetUuid, slot)) { - GUI gui = new GUI(plugin); - gui.InspectEc(player, targetUuid, slot); - } else { - String targetName = getPlayerName(targetUuid); - - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("" + targetName + - " does not have an enderchest in slot " + slot + ""))); - SoundManager.playFailure(player); - } - }); - }); - }); - - // Handle exceptions - future.exceptionally(ex -> { - Bukkit.getScheduler().runTask(plugin, () -> { - plugin.getLogger().severe("Error loading enderchest data: " + ex.getMessage()); - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("An error occurred while loading enderchest data. " + - "See console for details."))); - SoundManager.playFailure(player); - }); - return null; - }); - - return true; - } - - @Override - public @Nullable List onTabComplete(@NotNull CommandSender sender, - @NotNull Command command, - @NotNull String label, - @NotNull String[] args) { - if (!(sender instanceof Player) || !sender.hasPermission("perplayerkit.inspect")) { - return List.of(); - } - - if (args.length == 1) { - String input = args[0].toLowerCase(); - List completions = new ArrayList<>(Bukkit.getOnlinePlayers().stream() - .map(Player::getName) - .filter(name -> name.toLowerCase().startsWith(input)) - .toList()); - if (input.length() >= 4 && input.contains("-")) { - completions.addAll(Bukkit.getOnlinePlayers().stream() - .map(Player::getUniqueId) - .map(UUID::toString) - .filter(uuid -> uuid.startsWith(input)) - .toList()); - } - return completions; - } else if (args.length == 2) { - return IntStream.rangeClosed(MIN_SLOT, MAX_SLOT) - .mapToObj(String::valueOf) - .filter(slot -> slot.startsWith(args[1])) - .collect(Collectors.toList()); - } - - return new ArrayList<>(); - } -} \ No newline at end of file diff --git a/src/main/java/dev/noah/perplayerkit/commands/PerPlayerKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/PerPlayerKitCommand.java deleted file mode 100644 index a89415c..0000000 --- a/src/main/java/dev/noah/perplayerkit/commands/PerPlayerKitCommand.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2022-2025 Noah Ross - * - * This file is part of PerPlayerKit. - * - * PerPlayerKit is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for - * more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with PerPlayerKit. If not, see . - */ -package dev.noah.perplayerkit.commands; - -import dev.noah.perplayerkit.storage.StorageMigrator; -import dev.noah.perplayerkit.util.importutil.KitsXImporter; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -public class PerPlayerKitCommand implements CommandExecutor, TabCompleter { - - private static final List STORAGE_TYPES = Arrays.asList("sqlite", "mysql", "redis", "yml"); - - private Plugin plugin; - public PerPlayerKitCommand(Plugin plugin){ - this.plugin = plugin; - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (args.length == 0) { - sender.sendMessage(ChatColor.RED + "Missing arguments!"); - return true; - } - - switch (args[0].toLowerCase()) { - case "about": - sender.sendMessage(ChatColor.GREEN + "PerPlayerKit is a plugin that allows players to have their own kits."); - return true; - case "import": - if (args.length < 2) { - sender.sendMessage(ChatColor.RED + "Missing import type!"); - return true; - } - - switch (args[1].toLowerCase()) { - case "kitsx": - sender.sendMessage(ChatColor.GREEN + "Starting import..."); - KitsXImporter importer = new KitsXImporter(plugin,sender); - if(!importer.checkForFiles()){ - sender.sendMessage(ChatColor.RED+"Missing files to import"); - sender.sendMessage(ChatColor.RED+"Copy data folder from KitsX into the PerPlayerKit folder"); - } - importer.importFiles(); - sender.sendMessage(ChatColor.GREEN + "Attempted import of KitsX data!"); - - break; - default: - sender.sendMessage(ChatColor.RED + "Invalid import type!"); - break; - } - return true; - case "migrate": - if (args.length < 3) { - sender.sendMessage(ChatColor.RED + "Usage: /perplayerkit migrate "); - sender.sendMessage(ChatColor.GRAY + "Available storage types: sqlite, mysql, redis, yml"); - return true; - } - - String sourceType = args[1].toLowerCase(); - String destType = args[2].toLowerCase(); - - if (!STORAGE_TYPES.contains(sourceType)) { - sender.sendMessage(ChatColor.RED + "Invalid source storage type: " + sourceType); - sender.sendMessage(ChatColor.GRAY + "Available types: sqlite, mysql, redis, yml"); - return true; - } - - if (!STORAGE_TYPES.contains(destType)) { - sender.sendMessage(ChatColor.RED + "Invalid destination storage type: " + destType); - sender.sendMessage(ChatColor.GRAY + "Available types: sqlite, mysql, redis, yml"); - return true; - } - - if (sourceType.equals(destType)) { - sender.sendMessage(ChatColor.RED + "Source and destination cannot be the same!"); - return true; - } - - sender.sendMessage(ChatColor.YELLOW + "Starting migration from " + sourceType + " to " + destType + "..."); - sender.sendMessage(ChatColor.GRAY + "This may take a while for large datasets. Check console for progress."); - - // Run migration asynchronously to avoid blocking the main thread - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - StorageMigrator migrator = new StorageMigrator(plugin); - StorageMigrator.MigrationResult result = migrator.migrate(sourceType, destType, - message -> Bukkit.getScheduler().runTask(plugin, () -> sender.sendMessage(ChatColor.GRAY + message))); - - Bukkit.getScheduler().runTask(plugin, () -> { - if (result.isSuccess()) { - sender.sendMessage(ChatColor.GREEN + "Migration completed successfully!"); - sender.sendMessage(ChatColor.GREEN + "Migrated: " + result.getMigratedCount() + " entries"); - if (result.getFailedCount() > 0) { - sender.sendMessage(ChatColor.YELLOW + "Failed: " + result.getFailedCount() + " entries"); - } - sender.sendMessage(ChatColor.YELLOW + "Remember to update your config.yml storage.type to '" + destType + "' and restart the server."); - } else { - sender.sendMessage(ChatColor.RED + "Migration failed: " + result.getErrorMessage()); - } - }); - }); - return true; - default: - sender.sendMessage(ChatColor.RED + "Invalid subcommand!"); - return true; - - } - } - - - @Nullable - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - - if(args.length == 1) { - return List.of("about", "import", "migrate"); - } - - if(args.length == 2 && args[0].equalsIgnoreCase("import")) { - return List.of("kitsx"); - } - - if(args.length == 2 && args[0].equalsIgnoreCase("migrate")) { - return STORAGE_TYPES.stream() - .filter(type -> type.startsWith(args[1].toLowerCase())) - .collect(Collectors.toList()); - } - - if(args.length == 3 && args[0].equalsIgnoreCase("migrate")) { - String sourceType = args[1].toLowerCase(); - return STORAGE_TYPES.stream() - .filter(type -> !type.equals(sourceType)) - .filter(type -> type.startsWith(args[2].toLowerCase())) - .collect(Collectors.toList()); - } - - return null; - } -} diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java b/src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java deleted file mode 100644 index 476bc70..0000000 --- a/src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2022-2025 Noah Ross - * - * This file is part of PerPlayerKit. - * - * PerPlayerKit is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for - * more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with PerPlayerKit. If not, see . - */ -package dev.noah.perplayerkit.commands; - -import dev.noah.perplayerkit.util.DisabledCommand; -import dev.noah.perplayerkit.KitManager; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -public class ShortECCommand implements CommandExecutor { - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - - if (!(sender instanceof Player player)) { - sender.sendMessage("Only players can use this command."); - return true; - } - - if (DisabledCommand.isBlockedInWorld(player)) { - return true; - } - - UUID uuid = player.getUniqueId(); - - if (label.matches("ec[1-9]")) { - int ecNumber = Integer.parseInt(label.substring(2)); // Extract the number from the label - KitManager.get().loadEnderchest(player, ecNumber); - } else if (label.matches("enderchest[1-9]")) { - int ecNumber = Integer.parseInt(label.substring(10)); // Extract the number from the label - KitManager.get().loadEnderchest(player, ecNumber); - } else { - player.sendMessage("Invalid command label."); - } - - return true; - } -} diff --git a/src/main/java/dev/noah/perplayerkit/commands/AboutCommandListener.java b/src/main/java/dev/noah/perplayerkit/commands/admin/AboutCommandListener.java similarity index 98% rename from src/main/java/dev/noah/perplayerkit/commands/AboutCommandListener.java rename to src/main/java/dev/noah/perplayerkit/commands/admin/AboutCommandListener.java index d126529..3d0d51f 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/AboutCommandListener.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/AboutCommandListener.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.admin; import org.bukkit.command.CommandSender; import org.bukkit.event.EventHandler; diff --git a/src/main/java/dev/noah/perplayerkit/commands/KitRoomCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/KitRoomCommand.java similarity index 60% rename from src/main/java/dev/noah/perplayerkit/commands/KitRoomCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/admin/KitRoomCommand.java index 0018474..ec12c3f 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/KitRoomCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/KitRoomCommand.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.admin; import dev.noah.perplayerkit.KitRoomDataManager; import org.bukkit.ChatColor; @@ -33,29 +33,31 @@ import java.util.List; public class KitRoomCommand implements CommandExecutor, TabCompleter { + private static final String LOAD = "load"; + private static final String SAVE = "save"; + @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (args.length == 1) { - if (args[0].equalsIgnoreCase("load")) { - KitRoomDataManager.get().loadFromDB(); - sender.sendMessage(ChatColor.GREEN + "Kit Room loaded from SQL"); - if (sender instanceof Player p) SoundManager.playSuccess(p); - } else if (args[0].equalsIgnoreCase("save")) { - KitRoomDataManager.get().saveToDBAsync(); - sender.sendMessage(ChatColor.GREEN + "Kit Room saved to SQL"); - if (sender instanceof Player p) SoundManager.playSuccess(p); - } else { - sender.sendMessage(ChatColor.RED + "Incorrect Usage!"); - sender.sendMessage("/kitroom "); - if (sender instanceof Player p) SoundManager.playFailure(p); - } - } else { - sender.sendMessage(ChatColor.RED + "Incorrect Usage!"); - sender.sendMessage("/kitroom "); - if (sender instanceof Player p) SoundManager.playFailure(p); + if (args.length != 1) { + sendUsageError(sender); + return true; + } + + if (args[0].equalsIgnoreCase(LOAD)) { + KitRoomDataManager.get().loadFromDB(); + sender.sendMessage(ChatColor.GREEN + "Kit Room loaded from SQL"); + if (sender instanceof Player p) SoundManager.playSuccess(p); + return true; } + if (args[0].equalsIgnoreCase(SAVE)) { + KitRoomDataManager.get().saveToDBAsync(); + sender.sendMessage(ChatColor.GREEN + "Kit Room saved to SQL"); + if (sender instanceof Player p) SoundManager.playSuccess(p); + return true; + } + sendUsageError(sender); return true; } @@ -63,10 +65,18 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { if (args.length == 1) { List list = new ArrayList<>(); - list.add("save"); - list.add("load"); + list.add(SAVE); + list.add(LOAD); return list; } return null; } + + private void sendUsageError(CommandSender sender) { + sender.sendMessage(ChatColor.RED + "Incorrect Usage!"); + sender.sendMessage("/kitroom "); + if (sender instanceof Player p) { + SoundManager.playFailure(p); + } + } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java new file mode 100644 index 0000000..74a7223 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java @@ -0,0 +1,189 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.admin; + +import dev.noah.perplayerkit.storage.StorageMigrator; +import dev.noah.perplayerkit.util.importutil.KitsXImporter; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class PerPlayerKitCommand implements CommandExecutor, TabCompleter { + + private static final List STORAGE_TYPES = Arrays.asList("sqlite", "mysql", "redis", "yml"); + + private final Plugin plugin; + + public PerPlayerKitCommand(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length == 0) { + sender.sendMessage(ChatColor.RED + "Missing arguments!"); + return true; + } + + switch (args[0].toLowerCase()) { + case "about": + sender.sendMessage(ChatColor.GREEN + "PerPlayerKit is a plugin that allows players to have their own kits."); + return true; + case "import": + return handleImport(sender, args); + case "migrate": + return handleMigrate(sender, args); + default: + sender.sendMessage(ChatColor.RED + "Invalid subcommand!"); + return true; + + } + } + + private boolean handleImport(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(ChatColor.RED + "Missing import type!"); + return true; + } + + if (!args[1].equalsIgnoreCase("kitsx")) { + sender.sendMessage(ChatColor.RED + "Invalid import type!"); + return true; + } + + sender.sendMessage(ChatColor.GREEN + "Starting import..."); + KitsXImporter importer = new KitsXImporter(plugin, sender); + if (!importer.checkForFiles()) { + sender.sendMessage(ChatColor.RED + "Missing files to import"); + sender.sendMessage(ChatColor.RED + "Copy data folder from KitsX into the PerPlayerKit folder"); + return true; + } + + importer.importFiles(); + sender.sendMessage(ChatColor.GREEN + "Attempted import of KitsX data!"); + return true; + } + + private boolean handleMigrate(CommandSender sender, String[] args) { + if (args.length < 3) { + sendMigrateUsage(sender); + return true; + } + + String sourceType = args[1].toLowerCase(); + String destinationType = args[2].toLowerCase(); + + if (!validateStorageType(sender, sourceType, "source")) { + return true; + } + if (!validateStorageType(sender, destinationType, "destination")) { + return true; + } + if (sourceType.equals(destinationType)) { + sender.sendMessage(ChatColor.RED + "Source and destination cannot be the same!"); + return true; + } + + sender.sendMessage(ChatColor.YELLOW + "Starting migration from " + sourceType + " to " + destinationType + "..."); + sender.sendMessage(ChatColor.GRAY + "This may take a while for large datasets. Check console for progress."); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> runMigration(sender, sourceType, destinationType)); + return true; + } + + private void sendMigrateUsage(CommandSender sender) { + sender.sendMessage(ChatColor.RED + "Usage: /perplayerkit migrate "); + sender.sendMessage(ChatColor.GRAY + "Available storage types: sqlite, mysql, redis, yml"); + } + + private boolean validateStorageType(CommandSender sender, String storageType, String role) { + if (STORAGE_TYPES.contains(storageType)) { + return true; + } + + sender.sendMessage(ChatColor.RED + "Invalid " + role + " storage type: " + storageType); + sender.sendMessage(ChatColor.GRAY + "Available types: sqlite, mysql, redis, yml"); + return false; + } + + private void runMigration(CommandSender sender, String sourceType, String destinationType) { + StorageMigrator migrator = new StorageMigrator(plugin); + StorageMigrator.MigrationResult result = migrator.migrate( + sourceType, + destinationType, + message -> Bukkit.getScheduler().runTask(plugin, () -> sender.sendMessage(ChatColor.GRAY + message)) + ); + + Bukkit.getScheduler().runTask(plugin, () -> sendMigrationResult(sender, destinationType, result)); + } + + private void sendMigrationResult(CommandSender sender, String destinationType, StorageMigrator.MigrationResult result) { + if (result.isSuccess()) { + sender.sendMessage(ChatColor.GREEN + "Migration completed successfully!"); + sender.sendMessage(ChatColor.GREEN + "Migrated: " + result.getMigratedCount() + " entries"); + if (result.getFailedCount() > 0) { + sender.sendMessage(ChatColor.YELLOW + "Failed: " + result.getFailedCount() + " entries"); + } + sender.sendMessage(ChatColor.YELLOW + "Remember to update your config.yml storage.type to '" + destinationType + "' and restart the server."); + return; + } + + sender.sendMessage(ChatColor.RED + "Migration failed: " + result.getErrorMessage()); + } + + + @Nullable + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + + if (args.length == 1) { + return List.of("about", "import", "migrate"); + } + + if (args.length == 2 && args[0].equalsIgnoreCase("import")) { + return List.of("kitsx"); + } + + if (args.length == 2 && args[0].equalsIgnoreCase("migrate")) { + return STORAGE_TYPES.stream() + .filter(type -> type.startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + + if (args.length == 3 && args[0].equalsIgnoreCase("migrate")) { + String sourceType = args[1].toLowerCase(); + return STORAGE_TYPES.stream() + .filter(type -> !type.equals(sourceType)) + .filter(type -> type.startsWith(args[2].toLowerCase())) + .collect(Collectors.toList()); + } + + return null; + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/SavePublicKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java similarity index 66% rename from src/main/java/dev/noah/perplayerkit/commands/SavePublicKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java index f159d3c..5c7cbd3 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/SavePublicKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java @@ -16,11 +16,11 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.admin; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.ItemFilter; import dev.noah.perplayerkit.KitManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -38,33 +38,26 @@ public class SavePublicKitCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - //if not player - if (!(sender instanceof Player p)) { - sender.sendMessage("Only players can use this command"); + Player player = CommandGuards.requirePlayerInEnabledWorld(sender); + if (player == null) { return true; } - if (DisabledCommand.isBlockedInWorld(p)) { - return true; - } - - //if not enough arguments - if (args.length < 1) { - p.sendMessage(ChatColor.RED + "You need to specify a kit id"); - p.sendMessage(ChatColor.RED + "Usage: /" + label + " "); + player.sendMessage(ChatColor.RED + "You need to specify a kit id"); + player.sendMessage(ChatColor.RED + "Usage: /" + label + " "); return true; } - String kidId = args[0]; + String kitId = args[0]; - if (KitManager.get().getPublicKitList().stream().noneMatch(kit -> kit.id.equals(kidId))) { - p.sendMessage(ChatColor.RED + "Public kit " + kidId + " does not exist"); - p.sendMessage(ChatColor.RED + "You may need to add a public kit in the config"); + if (KitManager.get().getPublicKitList().stream().noneMatch(kit -> kit.id.equals(kitId))) { + player.sendMessage(ChatColor.RED + "Public kit " + kitId + " does not exist"); + player.sendMessage(ChatColor.RED + "You may need to add a public kit in the config"); return true; } - Inventory inv = p.getInventory(); + Inventory inv = player.getInventory(); ItemStack[] data = new ItemStack[41]; // copy inventory into data @@ -80,14 +73,14 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command KitManager kitManager = KitManager.get(); //save kit - boolean success = kitManager.savePublicKit(kidId, data); + boolean success = kitManager.savePublicKit(kitId, data); if (success) { - kitManager.savePublicKitToDB(kidId); - p.sendMessage("Saved kit " + kidId); - SoundManager.playSuccess(p); + kitManager.savePublicKitToDB(kitId); + player.sendMessage("Saved kit " + kitId); + SoundManager.playSuccess(player); } else { - p.sendMessage("Error saving kit " + kidId); - SoundManager.playFailure(p); + player.sendMessage("Error saving kit " + kitId); + SoundManager.playFailure(player); } return true; @@ -99,6 +92,11 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command @Nullable @Override public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - return KitManager.get().getPublicKitList().stream().map(kit -> kit.id).toList(); + if (args.length == 1) { + List ids = KitManager.get().getPublicKitList().stream().map(kit -> kit.id).toList(); + return ids.isEmpty() ? null : ids; + } + + return null; } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/tabcompleters/ECSlotTabCompleter.java b/src/main/java/dev/noah/perplayerkit/commands/completion/ECSlotTabCompleter.java similarity index 96% rename from src/main/java/dev/noah/perplayerkit/commands/tabcompleters/ECSlotTabCompleter.java rename to src/main/java/dev/noah/perplayerkit/commands/completion/ECSlotTabCompleter.java index 9bf5827..7620a3f 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/tabcompleters/ECSlotTabCompleter.java +++ b/src/main/java/dev/noah/perplayerkit/commands/completion/ECSlotTabCompleter.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands.tabcompleters; +package dev.noah.perplayerkit.commands.completion; import dev.noah.perplayerkit.KitShareManager; import org.bukkit.command.Command; diff --git a/src/main/java/dev/noah/perplayerkit/commands/tabcompleters/KitSlotTabCompleter.java b/src/main/java/dev/noah/perplayerkit/commands/completion/KitSlotTabCompleter.java similarity index 96% rename from src/main/java/dev/noah/perplayerkit/commands/tabcompleters/KitSlotTabCompleter.java rename to src/main/java/dev/noah/perplayerkit/commands/completion/KitSlotTabCompleter.java index de14e6e..b81c238 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/tabcompleters/KitSlotTabCompleter.java +++ b/src/main/java/dev/noah/perplayerkit/commands/completion/KitSlotTabCompleter.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands.tabcompleters; +package dev.noah.perplayerkit.commands.completion; import dev.noah.perplayerkit.KitShareManager; import org.bukkit.command.Command; diff --git a/src/main/java/dev/noah/perplayerkit/commands/core/CommandGuards.java b/src/main/java/dev/noah/perplayerkit/commands/core/CommandGuards.java new file mode 100644 index 0000000..c6c0b02 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/core/CommandGuards.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.core; + +import dev.noah.perplayerkit.util.DisabledCommand; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +public final class CommandGuards { + private static final String DEFAULT_ONLY_PLAYERS_MESSAGE = "Only players can use this command"; + + private CommandGuards() { + } + + public static @Nullable Player requirePlayer(CommandSender sender) { + return requirePlayer(sender, DEFAULT_ONLY_PLAYERS_MESSAGE); + } + + public static @Nullable Player requirePlayer(CommandSender sender, String onlyPlayersMessage) { + if (sender instanceof Player player) { + return player; + } + sender.sendMessage(onlyPlayersMessage); + return null; + } + + public static @Nullable Player requirePlayerInEnabledWorld(CommandSender sender) { + return requirePlayerInEnabledWorld(sender, DEFAULT_ONLY_PLAYERS_MESSAGE); + } + + public static @Nullable Player requirePlayerInEnabledWorld(CommandSender sender, String onlyPlayersMessage) { + Player player = requirePlayer(sender, onlyPlayersMessage); + if (player == null) { + return null; + } + if (DisabledCommand.isBlockedInWorld(player)) { + return null; + } + return player; + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/core/SlotArgumentParser.java b/src/main/java/dev/noah/perplayerkit/commands/core/SlotArgumentParser.java new file mode 100644 index 0000000..fbe012d --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/core/SlotArgumentParser.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.core; + +import com.google.common.primitives.Ints; +import org.jetbrains.annotations.Nullable; + +public final class SlotArgumentParser { + private SlotArgumentParser() { + } + + public static @Nullable Integer parseSlot(String slotArgument) { + return Ints.tryParse(slotArgument); + } + + public static @Nullable Integer parseSlotInRange(String slotArgument, int min, int max) { + Integer slot = parseSlot(slotArgument); + if (slot == null || slot < min || slot > max) { + return null; + } + return slot; + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/extracommands/HealCommand.java b/src/main/java/dev/noah/perplayerkit/commands/features/HealCommand.java similarity index 86% rename from src/main/java/dev/noah/perplayerkit/commands/extracommands/HealCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/features/HealCommand.java index 1fe9775..7a6be6a 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/extracommands/HealCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/features/HealCommand.java @@ -16,11 +16,11 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands.extracommands; +package dev.noah.perplayerkit.commands.features; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.util.BroadcastManager; import dev.noah.perplayerkit.util.PlayerUtil; -import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -31,9 +31,8 @@ public class HealCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - - if (!(sender instanceof Player player)) { - sender.sendMessage("Only players can use this command!"); + Player player = CommandGuards.requirePlayer(sender, "Only players can use this command!"); + if (player == null) { return true; } diff --git a/src/main/java/dev/noah/perplayerkit/commands/RegearCommand.java b/src/main/java/dev/noah/perplayerkit/commands/features/RegearCommand.java similarity index 53% rename from src/main/java/dev/noah/perplayerkit/commands/RegearCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/features/RegearCommand.java index feec00e..68af438 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/RegearCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/features/RegearCommand.java @@ -1,10 +1,10 @@ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.features; import dev.noah.perplayerkit.KitManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.gui.ItemUtil; import dev.noah.perplayerkit.util.BroadcastManager; import dev.noah.perplayerkit.util.CooldownManager; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.util.StyleManager; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; @@ -28,6 +28,7 @@ public class RegearCommand implements CommandExecutor, Listener { public static final ItemStack REGEAR_SHULKER_ITEM = ItemUtil.createItem(Material.WHITE_SHULKER_BOX, 1, StyleManager.get().getPrimaryColor() + "Regear Shulker", "● Restocks Your Kit", "● Use " + StyleManager.get().getPrimaryColor() + "/rg to get another regear shulker"); public static final ItemStack REGEAR_SHELL_ITEM = ItemUtil.createItem(Material.SHULKER_SHELL, 1, StyleManager.get().getPrimaryColor() + "Regear Shell", "● Restocks Your Kit", "● Click to use!"); + private static final MiniMessage MM = MiniMessage.miniMessage(); private final Plugin plugin; private final CooldownManager commandCooldownManager; @@ -55,73 +56,23 @@ public void onPlayerTakesDamage(EntityDamageEvent event) { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player player)) { - sender.sendMessage("Only players can use this command!"); + Player player = CommandGuards.requirePlayerInEnabledWorld(sender, "Only players can use this command!"); + if (player == null) { return true; } - if (DisabledCommand.isBlockedInWorld(player)) { - return true; - } - - // Determine which mode to use based on the command label - String effectiveMode; - if (label.equalsIgnoreCase("rg")) { - effectiveMode = plugin.getConfig().getString("regear.rg-mode", "command"); - } else if (label.equalsIgnoreCase("regear")) { - effectiveMode = plugin.getConfig().getString("regear.regear-mode", "command"); - } else { - effectiveMode = plugin.getConfig().getString("regear.rg-mode", "command"); // Default fallback - } - + String effectiveMode = getEffectiveMode(label); if (effectiveMode.equalsIgnoreCase("shulker")) { - int slot = player.getInventory().firstEmpty(); - if (slot == -1) { - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("Your inventory is full, can't give you a regear shulker!")); - return true; - } - - player.getInventory().setItem(slot, REGEAR_SHULKER_ITEM); - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("Regear Shulker given!")); - + handleShulkerMode(player); return true; } if (effectiveMode.equalsIgnoreCase("command")) { - int slot = KitManager.get().getLastKitLoaded(player.getUniqueId()); - - if (slot == -1) { - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("You have not loaded a kit yet!")); - return true; - } - - if (!allowRegearWhileUsingElytra && player.isGliding() && player.getInventory().getChestplate() != null && player.getInventory().getChestplate().getType() == Material.ELYTRA) { - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("You cannot regear while using an elytra!")); - return true; - } - - if (damageCooldownManager.isOnCooldown(player)) { - int secondsLeft = damageCooldownManager.getTimeLeft(player); - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("You must be out of combat for " + secondsLeft + " more seconds before regearing!")); - return true; - } - - if (commandCooldownManager.isOnCooldown(player)) { - int secondsLeft = commandCooldownManager.getTimeLeft(player); - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("You must wait " + secondsLeft + " seconds before using this command again!")); - return true; - } - - KitManager.get().regearKit(player, slot); - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("Regeared!")); - BroadcastManager.get().broadcastPlayerRegeared(player); - - commandCooldownManager.setCooldown(player); - + handleCommandMode(player); return true; } - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("This command is not configured correctly, please contact an administrator.")); + sendMessage(player, "This command is not configured correctly, please contact an administrator."); return true; } @@ -133,22 +84,17 @@ public void onShulkerPlace(BlockPlaceEvent event) { event.setCancelled(true); Player player = event.getPlayer(); - int slot = KitManager.get().getLastKitLoaded(player.getUniqueId()); - - if (slot == -1) { - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("You have not loaded a kit yet!")); + Integer slot = getLastLoadedKitSlot(player); + if (slot == null) { return; } - if (damageCooldownManager.isOnCooldown(player)) { - int secondsLeft = damageCooldownManager.getTimeLeft(player); - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("You must be out of combat for " + secondsLeft + " more seconds before regearing!")); + if (isDamageCooldownBlocked(player)) { return; } player.getInventory().setItem(event.getHand(), null); -//custom inv with holder RegearInventoryHolder holder = new RegearInventoryHolder(player); Inventory inventory = holder.getInventory(); player.openInventory(inventory); @@ -175,16 +121,12 @@ public void onShulkerShellClick(InventoryClickEvent event) { Player player = holder.player(); - int slot = KitManager.get().getLastKitLoaded(player.getUniqueId()); - - if (slot == -1) { - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("You have not loaded a kit yet!")); + Integer slot = getLastLoadedKitSlot(player); + if (slot == null) { return; } - if (damageCooldownManager.isOnCooldown(player)) { - int secondsLeft = damageCooldownManager.getTimeLeft(player); - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("You must be out of combat for " + secondsLeft + " more seconds before regearing!")); + if (isDamageCooldownBlocked(player)) { return; } @@ -193,10 +135,104 @@ public void onShulkerShellClick(InventoryClickEvent event) { KitManager.get().regearKit(player, slot); player.updateInventory(); - BroadcastManager.get().sendComponentMessage(player, MiniMessage.miniMessage().deserialize("Regeared!")); + announceRegearSuccess(player); + } + + private String getEffectiveMode(String label) { + if (label.equalsIgnoreCase("rg")) { + return plugin.getConfig().getString("regear.rg-mode", "command"); + } + if (label.equalsIgnoreCase("regear")) { + return plugin.getConfig().getString("regear.regear-mode", "command"); + } + return plugin.getConfig().getString("regear.rg-mode", "command"); + } + + private void handleShulkerMode(Player player) { + int slot = player.getInventory().firstEmpty(); + if (slot == -1) { + sendMessage(player, "Your inventory is full, can't give you a regear shulker!"); + return; + } + + player.getInventory().setItem(slot, REGEAR_SHULKER_ITEM); + sendMessage(player, "Regear Shulker given!"); + } + + private void handleCommandMode(Player player) { + Integer slot = getLastLoadedKitSlot(player); + if (slot == null) { + return; + } + if (isElytraBlocked(player)) { + return; + } + if (isDamageCooldownBlocked(player)) { + return; + } + if (isCommandCooldownBlocked(player)) { + return; + } + + KitManager.get().regearKit(player, slot); + announceRegearSuccess(player); + commandCooldownManager.setCooldown(player); + } + + private Integer getLastLoadedKitSlot(Player player) { + int slot = KitManager.get().getLastKitLoaded(player.getUniqueId()); + if (slot != -1) { + return slot; + } + + sendMessage(player, "You have not loaded a kit yet!"); + return null; + } + + private boolean isElytraBlocked(Player player) { + if (allowRegearWhileUsingElytra) { + return false; + } + if (!player.isGliding()) { + return false; + } + if (player.getInventory().getChestplate() == null || player.getInventory().getChestplate().getType() != Material.ELYTRA) { + return false; + } + + sendMessage(player, "You cannot regear while using an elytra!"); + return true; + } + + private boolean isDamageCooldownBlocked(Player player) { + if (!damageCooldownManager.isOnCooldown(player)) { + return false; + } + + int secondsLeft = damageCooldownManager.getTimeLeft(player); + sendMessage(player, "You must be out of combat for " + secondsLeft + " more seconds before regearing!"); + return true; + } + + private boolean isCommandCooldownBlocked(Player player) { + if (!commandCooldownManager.isOnCooldown(player)) { + return false; + } + + int secondsLeft = commandCooldownManager.getTimeLeft(player); + sendMessage(player, "You must wait " + secondsLeft + " seconds before using this command again!"); + return true; + } + + private void announceRegearSuccess(Player player) { + sendMessage(player, "Regeared!"); BroadcastManager.get().broadcastPlayerRegeared(player); } + private void sendMessage(Player player, String message) { + BroadcastManager.get().sendComponentMessage(player, MM.deserialize(message)); + } + public record RegearInventoryHolder( Player player) implements InventoryHolder { diff --git a/src/main/java/dev/noah/perplayerkit/commands/extracommands/RepairCommand.java b/src/main/java/dev/noah/perplayerkit/commands/features/RepairCommand.java similarity index 86% rename from src/main/java/dev/noah/perplayerkit/commands/extracommands/RepairCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/features/RepairCommand.java index 389554e..6ab5c48 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/extracommands/RepairCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/features/RepairCommand.java @@ -16,8 +16,9 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands.extracommands; +package dev.noah.perplayerkit.commands.features; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.util.BroadcastManager; import dev.noah.perplayerkit.util.PlayerUtil; import org.bukkit.command.Command; @@ -30,8 +31,8 @@ public class RepairCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if(!(sender instanceof Player player)){ - sender.sendMessage("Only players can use this command!"); + Player player = CommandGuards.requirePlayer(sender, "Only players can use this command!"); + if (player == null) { return true; } diff --git a/src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java similarity index 51% rename from src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java index 47d2c27..3b320df 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java @@ -16,13 +16,11 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.inspect; import dev.noah.perplayerkit.KitManager; -import dev.noah.perplayerkit.gui.GUI; import dev.noah.perplayerkit.util.BroadcastManager; import dev.noah.perplayerkit.util.SoundManager; - import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -40,15 +38,33 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static dev.noah.perplayerkit.commands.InspectCommandUtil.*; +import static dev.noah.perplayerkit.commands.inspect.InspectCommandUtil.ERROR_PREFIX; +import static dev.noah.perplayerkit.commands.inspect.InspectCommandUtil.MAX_SLOT; +import static dev.noah.perplayerkit.commands.inspect.InspectCommandUtil.MIN_SLOT; +import static dev.noah.perplayerkit.commands.inspect.InspectCommandUtil.mm; +import static dev.noah.perplayerkit.commands.inspect.InspectCommandUtil.resolvePlayerIdentifierAsync; +import static dev.noah.perplayerkit.commands.inspect.InspectCommandUtil.showUsage; +import static dev.noah.perplayerkit.util.PlayerUtil.getPlayerName; -public class InspectKitCommand implements CommandExecutor, TabCompleter { - private final Plugin plugin; +public abstract class AbstractInspectCommand implements CommandExecutor, TabCompleter { + protected final Plugin plugin; - public InspectKitCommand(Plugin plugin) { + protected AbstractInspectCommand(Plugin plugin) { this.plugin = plugin; } + protected abstract String usageCommand(); + + protected abstract boolean hasData(UUID targetUuid, int slot); + + protected abstract void openInspectGui(Player inspector, UUID targetUuid, int slot); + + protected abstract String missingDataMessage(String targetName, int slot); + + protected abstract String loadErrorLogMessage(); + + protected abstract String loadErrorUserMessage(); + @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { @@ -58,86 +74,62 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - if (!player.hasPermission("perplayerkit.inspect")) { - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("You don't have permission to use this command."))); - SoundManager.playFailure(player); - return true; - } - if (args.length < 2) { - showUsage(player, "inspectkit"); + showUsage(player, usageCommand()); return true; } - // Parse slot number - int slot; - try { - slot = Integer.parseInt(args[1]); - if (slot < MIN_SLOT || slot > MAX_SLOT) { - throw new NumberFormatException(); - } - } catch (NumberFormatException e) { - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("Slot must be a number between " + - MIN_SLOT + " and " + MAX_SLOT + "."))); - SoundManager.playFailure(player); + int slot = parseSlot(args[1], player); + if (slot == -1) { return true; } - // Resolve player identifier asynchronously + UUID senderUuid = player.getUniqueId(); CompletableFuture future = resolvePlayerIdentifierAsync(args[0]) .thenCompose(targetUuid -> { if (targetUuid == null) { - // Player not found - schedule error message on main thread Bukkit.getScheduler().runTask(plugin, () -> { - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("Could not find a player with that name or UUID."))); - SoundManager.playFailure(player); + Player currentSender = Bukkit.getPlayer(senderUuid); + if (currentSender == null) { + return; + } + showPlayerNotFound(currentSender); }); return CompletableFuture.completedFuture(null); } - // Check if player is online first - Player targetPlayer = Bukkit.getPlayer(targetUuid); - - // Load player data asynchronously - return CompletableFuture.runAsync(() -> { - if (targetPlayer == null) { - // Only load from DB if player is offline - KitManager.get().loadPlayerDataFromDB(targetUuid); - } - }).thenRun(() -> { - // Run on the main thread after data is loaded - Bukkit.getScheduler().runTask(plugin, () -> { - if (KitManager.get().hasKit(targetUuid, slot)) { - GUI gui = new GUI(plugin); - gui.InspectKit(player, targetUuid, slot); - } else { - String targetName = getPlayerName(targetUuid); - - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("" + targetName + - " does not have a kit in slot " + slot + ""))); - SoundManager.playFailure(player); + CompletableFuture targetOnlineFuture = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(plugin, + () -> targetOnlineFuture.complete(Bukkit.getPlayer(targetUuid) != null)); + + return targetOnlineFuture.thenCompose(targetOnline -> { + CompletableFuture loadFuture = targetOnline + ? CompletableFuture.completedFuture(null) + : CompletableFuture.runAsync(() -> KitManager.get().loadPlayerDataFromDB(targetUuid)); + + return loadFuture.thenRun(() -> Bukkit.getScheduler().runTask(plugin, () -> { + Player currentSender = Bukkit.getPlayer(senderUuid); + if (currentSender == null) { + return; } - }); + + Player currentTarget = Bukkit.getPlayer(targetUuid); + showInspectResult(currentSender, currentTarget, targetUuid, slot); + })); }); }); - // Handle exceptions future.exceptionally(ex -> { Bukkit.getScheduler().runTask(plugin, () -> { - plugin.getLogger().severe("Error loading kit data: " + ex.getMessage()); - BroadcastManager.get().sendComponentMessage(player, - ERROR_PREFIX.append( - mm.deserialize("An error occurred while loading kit data. " + - "See console for details."))); - SoundManager.playFailure(player); + plugin.getLogger().severe(loadErrorLogMessage() + ": " + ex.getMessage()); + Player currentSender = Bukkit.getPlayer(senderUuid); + if (currentSender == null) { + return; + } + + BroadcastManager.get().sendComponentMessage(currentSender, + ERROR_PREFIX.append(mm.deserialize(loadErrorUserMessage()))); + SoundManager.playFailure(currentSender); }); return null; }); @@ -150,20 +142,16 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player) || !sender.hasPermission("perplayerkit.inspect")) { + if (!(sender instanceof Player)) { return List.of(); } if (args.length == 1) { String input = args[0].toLowerCase(); - - // Add online player names List completions = new ArrayList<>(Bukkit.getOnlinePlayers().stream() .map(Player::getName) .filter(name -> name.toLowerCase().startsWith(input)) .toList()); - - // Add UUIDs if the input looks like it might be a UUID if (input.length() >= 4 && input.contains("-")) { completions.addAll(Bukkit.getOnlinePlayers().stream() .map(Player::getUniqueId) @@ -171,10 +159,10 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command .filter(uuid -> uuid.startsWith(input)) .toList()); } - return completions; - } else if (args.length == 2) { - // Return slot numbers for second argument + } + + if (args.length == 2) { return IntStream.rangeClosed(MIN_SLOT, MAX_SLOT) .mapToObj(String::valueOf) .filter(slot -> slot.startsWith(args[1])) @@ -183,4 +171,43 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return new ArrayList<>(); } + + private int parseSlot(String slotArg, Player player) { + try { + int slot = Integer.parseInt(slotArg); + if (slot < MIN_SLOT || slot > MAX_SLOT) { + throw new NumberFormatException(); + } + return slot; + } catch (NumberFormatException e) { + BroadcastManager.get().sendComponentMessage(player, + ERROR_PREFIX.append( + mm.deserialize("Slot must be a number between " + + MIN_SLOT + " and " + MAX_SLOT + "."))); + SoundManager.playFailure(player); + return -1; + } + } + + private void showInspectResult(Player inspector, @Nullable Player targetPlayer, UUID targetUuid, int slot) { + if (hasData(targetUuid, slot)) { + openInspectGui(inspector, targetUuid, slot); + return; + } + + String targetName = targetPlayer != null ? targetPlayer.getName() : getPlayerName(targetUuid); + if (targetName == null) { + targetName = targetUuid.toString(); + } + BroadcastManager.get().sendComponentMessage(inspector, + ERROR_PREFIX.append(mm.deserialize(missingDataMessage(targetName, slot)))); + SoundManager.playFailure(inspector); + } + + private void showPlayerNotFound(Player player) { + BroadcastManager.get().sendComponentMessage(player, + ERROR_PREFIX.append( + mm.deserialize("Could not find a player with that name or UUID."))); + SoundManager.playFailure(player); + } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java new file mode 100644 index 0000000..b801904 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java @@ -0,0 +1,175 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.inspect; + +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import dev.noah.perplayerkit.util.BroadcastManager; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class InspectCommandUtil { + public static final int MIN_SLOT = 1; + public static final int MAX_SLOT = 9; + public static final MiniMessage mm = MiniMessage.miniMessage(); + public static final Component ERROR_PREFIX = mm.deserialize("Error: "); + + private InspectCommandUtil() { + // Utility class + } + + /** + * Attempts to resolve a player identifier (name or UUID) to a UUID asynchronously. + * This method first tries to parse as UUID, then checks online players synchronously, + * and finally searches cached offline players and Mojang asynchronously. + * + * @param identifier Player name or UUID string + * @return CompletableFuture containing the resolved UUID, or null when the player + * could not be identified from local cache or Mojang + */ + public static CompletableFuture<@Nullable UUID> resolvePlayerIdentifierAsync(String identifier) { + // First try to parse as UUID + try { + UUID uuid = UUID.fromString(identifier); + return CompletableFuture.completedFuture(uuid); + } catch (IllegalArgumentException ignored) { + // Not a UUID, continue + } + + // Try to find online player (this is fast and safe to do synchronously) + Player onlinePlayer = Bukkit.getPlayerExact(identifier); + if (onlinePlayer != null) { + return CompletableFuture.completedFuture(onlinePlayer.getUniqueId()); + } + + return CompletableFuture.supplyAsync(() -> { + UUID cachedOfflinePlayer = findCachedOfflinePlayerUuid(identifier); + return selectResolvedUuid(identifier, cachedOfflinePlayer, + () -> lookupPlayerUuidFromMojang(identifier)); + }); + } + + /** + * Shows command usage message to the player. + * + * @param player Player to send message to + * @param commandName Name of the command (e.g., "inspectkit" or "inspectec") + */ + public static void showUsage(@NotNull Player player, @NotNull String commandName) { + BroadcastManager.get().sendComponentMessage(player, + ERROR_PREFIX.append( + mm.deserialize("Usage: /" + commandName + " "))); + } + + private static @Nullable UUID findCachedOfflinePlayerUuid(@NotNull String identifier) { + OfflinePlayer cachedOfflinePlayer = getOfflinePlayerIfCached(identifier); + if (cachedOfflinePlayer == null) { + return null; + } + + String cachedName = cachedOfflinePlayer.getName(); + if (cachedName == null || !cachedName.equalsIgnoreCase(identifier)) { + return null; + } + + return cachedOfflinePlayer.getUniqueId(); + } + + private static @Nullable OfflinePlayer getOfflinePlayerIfCached(@NotNull String identifier) { + Object server = Bukkit.getServer(); + if (server == null) { + return null; + } + + try { + Method method = server.getClass().getMethod("getOfflinePlayerIfCached", String.class); + Object offlinePlayer = method.invoke(server, identifier); + return offlinePlayer instanceof OfflinePlayer ? (OfflinePlayer) offlinePlayer : null; + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + private static @Nullable UUID lookupPlayerUuidFromMojang(@NotNull String identifier) { + try { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url("https://api.mojang.com/users/profiles/minecraft/" + identifier) + .build(); + Response response = client.newCall(request).execute(); + try { + if (!response.isSuccessful() || response.body() == null) { + return null; + } + + String body = response.body().string(); + int idStart = body.indexOf("\"id\":\""); + if (idStart < 0) { + return null; + } + + idStart += 6; + int idEnd = body.indexOf("\"", idStart); + if (idEnd <= idStart) { + return null; + } + + String raw = body.substring(idStart, idEnd); + String formatted = raw.substring(0, 8) + "-" + + raw.substring(8, 12) + "-" + + raw.substring(12, 16) + "-" + + raw.substring(16, 20) + "-" + + raw.substring(20); + return UUID.fromString(formatted); + } finally { + if (response.body() != null) { + response.body().close(); + } + } + } catch (IOException | IllegalArgumentException ignored) { + return null; + } + } + + static @Nullable UUID selectResolvedUuid(@NotNull String identifier, @Nullable UUID cachedOfflinePlayer, + @NotNull Supplier mojangLookup) { + if (cachedOfflinePlayer != null) { + return cachedOfflinePlayer; + } + + UUID mojangUuid = mojangLookup.get(); + if (mojangUuid != null) { + return mojangUuid; + } + + return null; + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectEcCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectEcCommand.java new file mode 100644 index 0000000..f85d127 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectEcCommand.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.inspect; + +import dev.noah.perplayerkit.KitManager; +import dev.noah.perplayerkit.gui.GUI; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import java.util.UUID; +public class InspectEcCommand extends AbstractInspectCommand { + + public InspectEcCommand(Plugin plugin) { + super(plugin); + } + + @Override + protected String usageCommand() { + return "inspectec"; + } + + @Override + protected boolean hasData(UUID targetUuid, int slot) { + return KitManager.get().hasEC(targetUuid, slot); + } + + @Override + protected void openInspectGui(Player inspector, UUID targetUuid, int slot) { + GUI gui = new GUI(plugin); + gui.InspectEc(inspector, targetUuid, slot); + } + + @Override + protected String missingDataMessage(String targetName, int slot) { + return "" + targetName + " does not have an enderchest in slot " + slot + ""; + } + + @Override + protected String loadErrorLogMessage() { + return "Error loading enderchest data"; + } + + @Override + protected String loadErrorUserMessage() { + return "An error occurred while loading enderchest data. See console for details."; + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectKitCommand.java new file mode 100644 index 0000000..810d631 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectKitCommand.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.inspect; + +import dev.noah.perplayerkit.KitManager; +import dev.noah.perplayerkit.gui.GUI; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import java.util.UUID; +public class InspectKitCommand extends AbstractInspectCommand { + + public InspectKitCommand(Plugin plugin) { + super(plugin); + } + + @Override + protected String usageCommand() { + return "inspectkit"; + } + + @Override + protected boolean hasData(UUID targetUuid, int slot) { + return KitManager.get().hasKit(targetUuid, slot); + } + + @Override + protected void openInspectGui(Player inspector, UUID targetUuid, int slot) { + GUI gui = new GUI(plugin); + gui.InspectKit(inspector, targetUuid, slot); + } + + @Override + protected String missingDataMessage(String targetName, int slot) { + return "" + targetName + " does not have a kit in slot " + slot + ""; + } + + @Override + protected String loadErrorLogMessage() { + return "Error loading kit data"; + } + + @Override + protected String loadErrorUserMessage() { + return "An error occurred while loading kit data. See console for details."; + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java similarity index 52% rename from src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java index c9384a8..6e9a33c 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java @@ -16,11 +16,11 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.kits; -import com.google.common.primitives.Ints; -import dev.noah.perplayerkit.KitShareManager; -import dev.noah.perplayerkit.util.CooldownManager; +import dev.noah.perplayerkit.KitManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; +import dev.noah.perplayerkit.commands.core.SlotArgumentParser; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -29,44 +29,45 @@ import dev.noah.perplayerkit.util.SoundManager; import org.jetbrains.annotations.NotNull; -public class ShareECKitCommand implements CommandExecutor { - - private final CooldownManager shareECCommandCooldown; - - public ShareECKitCommand() { - this.shareECCommandCooldown = new CooldownManager(5); - } +import java.util.UUID; +public class DeleteKitCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - - if (!(sender instanceof Player player)) { - sender.sendMessage("Only players can use this command"); + Player player = CommandGuards.requirePlayer(sender, ChatColor.RED + "Only Players can use this!"); + if (player == null) { return true; } - if (args.length < 1) { - player.sendMessage(ChatColor.RED + "Error, you must select a EC slot to share"); + UUID uuid = player.getUniqueId(); + if (args.length != 1) { + player.sendMessage(ChatColor.RED + "Usage: /deletekit "); SoundManager.playFailure(player); return true; } - if (shareECCommandCooldown.isOnCooldown(player)) { - player.sendMessage(ChatColor.RED + "Please don't spam the command (5 second cooldown)"); + Integer slot = SlotArgumentParser.parseSlotInRange(args[0], 1, 9); + KitManager kitManager = KitManager.get(); + if (slot == null) { + player.sendMessage(ChatColor.RED + "Usage: /deletekit "); + player.sendMessage(ChatColor.RED + "Select a real number"); SoundManager.playFailure(player); return true; } - Integer slot = Ints.tryParse(args[0]); - - if (slot == null || slot < 1 || slot > 9) { - player.sendMessage(ChatColor.RED + "Select a valid kit slot"); + if (!kitManager.hasKit(uuid, slot)) { + player.sendMessage(ChatColor.RED + "Kit " + slot + " doesnt exist!"); SoundManager.playFailure(player); return true; } - KitShareManager.get().shareEC(player, slot); - shareECCommandCooldown.setCooldown(player); + if (kitManager.deleteKit(uuid, slot)) { + player.sendMessage(ChatColor.GREEN + "Kit " + slot + " deleted!"); + SoundManager.playSuccess(player); + } else { + player.sendMessage(ChatColor.RED + "Kit deletion failed!"); + SoundManager.playFailure(player); + } return true; } diff --git a/src/main/java/dev/noah/perplayerkit/commands/EnderchestCommand.java b/src/main/java/dev/noah/perplayerkit/commands/kits/EnderchestCommand.java similarity index 77% rename from src/main/java/dev/noah/perplayerkit/commands/EnderchestCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/EnderchestCommand.java index f8d6c0e..49bf713 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/EnderchestCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/EnderchestCommand.java @@ -16,16 +16,16 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.kits; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.gui.ItemUtil; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.util.StyleManager; +import dev.noah.perplayerkit.util.SoundManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import dev.noah.perplayerkit.util.SoundManager; import org.bukkit.inventory.ItemStack; import org.ipvp.canvas.Menu; import org.ipvp.canvas.type.ChestMenu; @@ -34,21 +34,16 @@ public class EnderchestCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (sender instanceof Player player) { - - if (DisabledCommand.isBlockedInWorld(player)) { - return true; - } - viewOnlyEC(player); + Player player = CommandGuards.requirePlayerInEnabledWorld(sender); + if (player == null) { return true; } - sender.sendMessage("Only players can use this command"); - if (sender instanceof Player s) SoundManager.playFailure(s); + viewOnlyEC(player); return true; } - public void viewOnlyEC(Player p) { + public void viewOnlyEC(Player player) { ItemStack fill = ItemUtil.createGlassPane(); @@ -62,13 +57,11 @@ public void viewOnlyEC(Player p) { menu.getSlot(i).setItem(fill); } // set the items in the inventory to the items in the enderchest - ItemStack[] items = p.getEnderChest().getContents(); + ItemStack[] items = player.getEnderChest().getContents(); for (int i = 0; i < 27; i++) { menu.getSlot(i + 9).setItem(items[i]); } - menu.open(p); - SoundManager.playOpenGui(p); + menu.open(player); + SoundManager.playOpenGui(player); } } - - diff --git a/src/main/java/dev/noah/perplayerkit/commands/MainMenuCommand.java b/src/main/java/dev/noah/perplayerkit/commands/kits/MainMenuCommand.java similarity index 83% rename from src/main/java/dev/noah/perplayerkit/commands/MainMenuCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/MainMenuCommand.java index 63acebf..1a8e14b 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/MainMenuCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/MainMenuCommand.java @@ -16,9 +16,9 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.kits; -import dev.noah.perplayerkit.util.DisabledCommand; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.gui.GUI; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -29,21 +29,21 @@ public class MainMenuCommand implements CommandExecutor { - private Plugin plugin; + private final Plugin plugin; + public MainMenuCommand(Plugin plugin) { this.plugin = plugin; } @Override public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) { - Player p = (Player) commandSender; - - if (DisabledCommand.isBlockedInWorld(p)) { + Player player = CommandGuards.requirePlayerInEnabledWorld(commandSender); + if (player == null) { return true; } GUI main = new GUI(plugin); - main.OpenMainMenu(p); + main.OpenMainMenu(player); return true; } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/kits/PublicKitCommand.java similarity index 83% rename from src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/PublicKitCommand.java index 47d51d4..b365bb9 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/PublicKitCommand.java @@ -16,10 +16,10 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.kits; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.KitManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.gui.GUI; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -35,51 +35,40 @@ public class PublicKitCommand implements CommandExecutor, TabCompleter { - private Plugin plugin; + private final Plugin plugin; + public PublicKitCommand(Plugin plugin) { this.plugin = plugin; } + @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player player)) { - sender.sendMessage("Only players can use this command"); - return true; - } - - if (DisabledCommand.isBlockedInWorld(player)) { + Player player = CommandGuards.requirePlayerInEnabledWorld(sender); + if (player == null) { return true; } - //if args.length<1 open the kit menu if (args.length < 1) { GUI kitMenu = new GUI(plugin); kitMenu.OpenPublicKitMenu(player); return true; } - //if args.length==1 open the kit menu with the kit String kitName = args[0]; KitManager.get().loadPublicKit(player, kitName); return true; - - } @Override public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { if (args.length == 1) { - List list = new ArrayList<>(); KitManager.get().getPublicKitList().forEach((kit) -> list.add(kit.id)); - return list; - } return null; - - } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/SwapKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java similarity index 86% rename from src/main/java/dev/noah/perplayerkit/commands/SwapKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java index 06815c6..3fc02da 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/SwapKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java @@ -16,10 +16,11 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.kits; -import com.google.common.primitives.Ints; import dev.noah.perplayerkit.KitManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; +import dev.noah.perplayerkit.commands.core.SlotArgumentParser; import org.bukkit.ChatColor; import dev.noah.perplayerkit.util.SoundManager; import org.bukkit.command.Command; @@ -34,8 +35,8 @@ public class SwapKitCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player player)) { - sender.sendMessage(ChatColor.RED + "Only Players can use this!"); + Player player = CommandGuards.requirePlayer(sender, ChatColor.RED + "Only Players can use this!"); + if (player == null) { return true; } @@ -45,8 +46,8 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - Integer slot1 = Ints.tryParse(args[0]); - Integer slot2 = Ints.tryParse(args[1]); + Integer slot1 = SlotArgumentParser.parseSlotInRange(args[0], 1, 9); + Integer slot2 = SlotArgumentParser.parseSlotInRange(args[1], 1, 9); if (slot1 == null || slot2 == null) { player.sendMessage(ChatColor.RED + "Usage: /swapkit "); diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/share/AbstractShareSlotCommand.java similarity index 60% rename from src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/share/AbstractShareSlotCommand.java index 52c4b89..96e7c4a 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/share/AbstractShareSlotCommand.java @@ -16,58 +16,61 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.share; -import com.google.common.primitives.Ints; -import dev.noah.perplayerkit.KitShareManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; +import dev.noah.perplayerkit.commands.core.SlotArgumentParser; import dev.noah.perplayerkit.util.CooldownManager; +import dev.noah.perplayerkit.util.SoundManager; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import dev.noah.perplayerkit.util.SoundManager; import org.jetbrains.annotations.NotNull; -public class ShareKitCommand implements CommandExecutor { +import java.util.function.BiConsumer; + +public abstract class AbstractShareSlotCommand implements CommandExecutor { - private final CooldownManager shareKitCommandCooldown; + private static final int COOLDOWN_SECONDS = 5; + private final CooldownManager cooldownManager = new CooldownManager(COOLDOWN_SECONDS); + private final String missingSlotMessage; + private final BiConsumer shareAction; - public ShareKitCommand() { - this.shareKitCommandCooldown = new CooldownManager(5); + protected AbstractShareSlotCommand(String missingSlotMessage, BiConsumer shareAction) { + this.missingSlotMessage = missingSlotMessage; + this.shareAction = shareAction; } @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - - if (!(sender instanceof Player player)) { - sender.sendMessage("Only players can use this command"); + Player player = CommandGuards.requirePlayer(sender); + if (player == null) { return true; } if (args.length < 1) { - player.sendMessage(ChatColor.RED + "Error, you must select a kit slot to share"); + player.sendMessage(ChatColor.RED + missingSlotMessage); SoundManager.playFailure(player); return true; } - if (shareKitCommandCooldown.isOnCooldown(player)) { + if (cooldownManager.isOnCooldown(player)) { player.sendMessage(ChatColor.RED + "Please don't spam the command (5 second cooldown)"); SoundManager.playFailure(player); return true; } - Integer slot = Ints.tryParse(args[0]); - - if (slot == null || slot < 1 || slot > 9) { + Integer slot = SlotArgumentParser.parseSlotInRange(args[0], 1, 9); + if (slot == null) { player.sendMessage(ChatColor.RED + "Select a valid kit slot"); SoundManager.playFailure(player); return true; } - KitShareManager.get().shareKit(player, slot); - shareKitCommandCooldown.setCooldown(player); - + shareAction.accept(player, slot); + cooldownManager.setCooldown(player); return true; } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/CopyKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/share/CopyKitCommand.java similarity index 69% rename from src/main/java/dev/noah/perplayerkit/commands/CopyKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/share/CopyKitCommand.java index 2488c41..e171378 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/CopyKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/share/CopyKitCommand.java @@ -16,10 +16,10 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.share; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.KitShareManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -31,24 +31,18 @@ public class CopyKitCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + Player player = CommandGuards.requirePlayerInEnabledWorld(sender); + if (player == null) { + return true; + } - if (sender instanceof Player player) { - - if (DisabledCommand.isBlockedInWorld(player)) { - return true; - } - - - if (args.length > 0) { - KitShareManager.get().copyKit(player, args[0]); - } else { - player.sendMessage(ChatColor.RED + "Error, you must enter a kit code to copy"); - SoundManager.playFailure(player); - } + if (args.length > 0) { + KitShareManager.get().copyKit(player, args[0]); } else { - sender.sendMessage("Only players can use this command"); + player.sendMessage(ChatColor.RED + "Error, you must enter a kit code to copy"); + SoundManager.playFailure(player); } return true; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java new file mode 100644 index 0000000..6951e73 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.share; + +import dev.noah.perplayerkit.KitShareManager; + +public class ShareECKitCommand extends AbstractShareSlotCommand { + + public ShareECKitCommand() { + super("Error, you must select an EC slot to share", (player, slot) -> KitShareManager.get().shareEC(player, slot)); + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/share/ShareKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/share/ShareKitCommand.java new file mode 100644 index 0000000..7dc567b --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/share/ShareKitCommand.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.share; + +import dev.noah.perplayerkit.KitShareManager; + +public class ShareKitCommand extends AbstractShareSlotCommand { + + public ShareKitCommand() { + super("Error, you must select a kit slot to share", (player, slot) -> KitShareManager.get().shareKit(player, slot)); + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/AbstractShortSlotCommand.java similarity index 53% rename from src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/shortcuts/AbstractShortSlotCommand.java index 20f864e..ad0a027 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/AbstractShortSlotCommand.java @@ -16,23 +16,29 @@ * You should have received a copy of the GNU Affero General Public License * along with PerPlayerKit. If not, see . */ -package dev.noah.perplayerkit.commands; +package dev.noah.perplayerkit.commands.shortcuts; import dev.noah.perplayerkit.util.DisabledCommand; -import dev.noah.perplayerkit.KitManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import java.util.UUID; +import java.util.Locale; -public class ShortKitCommand implements CommandExecutor { +public abstract class AbstractShortSlotCommand implements CommandExecutor { + + private final String shortPrefix; + private final String longPrefix; + + protected AbstractShortSlotCommand(String shortPrefix, String longPrefix) { + this.shortPrefix = shortPrefix; + this.longPrefix = longPrefix; + } @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player player)) { sender.sendMessage("Only players can use this command."); return true; @@ -42,19 +48,37 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - UUID uuid = player.getUniqueId(); - - // Check if the label matches "kX" or "kitX" where X is a number between 1 and 9 - if (label.matches("k[1-9]")) { - int kitNumber = Integer.parseInt(label.substring(1)); // Extract the number for "kX" - KitManager.get().loadKit(player, kitNumber); - } else if (label.matches("kit[1-9]")) { - int kitNumber = Integer.parseInt(label.substring(3)); // Extract the number for "kitX" - KitManager.get().loadKit(player, kitNumber); - } else { + Integer slot = parseSlot(label); + if (slot == null) { player.sendMessage("Invalid command label."); + return true; } + executeForSlot(player, slot); return true; } + + protected abstract void executeForSlot(Player player, int slot); + + private Integer parseSlot(String label) { + String normalizedLabel = label.toLowerCase(Locale.ROOT); + Integer fromShort = parseSingleDigitSuffix(normalizedLabel, shortPrefix); + if (fromShort != null) { + return fromShort; + } + return parseSingleDigitSuffix(normalizedLabel, longPrefix); + } + + private Integer parseSingleDigitSuffix(String label, String prefix) { + if (!label.startsWith(prefix) || label.length() != prefix.length() + 1) { + return null; + } + + char slot = label.charAt(prefix.length()); + if (slot < '1' || slot > '9') { + return null; + } + + return slot - '0'; + } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortECCommand.java b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortECCommand.java new file mode 100644 index 0000000..0a86b27 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortECCommand.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.shortcuts; + +import dev.noah.perplayerkit.KitManager; +import org.bukkit.entity.Player; + +public class ShortECCommand extends AbstractShortSlotCommand { + + public ShortECCommand() { + super("ec", "enderchest"); + } + + @Override + protected void executeForSlot(Player player, int slot) { + KitManager.get().loadEnderchest(player, slot); + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortKitCommand.java new file mode 100644 index 0000000..368ee32 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortKitCommand.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.commands.shortcuts; + +import dev.noah.perplayerkit.KitManager; +import org.bukkit.entity.Player; + +public class ShortKitCommand extends AbstractShortSlotCommand { + + public ShortKitCommand() { + super("k", "kit"); + } + + @Override + protected void executeForSlot(Player player, int slot) { + KitManager.get().loadKit(player, slot); + } +} diff --git a/src/main/java/dev/noah/perplayerkit/gui/GUI.java b/src/main/java/dev/noah/perplayerkit/gui/GUI.java index 03fb6f4..52a5afe 100644 --- a/src/main/java/dev/noah/perplayerkit/gui/GUI.java +++ b/src/main/java/dev/noah/perplayerkit/gui/GUI.java @@ -24,17 +24,13 @@ import dev.noah.perplayerkit.PublicKit; import dev.noah.perplayerkit.util.*; import net.md_5.bungee.api.ChatColor; -import org.bukkit.Bukkit; import org.bukkit.Material; -import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.ipvp.canvas.Menu; -import org.ipvp.canvas.slot.ClickOptions; import org.ipvp.canvas.slot.Slot; -import org.ipvp.canvas.type.ChestMenu; import java.util.HashMap; import java.util.HashSet; @@ -46,6 +42,8 @@ import static dev.noah.perplayerkit.gui.ItemUtil.addHideFlags; import static dev.noah.perplayerkit.gui.ItemUtil.createItem; import static dev.noah.perplayerkit.gui.ItemUtil.createGlassPane; +import static dev.noah.perplayerkit.gui.GuiLayoutUtils.*; +import static dev.noah.perplayerkit.util.PlayerUtil.getPlayerName; public class GUI { private final Plugin plugin; @@ -74,105 +72,77 @@ public static void addLoadPublicKit(Slot slot, String id) { }); } - public static Menu createPublicKitMenu() { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Public Kit Room").redraw(true).build(); - } - public static boolean removeKitDeletionFlag(Player player) { return kitDeletionFlag.remove(player.getUniqueId()); } public void OpenKitMenu(Player p, int slot) { - Menu menu = createKitMenu(slot); + Menu menu = GuiMenuFactory.createKitMenu(slot); if (KitManager.get().getItemStackArrayById(p.getUniqueId().toString() + slot) != null) { ItemStack[] kit = KitManager.get().getItemStackArrayById(p.getUniqueId().toString() + slot); - for (int i = 0; i < 41; i++) { + for (int i = 0; i < KIT_CONTENT_END; i++) { menu.getSlot(i).setItem(kit[i]); } } - for (int i = 0; i < 41; i++) { - allowModification(menu.getSlot(i)); - } - for (int i = 41; i < 54; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); - } - menu.getSlot(45).setItem(createItem(Material.CHAINMAIL_BOOTS, 1, "BOOTS")); - menu.getSlot(46).setItem(createItem(Material.CHAINMAIL_LEGGINGS, 1, "LEGGINGS")); - menu.getSlot(47).setItem(createItem(Material.CHAINMAIL_CHESTPLATE, 1, "CHESTPLATE")); - menu.getSlot(48).setItem(createItem(Material.CHAINMAIL_HELMET, 1, "HELMET")); - menu.getSlot(49).setItem(createItem(Material.SHIELD, 1, "OFFHAND")); - - menu.getSlot(51).setItem(createItem(Material.CHEST, 1, "IMPORT", "● Import from inventory")); - menu.getSlot(52).setItem(createItem(Material.BARRIER, 1, "CLEAR KIT", "● Shift click to clear")); - menu.getSlot(53).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); - addMainButton(menu.getSlot(53)); - addClear(menu.getSlot(52)); - addImport(menu.getSlot(51)); + allowModificationRange(menu, 0, KIT_CONTENT_END); + setGlassPaneRange(menu, KIT_CONTENT_END, MENU_SIZE); + setArmorAndOffhandIndicators(menu); + + menu.getSlot(IMPORT_SLOT).setItem(createItem(Material.CHEST, 1, "IMPORT", "● Import from inventory")); + menu.getSlot(CLEAR_SLOT).setItem(createItem(Material.BARRIER, 1, "CLEAR KIT", "● Shift click to clear")); + menu.getSlot(BACK_SLOT).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); + addMainButton(menu.getSlot(BACK_SLOT)); + addClear(menu.getSlot(CLEAR_SLOT)); + addImport(menu.getSlot(IMPORT_SLOT)); menu.setCursorDropHandler(Menu.ALLOW_CURSOR_DROPPING); menu.open(p); } public void OpenPublicKitEditor(Player p, String kitId) { - Menu menu = createPublicKitMenu(kitId); + Menu menu = GuiMenuFactory.createPublicKitMenu(kitId); if (KitManager.get().getItemStackArrayById(IDUtil.getPublicKitId(kitId)) != null) { ItemStack[] kit = KitManager.get().getItemStackArrayById(IDUtil.getPublicKitId(kitId)); - for (int i = 0; i < 41; i++) { + for (int i = 0; i < KIT_CONTENT_END; i++) { menu.getSlot(i).setItem(kit[i]); } } - for (int i = 0; i < 41; i++) { - allowModification(menu.getSlot(i)); - } - for (int i = 41; i < 54; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); - } - menu.getSlot(45).setItem(createItem(Material.CHAINMAIL_BOOTS, 1, "BOOTS")); - menu.getSlot(46).setItem(createItem(Material.CHAINMAIL_LEGGINGS, 1, "LEGGINGS")); - menu.getSlot(47).setItem(createItem(Material.CHAINMAIL_CHESTPLATE, 1, "CHESTPLATE")); - menu.getSlot(48).setItem(createItem(Material.CHAINMAIL_HELMET, 1, "HELMET")); - menu.getSlot(49).setItem(createItem(Material.SHIELD, 1, "OFFHAND")); - - menu.getSlot(51).setItem(createItem(Material.CHEST, 1, "IMPORT", "● Import from inventory")); - menu.getSlot(52).setItem(createItem(Material.BARRIER, 1, "CLEAR KIT", "● Shift click to clear")); - menu.getSlot(53).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); - addMainButton(menu.getSlot(53)); - addClear(menu.getSlot(52)); - addImport(menu.getSlot(51)); + allowModificationRange(menu, 0, KIT_CONTENT_END); + setGlassPaneRange(menu, KIT_CONTENT_END, MENU_SIZE); + setArmorAndOffhandIndicators(menu); + + menu.getSlot(IMPORT_SLOT).setItem(createItem(Material.CHEST, 1, "IMPORT", "● Import from inventory")); + menu.getSlot(CLEAR_SLOT).setItem(createItem(Material.BARRIER, 1, "CLEAR KIT", "● Shift click to clear")); + menu.getSlot(BACK_SLOT).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); + addMainButton(menu.getSlot(BACK_SLOT)); + addClear(menu.getSlot(CLEAR_SLOT)); + addImport(menu.getSlot(IMPORT_SLOT)); menu.setCursorDropHandler(Menu.ALLOW_CURSOR_DROPPING); menu.open(p); } public void OpenECKitKenu(Player p, int slot) { - Menu menu = createECMenu(slot); - - for (int i = 0; i < 9; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); + Menu menu = GuiMenuFactory.createECMenu(slot); - } - for (int i = 36; i < 54; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); - - } + setGlassPaneRange(menu, 0, EC_CONTENT_START); + setGlassPaneRange(menu, EC_CONTENT_END, MENU_SIZE); if (KitManager.get().getItemStackArrayById(p.getUniqueId() + "ec" + slot) != null) { ItemStack[] kit = KitManager.get().getItemStackArrayById(p.getUniqueId() + "ec" + slot); - for (int i = 9; i < 36; i++) { - menu.getSlot(i).setItem(kit[i - 9]); + for (int i = EC_CONTENT_START; i < EC_CONTENT_END; i++) { + menu.getSlot(i).setItem(kit[i - EC_CONTENT_START]); } } - for (int i = 9; i < 36; i++) { - allowModification(menu.getSlot(i)); - } - menu.getSlot(51).setItem(createItem(Material.ENDER_CHEST, 1, "IMPORT", "● Import from enderchest")); - menu.getSlot(52).setItem(createItem(Material.BARRIER, 1, "CLEAR KIT", "● Shift click to clear")); - menu.getSlot(53).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); - addMainButton(menu.getSlot(53)); - addClear(menu.getSlot(52), 9, 36); - addImportEC(menu.getSlot(51)); + allowModificationRange(menu, EC_CONTENT_START, EC_CONTENT_END); + menu.getSlot(IMPORT_SLOT).setItem(createItem(Material.ENDER_CHEST, 1, "IMPORT", "● Import from enderchest")); + menu.getSlot(CLEAR_SLOT).setItem(createItem(Material.BARRIER, 1, "CLEAR KIT", "● Shift click to clear")); + menu.getSlot(BACK_SLOT).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); + addMainButton(menu.getSlot(BACK_SLOT)); + addClear(menu.getSlot(CLEAR_SLOT), EC_CONTENT_START, EC_CONTENT_END); + addImportEC(menu.getSlot(IMPORT_SLOT)); menu.setCursorDropHandler(Menu.ALLOW_CURSOR_DROPPING); menu.open(p); } @@ -180,36 +150,28 @@ public void OpenECKitKenu(Player p, int slot) { public void InspectKit(Player p, UUID target, int slot) { setInspectTarget(p.getUniqueId(), target); String playerName = getPlayerName(target); - Menu menu = createInspectMenu(slot, playerName); + Menu menu = GuiMenuFactory.createInspectMenu(slot, playerName); if (KitManager.get().hasKit(target, slot)) { ItemStack[] kit = KitManager.get().getItemStackArrayById(target.toString() + slot); - for (int i = 0; i < 41; i++) { + for (int i = 0; i < KIT_CONTENT_END; i++) { menu.getSlot(i).setItem(kit[i]); } } - for (int i = 41; i < 54; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); - } - menu.getSlot(45).setItem(createItem(Material.CHAINMAIL_BOOTS, 1, "BOOTS")); - menu.getSlot(46).setItem(createItem(Material.CHAINMAIL_LEGGINGS, 1, "LEGGINGS")); - menu.getSlot(47).setItem(createItem(Material.CHAINMAIL_CHESTPLATE, 1, "CHESTPLATE")); - menu.getSlot(48).setItem(createItem(Material.CHAINMAIL_HELMET, 1, "HELMET")); - menu.getSlot(49).setItem(createItem(Material.SHIELD, 1, "OFFHAND")); - - menu.getSlot(53).setItem(createItem(Material.OAK_DOOR, 1, "CLOSE")); - menu.getSlot(53).setClickHandler((player, info) -> { + setGlassPaneRange(menu, KIT_CONTENT_END, MENU_SIZE); + setArmorAndOffhandIndicators(menu); + + menu.getSlot(BACK_SLOT).setItem(createItem(Material.OAK_DOOR, 1, "CLOSE")); + menu.getSlot(BACK_SLOT).setClickHandler((player, info) -> { SoundManager.playClick(player); info.getClickedMenu().close(); SoundManager.playCloseGui(player); }); if (p.hasPermission("perplayerkit.admin")) { - for (int i = 0; i < 41; i++) { - allowModification(menu.getSlot(i)); - } - menu.getSlot(52).setItem(createItem(Material.BARRIER, 1, "CLEAR KIT", "● Shift click to delete kit")); - addClearKit(menu.getSlot(52), target, slot); + allowModificationRange(menu, 0, KIT_CONTENT_END); + menu.getSlot(CLEAR_SLOT).setItem(createItem(Material.BARRIER, 1, "CLEAR KIT", "● Shift click to delete kit")); + addClearKit(menu.getSlot(CLEAR_SLOT), target, slot); } menu.setCursorDropHandler(Menu.ALLOW_CURSOR_DROPPING); @@ -220,37 +182,32 @@ public void InspectKit(Player p, UUID target, int slot) { public void InspectEc(Player p, UUID target, int slot) { setInspectTarget(p.getUniqueId(), target); String playerName = getPlayerName(target); - Menu menu = createInspectEcMenu(slot, playerName); - - for (int i = 0; i < 9; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); - + if (playerName == null) { + playerName = target.toString(); } - for (int i = 36; i < 54; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); + Menu menu = GuiMenuFactory.createInspectEcMenu(slot, playerName); - } + setGlassPaneRange(menu, 0, EC_CONTENT_START); + setGlassPaneRange(menu, EC_CONTENT_END, MENU_SIZE); if (KitManager.get().getItemStackArrayById(target + "ec" + slot) != null) { ItemStack[] kit = KitManager.get().getItemStackArrayById(target + "ec" + slot); - for (int i = 9; i < 36; i++) { - menu.getSlot(i).setItem(kit[i - 9]); + for (int i = EC_CONTENT_START; i < EC_CONTENT_END; i++) { + menu.getSlot(i).setItem(kit[i - EC_CONTENT_START]); } } - menu.getSlot(53).setItem(createItem(Material.OAK_DOOR, 1, "CLOSE")); - menu.getSlot(53).setClickHandler((player, info) -> { + menu.getSlot(BACK_SLOT).setItem(createItem(Material.OAK_DOOR, 1, "CLOSE")); + menu.getSlot(BACK_SLOT).setClickHandler((player, info) -> { SoundManager.playClick(player); info.getClickedMenu().close(); SoundManager.playCloseGui(player); }); if (p.hasPermission("perplayerkit.admin")) { - for (int i = 9; i < 36; i++) { - allowModification(menu.getSlot(i)); - } - menu.getSlot(52).setItem(createItem(Material.BARRIER, 1, "CLEAR ENDERCHEST", "● Shift click to delete enderchest")); - addClearEnderchest(menu.getSlot(52), target, slot); + allowModificationRange(menu, EC_CONTENT_START, EC_CONTENT_END); + menu.getSlot(CLEAR_SLOT).setItem(createItem(Material.BARRIER, 1, "CLEAR ENDERCHEST", "● Shift click to delete enderchest")); + addClearEnderchest(menu.getSlot(CLEAR_SLOT), target, slot); } menu.setCursorDropHandler(Menu.ALLOW_CURSOR_DROPPING); @@ -259,8 +216,8 @@ public void InspectEc(Player p, UUID target, int slot) { } public void OpenMainMenu(Player p) { - Menu menu = createMainMenu(p); - for (int i = 0; i < 54; i++) { + Menu menu = GuiMenuFactory.createMainMenu(p); + for (int i = 0; i < MENU_SIZE; i++) { menu.getSlot(i).setItem(createGlassPane()); } for (int i = 9; i < 18; i++) { @@ -309,15 +266,11 @@ public void OpenKitRoom(Player p) { } public void OpenKitRoom(Player p, int page) { - Menu menu = createKitRoom(); - for (int i = 0; i < 45; i++) { - allowModification(menu.getSlot(i)); - } - for (int i = 45; i < 54; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); - } + Menu menu = GuiMenuFactory.createKitRoomMenu(); + allowModificationRange(menu, 0, FOOTER_START); + setGlassPaneRange(menu, FOOTER_START, MENU_SIZE); if (KitRoomDataManager.get().getKitRoomPage(page) != null) { - for (int i = 0; i < 45; i++) { + for (int i = 0; i < FOOTER_START; i++) { menu.getSlot(i).setItem(KitRoomDataManager.get().getKitRoomPage(page)[i]); } } @@ -357,9 +310,9 @@ public Menu ViewPublicKitMenu(Player p, String id) { } return null; } - Menu menu = ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Viewing Public Kit: " + id).redraw(true).build(); + Menu menu = GuiMenuFactory.createViewPublicKitMenu(id); - for (int i = 0; i < 54; i++) { + for (int i = 0; i < MENU_SIZE; i++) { menu.getSlot(i).setItem(ItemUtil.createGlassPane()); } @@ -373,16 +326,11 @@ public Menu ViewPublicKitMenu(Player p, String id) { menu.getSlot(i + 9).setItem(kit[i]); } - menu.getSlot(45).setItem(createItem(Material.CHAINMAIL_BOOTS, 1, "BOOTS")); - menu.getSlot(46).setItem(createItem(Material.CHAINMAIL_LEGGINGS, 1, "LEGGINGS")); - menu.getSlot(47).setItem(createItem(Material.CHAINMAIL_CHESTPLATE, 1, "CHESTPLATE")); - menu.getSlot(48).setItem(createItem(Material.CHAINMAIL_HELMET, 1, "HELMET")); - menu.getSlot(49).setItem(createItem(Material.SHIELD, 1, "OFFHAND")); - - menu.getSlot(52).setItem(createItem(Material.APPLE, 1, "LOAD KIT")); - menu.getSlot(53).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); - addPublicKitMenu(menu.getSlot(53)); - addLoadPublicKit(menu.getSlot(52), id); + setArmorAndOffhandIndicators(menu); + menu.getSlot(LOAD_PUBLIC_KIT_SLOT).setItem(createItem(Material.APPLE, 1, "LOAD KIT")); + menu.getSlot(BACK_SLOT).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); + addPublicKitMenu(menu.getSlot(BACK_SLOT)); + addLoadPublicKit(menu.getSlot(LOAD_PUBLIC_KIT_SLOT), id); menu.open(p); @@ -390,8 +338,8 @@ public Menu ViewPublicKitMenu(Player p, String id) { } public void OpenPublicKitMenu(Player player) { - Menu menu = createPublicKitMenu(); - for (int i = 0; i < 54; i++) { + Menu menu = GuiMenuFactory.createPublicKitRoomMenu(); + for (int i = 0; i < MENU_SIZE; i++) { menu.getSlot(i).setItem(ItemUtil.createGlassPane()); } @@ -422,9 +370,8 @@ public void OpenPublicKitMenu(Player player) { } } - addMainButton(menu.getSlot(53)); - - menu.getSlot(53).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); + addMainButton(menu.getSlot(BACK_SLOT)); + menu.getSlot(BACK_SLOT).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); menu.open(player); } @@ -662,47 +609,4 @@ public void addEditLoadEC(Slot slot, int i) { } }); } - - public Menu createKitMenu(int slot) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Kit: " + slot).build(); - } - - public Menu createPublicKitMenu(String id) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Public Kit: " + id).build(); - } - - public Menu createECMenu(int slot) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Enderchest: " + slot).build(); - } - - public Menu createInspectMenu(int slot, String playerName) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Inspecting " + playerName + "'s kit " + slot).build(); - } - - public Menu createInspectEcMenu(int slot, String playerName) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Inspecting " + playerName + "'s enderchest " + slot).build(); - } - - public Menu createMainMenu(Player p) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + p.getName() + "'s Kits").build(); - } - - public Menu createKitRoom() { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Kit Room").redraw(true).build(); - } - - public void allowModification(Slot slot) { - ClickOptions options = ClickOptions.ALLOW_ALL; - slot.setClickOptions(options); - } - - private String getPlayerName(UUID uuid) { - Player onlinePlayer = Bukkit.getPlayer(uuid); - if (onlinePlayer != null) { - return onlinePlayer.getName(); - } - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); - String name = offlinePlayer.getName(); - return name != null ? name : uuid.toString(); - } -} \ No newline at end of file +} diff --git a/src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java b/src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java new file mode 100644 index 0000000..e225370 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.gui; + +import org.bukkit.Material; +import org.ipvp.canvas.Menu; +import org.ipvp.canvas.slot.ClickOptions; + +import static dev.noah.perplayerkit.gui.ItemUtil.createGlassPane; +import static dev.noah.perplayerkit.gui.ItemUtil.createItem; + +public final class GuiLayoutUtils { + public static final int MENU_SIZE = 54; + public static final int KIT_CONTENT_END = 41; + public static final int EC_CONTENT_START = 9; + public static final int EC_CONTENT_END = 36; + public static final int FOOTER_START = 45; + public static final int ARMOR_INDICATOR_START = 45; + public static final int OFFHAND_INDICATOR_SLOT = 49; + public static final int IMPORT_SLOT = 51; + public static final int LOAD_PUBLIC_KIT_SLOT = 52; + public static final int CLEAR_SLOT = 52; + public static final int BACK_SLOT = 53; + + private GuiLayoutUtils() { + } + + public static void allowModificationRange(Menu menu, int startInclusive, int endExclusive) { + for (int i = startInclusive; i < endExclusive; i++) { + menu.getSlot(i).setClickOptions(ClickOptions.ALLOW_ALL); + } + } + + public static void setGlassPaneRange(Menu menu, int startInclusive, int endExclusive) { + for (int i = startInclusive; i < endExclusive; i++) { + menu.getSlot(i).setItem(createGlassPane()); + } + } + + public static void setArmorAndOffhandIndicators(Menu menu) { + menu.getSlot(ARMOR_INDICATOR_START).setItem(createItem(Material.CHAINMAIL_BOOTS, 1, "BOOTS")); + menu.getSlot(ARMOR_INDICATOR_START + 1).setItem(createItem(Material.CHAINMAIL_LEGGINGS, 1, "LEGGINGS")); + menu.getSlot(ARMOR_INDICATOR_START + 2).setItem(createItem(Material.CHAINMAIL_CHESTPLATE, 1, "CHESTPLATE")); + menu.getSlot(ARMOR_INDICATOR_START + 3).setItem(createItem(Material.CHAINMAIL_HELMET, 1, "HELMET")); + menu.getSlot(OFFHAND_INDICATOR_SLOT).setItem(createItem(Material.SHIELD, 1, "OFFHAND")); + } +} diff --git a/src/main/java/dev/noah/perplayerkit/gui/GuiMenuFactory.java b/src/main/java/dev/noah/perplayerkit/gui/GuiMenuFactory.java new file mode 100644 index 0000000..36d894b --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/gui/GuiMenuFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022-2025 Noah Ross + * + * This file is part of PerPlayerKit. + * + * PerPlayerKit is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * PerPlayerKit is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for + * more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PerPlayerKit. If not, see . + */ +package dev.noah.perplayerkit.gui; + +import dev.noah.perplayerkit.util.StyleManager; +import org.bukkit.entity.Player; +import org.ipvp.canvas.Menu; +import org.ipvp.canvas.type.ChestMenu; + +public final class GuiMenuFactory { + private GuiMenuFactory() { + } + + public static Menu createPublicKitRoomMenu() { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Public Kit Room").redraw(true).build(); + } + + public static Menu createKitMenu(int slot) { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Kit: " + slot).build(); + } + + public static Menu createPublicKitMenu(String id) { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Public Kit: " + id).build(); + } + + public static Menu createECMenu(int slot) { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Enderchest: " + slot).build(); + } + + public static Menu createInspectMenu(int slot, String playerName) { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Inspecting " + playerName + "'s kit " + slot).build(); + } + + public static Menu createInspectEcMenu(int slot, String playerName) { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Inspecting " + playerName + "'s enderchest " + slot).build(); + } + + public static Menu createMainMenu(Player player) { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + player.getName() + "'s Kits").build(); + } + + public static Menu createKitRoomMenu() { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Kit Room").redraw(true).build(); + } + + public static Menu createViewPublicKitMenu(String id) { + return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Viewing Public Kit: " + id).redraw(true).build(); + } +} diff --git a/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java b/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java index 4b02472..ca11158 100644 --- a/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java +++ b/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java @@ -18,7 +18,6 @@ */ package dev.noah.perplayerkit.storage; -import dev.noah.perplayerkit.PerPlayerKit; import org.bukkit.plugin.Plugin; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; @@ -62,7 +61,7 @@ public boolean isConnected() { try (Jedis jedis = getConnection()) { return "PONG".equals(jedis.ping()); } catch (Exception e) { - e.printStackTrace(); + logRedisFailure("connectivity check", e); return false; } } @@ -84,7 +83,7 @@ public void keepAlive() { try (Jedis jedis = getConnection()) { jedis.ping(); } catch (Exception e) { - e.printStackTrace(); + logRedisFailure("keepalive", e); } } @@ -93,7 +92,7 @@ public void saveKitDataByID(String kitID, String data) { try (Jedis jedis = getConnection()) { jedis.set(kitID, data); } catch (Exception e) { - e.printStackTrace(); + logRedisFailure("save operation for kit ID " + kitID, e); } } @@ -103,7 +102,7 @@ public String getKitDataByID(String kitID) { String data = jedis.get(kitID); return data == null ? "Error" : data; } catch (Exception e) { - e.printStackTrace(); + logRedisFailure("read operation for kit ID " + kitID, e); return "Error"; } } @@ -113,7 +112,7 @@ public boolean doesKitExistByID(String kitID) { try (Jedis jedis = getConnection()) { return jedis.exists(kitID); } catch (Exception e) { - e.printStackTrace(); + logRedisFailure("existence check for kit ID " + kitID, e); return false; } } @@ -123,7 +122,7 @@ public void deleteKitByID(String kitID) { try (Jedis jedis = getConnection()) { jedis.del(kitID); } catch (Exception e) { - e.printStackTrace(); + logRedisFailure("delete operation for kit ID " + kitID, e); } } @@ -140,8 +139,19 @@ public Set getAllKitIDs() { try (Jedis jedis = getConnection()) { kitIDs.addAll(jedis.keys("*")); } catch (Exception e) { - e.printStackTrace(); + logRedisFailure("list operation", e); } return kitIDs; } + + private void logRedisFailure(String operation, Exception exception) { + if (plugin == null || plugin.getLogger() == null) { + return; + } + plugin.getLogger().severe("Redis " + operation + " failed: " + exception.getMessage()); + plugin.getLogger().severe(exception.toString()); + for (StackTraceElement element : exception.getStackTrace()) { + plugin.getLogger().severe("\tat " + element); + } + } } diff --git a/src/main/java/dev/noah/perplayerkit/storage/StorageMigrator.java b/src/main/java/dev/noah/perplayerkit/storage/StorageMigrator.java index d697c54..fc85c5a 100644 --- a/src/main/java/dev/noah/perplayerkit/storage/StorageMigrator.java +++ b/src/main/java/dev/noah/perplayerkit/storage/StorageMigrator.java @@ -52,10 +52,10 @@ public MigrationResult migrate(String sourceType, String destinationType, Consum try { // Create storage managers log(progressCallback, "Creating source storage connection (" + sourceType + ")..."); - source = new StorageSelector(plugin, sourceType).getDbManager(); + source = createStorageManager(sourceType); log(progressCallback, "Creating destination storage connection (" + destinationType + ")..."); - destination = new StorageSelector(plugin, destinationType).getDbManager(); + destination = createStorageManager(destinationType); // Connect to both log(progressCallback, "Connecting to source storage..."); @@ -125,6 +125,10 @@ public MigrationResult migrate(String sourceType, String destinationType, Consum } } + StorageManager createStorageManager(String storageType) { + return new StorageSelector(plugin, storageType).getDbManager(); + } + private void log(Consumer callback, String message) { plugin.getLogger().info("[Migration] " + message); if (callback != null) { diff --git a/src/main/java/dev/noah/perplayerkit/util/PlayerUtil.java b/src/main/java/dev/noah/perplayerkit/util/PlayerUtil.java index 651a068..26159aa 100644 --- a/src/main/java/dev/noah/perplayerkit/util/PlayerUtil.java +++ b/src/main/java/dev/noah/perplayerkit/util/PlayerUtil.java @@ -19,11 +19,16 @@ package dev.noah.perplayerkit.util; import dev.noah.perplayerkit.PerPlayerKit; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; public class PlayerUtil { @@ -66,4 +71,18 @@ public static void healPlayerSilent(Player p) { p.setSaturation(20); } + public static @NotNull String getPlayerName(@NotNull UUID uuid) { + Player onlinePlayer = Bukkit.getPlayer(uuid); + if (onlinePlayer != null) { + return onlinePlayer.getName(); + } + + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); + return fallbackPlayerName(offlinePlayer.getName(), uuid); + } + + static @NotNull String fallbackPlayerName(String playerName, @NotNull UUID uuid) { + return playerName != null ? playerName : uuid.toString(); + } + } diff --git a/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java b/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java new file mode 100644 index 0000000..a888e15 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java @@ -0,0 +1,99 @@ +package dev.noah.perplayerkit.commands; + +import dev.noah.perplayerkit.commands.share.AbstractShareSlotCommand; +import dev.noah.perplayerkit.util.SoundManager; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +class AbstractShareSlotCommandTest { + + @Test + void executesActionForValidSlot() { + ShareRecorder recorder = new ShareRecorder(); + AbstractShareSlotCommand command = new TestShareSlotCommand(recorder); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + command.onCommand(player, null, "sharekit", new String[]{"2"}); + + assertEquals(2, recorder.lastSharedSlot); + assertEquals(1, recorder.executionCount); + } + + @Test + void nonPlayerSenderGetsOnlyPlayersMessage() { + ShareRecorder recorder = new ShareRecorder(); + AbstractShareSlotCommand command = new TestShareSlotCommand(recorder); + CommandSender sender = mock(CommandSender.class); + + command.onCommand(sender, null, "sharekit", new String[]{"2"}); + + verify(sender).sendMessage("Only players can use this command"); + assertNull(recorder.lastSharedSlot); + assertEquals(0, recorder.executionCount); + } + + @Test + void invalidSlotDoesNotExecuteAction() { + ShareRecorder recorder = new ShareRecorder(); + AbstractShareSlotCommand command = new TestShareSlotCommand(recorder); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + try (MockedStatic soundManager = mockStatic(SoundManager.class)) { + command.onCommand(player, null, "sharekit", new String[]{"12"}); + + verify(player).sendMessage(contains("Select a valid kit slot")); + soundManager.verify(() -> SoundManager.playFailure(player)); + } + + assertNull(recorder.lastSharedSlot); + assertEquals(0, recorder.executionCount); + } + + @Test + void cooldownPreventsImmediateSecondExecution() { + ShareRecorder recorder = new ShareRecorder(); + AbstractShareSlotCommand command = new TestShareSlotCommand(recorder); + Player player = mock(Player.class); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + command.onCommand(player, null, "sharekit", new String[]{"3"}); + + try (MockedStatic soundManager = mockStatic(SoundManager.class)) { + command.onCommand(player, null, "sharekit", new String[]{"3"}); + + verify(player).sendMessage(contains("Please don't spam the command")); + soundManager.verify(() -> SoundManager.playFailure(player)); + } + + assertEquals(3, recorder.lastSharedSlot); + assertEquals(1, recorder.executionCount); + } + + private static class TestShareSlotCommand extends AbstractShareSlotCommand { + private TestShareSlotCommand(ShareRecorder recorder) { + super("Error, missing slot", (player, slot) -> { + recorder.lastSharedSlot = slot; + recorder.executionCount++; + }); + } + } + + private static class ShareRecorder { + private Integer lastSharedSlot; + private int executionCount; + } +} diff --git a/src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java b/src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java new file mode 100644 index 0000000..0208c0d --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java @@ -0,0 +1,98 @@ +package dev.noah.perplayerkit.commands; + +import dev.noah.perplayerkit.commands.shortcuts.AbstractShortSlotCommand; +import dev.noah.perplayerkit.util.DisabledCommand; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +class AbstractShortSlotCommandTest { + + @Test + void executesForShortPrefixLabel() { + TestShortSlotCommand command = new TestShortSlotCommand(); + Player player = mock(Player.class); + + try (MockedStatic disabledCommand = mockStatic(DisabledCommand.class)) { + disabledCommand.when(() -> DisabledCommand.isBlockedInWorld(player)).thenReturn(false); + + command.onCommand(player, null, "k4", new String[0]); + } + + assertEquals(4, command.executedSlot); + } + + @Test + void executesForLongPrefixLabel() { + TestShortSlotCommand command = new TestShortSlotCommand(); + Player player = mock(Player.class); + + try (MockedStatic disabledCommand = mockStatic(DisabledCommand.class)) { + disabledCommand.when(() -> DisabledCommand.isBlockedInWorld(player)).thenReturn(false); + + command.onCommand(player, null, "KIT9", new String[0]); + } + + assertEquals(9, command.executedSlot); + } + + @Test + void doesNotExecuteWhenBlockedWorld() { + TestShortSlotCommand command = new TestShortSlotCommand(); + Player player = mock(Player.class); + + try (MockedStatic disabledCommand = mockStatic(DisabledCommand.class)) { + disabledCommand.when(() -> DisabledCommand.isBlockedInWorld(player)).thenReturn(true); + + command.onCommand(player, null, "k3", new String[0]); + } + + assertNull(command.executedSlot); + } + + @Test + void sendsErrorForInvalidLabel() { + TestShortSlotCommand command = new TestShortSlotCommand(); + Player player = mock(Player.class); + + try (MockedStatic disabledCommand = mockStatic(DisabledCommand.class)) { + disabledCommand.when(() -> DisabledCommand.isBlockedInWorld(player)).thenReturn(false); + + command.onCommand(player, null, "k0", new String[0]); + } + + verify(player).sendMessage("Invalid command label."); + assertNull(command.executedSlot); + } + + @Test + void nonPlayerSenderGetsOnlyPlayersMessage() { + TestShortSlotCommand command = new TestShortSlotCommand(); + CommandSender sender = mock(CommandSender.class); + + command.onCommand(sender, null, "k1", new String[0]); + + verify(sender).sendMessage("Only players can use this command."); + assertNull(command.executedSlot); + } + + private static class TestShortSlotCommand extends AbstractShortSlotCommand { + private Integer executedSlot; + + private TestShortSlotCommand() { + super("k", "kit"); + } + + @Override + protected void executeForSlot(Player player, int slot) { + this.executedSlot = slot; + } + } +} diff --git a/src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java b/src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java new file mode 100644 index 0000000..17a88a2 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java @@ -0,0 +1,60 @@ +package dev.noah.perplayerkit.commands; + +import dev.noah.perplayerkit.commands.core.CommandGuards; +import dev.noah.perplayerkit.util.DisabledCommand; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +class CommandGuardsTest { + + @Test + void requirePlayerReturnsPlayerForPlayerSender() { + Player player = mock(Player.class); + + Player result = CommandGuards.requirePlayer(player); + + assertSame(player, result); + } + + @Test + void requirePlayerReturnsNullAndSendsMessageForNonPlayerSender() { + CommandSender sender = mock(CommandSender.class); + + Player result = CommandGuards.requirePlayer(sender, "Players only"); + + assertNull(result); + verify(sender).sendMessage("Players only"); + } + + @Test + void requirePlayerInEnabledWorldReturnsNullWhenBlocked() { + Player player = mock(Player.class); + try (MockedStatic disabledCommand = mockStatic(DisabledCommand.class)) { + disabledCommand.when(() -> DisabledCommand.isBlockedInWorld(player)).thenReturn(true); + + Player result = CommandGuards.requirePlayerInEnabledWorld(player); + + assertNull(result); + } + } + + @Test + void requirePlayerInEnabledWorldReturnsPlayerWhenAllowed() { + Player player = mock(Player.class); + try (MockedStatic disabledCommand = mockStatic(DisabledCommand.class)) { + disabledCommand.when(() -> DisabledCommand.isBlockedInWorld(player)).thenReturn(false); + + Player result = CommandGuards.requirePlayerInEnabledWorld(player); + + assertSame(player, result); + } + } +} diff --git a/src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java b/src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java new file mode 100644 index 0000000..7d81ec1 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java @@ -0,0 +1,30 @@ +package dev.noah.perplayerkit.commands; + +import dev.noah.perplayerkit.commands.core.SlotArgumentParser; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class SlotArgumentParserTest { + + @Test + void parseSlotReturnsIntegerForNumericInput() { + assertEquals(7, SlotArgumentParser.parseSlot("7")); + } + + @Test + void parseSlotReturnsNullForNonNumericInput() { + assertNull(SlotArgumentParser.parseSlot("abc")); + } + + @Test + void parseSlotInRangeReturnsIntegerInsideRange() { + assertEquals(3, SlotArgumentParser.parseSlotInRange("3", 1, 9)); + } + + @Test + void parseSlotInRangeReturnsNullOutsideRange() { + assertNull(SlotArgumentParser.parseSlotInRange("10", 1, 9)); + } +} diff --git a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java new file mode 100644 index 0000000..ae66883 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java @@ -0,0 +1,62 @@ +package dev.noah.perplayerkit.commands.inspect; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Method; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; + +class InspectCommandUtilTest { + + @Test + void selectResolvedUuidPrefersCachedOfflinePlayer() { + UUID cachedUuid = UUID.randomUUID(); + + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", cachedUuid, + () -> { + throw new AssertionError("Mojang lookup should not run when cached player exists"); + }); + + assertEquals(cachedUuid, resolvedUuid); + } + + @Test + void selectResolvedUuidUsesMojangResultWhenCacheMisses() { + UUID mojangUuid = UUID.randomUUID(); + + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", null, () -> mojangUuid); + + assertEquals(mojangUuid, resolvedUuid); + } + + @Test + void selectResolvedUuidReturnsNullWhenAllLookupsMiss() { + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", null, () -> null); + + assertNull(resolvedUuid); + } + + @Test + void findCachedOfflinePlayerUuidDoesNotScanOfflinePlayers() throws Exception { + Method method = InspectCommandUtil.class.getDeclaredMethod("findCachedOfflinePlayerUuid", String.class); + method.setAccessible(true); + Server server = mock(Server.class); + + try (MockedStatic bukkit = mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::getServer).thenReturn(server); + + UUID cachedUuid = (UUID) method.invoke(null, "TargetPlayer"); + + assertNull(cachedUuid); + bukkit.verify(Bukkit::getOfflinePlayers, never()); + } + } +} diff --git a/src/test/java/dev/noah/perplayerkit/storage/RedisStorageTest.java b/src/test/java/dev/noah/perplayerkit/storage/RedisStorageTest.java new file mode 100644 index 0000000..d3f8253 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/storage/RedisStorageTest.java @@ -0,0 +1,72 @@ +package dev.noah.perplayerkit.storage; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import redis.clients.jedis.JedisPool; + +import java.io.File; +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RedisStorageTest { + + private Plugin plugin; + + @BeforeEach + void setUp() { + plugin = mock(Plugin.class); + YamlConfiguration config = new YamlConfiguration(); + config.set("redis.host", "127.0.0.1"); + config.set("redis.port", 6379); + config.set("redis.password", ""); + + when(plugin.getConfig()).thenReturn(config); + when(plugin.getDataFolder()).thenReturn(new File("target/test-plugin-data")); + } + + @Test + void notConnectedInitially() { + RedisStorage storage = new RedisStorage(plugin); + + assertFalse(storage.isConnected()); + } + + @Test + void methodsFailGracefullyWhenPoolNotInitialized() { + RedisStorage storage = new RedisStorage(plugin); + + assertDoesNotThrow(() -> storage.keepAlive()); + assertDoesNotThrow(() -> storage.saveKitDataByID("kit-1", "payload-1")); + assertDoesNotThrow(() -> storage.deleteKitByID("kit-1")); + + assertEquals("Error", storage.getKitDataByID("kit-1")); + assertFalse(storage.doesKitExistByID("kit-1")); + assertTrue(storage.getAllKitIDs().isEmpty()); + } + + @Test + void connectInitializesPoolAndCloseIsSafe() throws Exception { + RedisStorage storage = new RedisStorage(plugin); + + storage.connect(); + + JedisPool pool = getPool(storage); + assertNotNull(pool); + assertDoesNotThrow(storage::close); + } + + private JedisPool getPool(RedisStorage storage) throws Exception { + Field field = RedisStorage.class.getDeclaredField("pool"); + field.setAccessible(true); + return (JedisPool) field.get(storage); + } +} diff --git a/src/test/java/dev/noah/perplayerkit/storage/SQLStorageTest.java b/src/test/java/dev/noah/perplayerkit/storage/SQLStorageTest.java new file mode 100644 index 0000000..42ba9c5 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/storage/SQLStorageTest.java @@ -0,0 +1,195 @@ +package dev.noah.perplayerkit.storage; + +import dev.noah.perplayerkit.storage.exceptions.StorageConnectionException; +import dev.noah.perplayerkit.storage.exceptions.StorageOperationException; +import dev.noah.perplayerkit.storage.sql.SQLDatabase; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SQLStorageTest { + + @Test + void connectInitAndCloseWorkWithRealInMemoryDatabase() throws Exception { + InMemorySQLiteDatabase db = new InMemorySQLiteDatabase(); + SQLStorage storage = new SQLStorage(db); + + storage.connect(); + storage.init(); + + assertTrue(storage.isConnected()); + + storage.close(); + assertFalse(storage.isConnected()); + } + + @Test + void saveGetExistsDeleteAndListWork() throws Exception { + InMemorySQLiteDatabase db = new InMemorySQLiteDatabase(); + SQLStorage storage = new SQLStorage(db); + storage.connect(); + storage.init(); + + storage.saveKitDataByID("kit-1", "payload-1"); + storage.saveKitDataByID("kit-2", "payload-2"); + + assertEquals("payload-1", storage.getKitDataByID("kit-1")); + assertTrue(storage.doesKitExistByID("kit-2")); + assertEquals(Set.of("kit-1", "kit-2"), storage.getAllKitIDs()); + + storage.deleteKitByID("kit-2"); + assertFalse(storage.doesKitExistByID("kit-2")); + assertEquals("Error", storage.getKitDataByID("kit-2")); + + storage.close(); + } + + @Test + void keepAliveSucceedsWhenConnected() throws Exception { + InMemorySQLiteDatabase db = new InMemorySQLiteDatabase(); + SQLStorage storage = new SQLStorage(db); + storage.connect(); + + storage.keepAlive(); + + storage.close(); + } + + @Test + void connectWrapsCheckedException() { + SQLStorage storage = new SQLStorage(new ThrowingConnectDatabase()); + + assertThrows(StorageConnectionException.class, storage::connect); + } + + @Test + void initWrapsSqlException() throws Exception { + SQLStorage storage = new SQLStorage(new ThrowingGetConnectionDatabase()); + + storage.connect(); + assertThrows(StorageOperationException.class, storage::init); + } + + @Test + void keepAliveWrapsSqlException() throws Exception { + SQLStorage storage = new SQLStorage(new ThrowingGetConnectionDatabase()); + + storage.connect(); + assertThrows(StorageConnectionException.class, storage::keepAlive); + } + + @Test + void closeWrapsSqlException() throws Exception { + SQLStorage storage = new SQLStorage(new ThrowingDisconnectDatabase()); + + storage.connect(); + assertThrows(StorageConnectionException.class, storage::close); + } + + private static class InMemorySQLiteDatabase implements SQLDatabase { + private final String jdbcUrl = "jdbc:sqlite:file:" + UUID.randomUUID() + "?mode=memory&cache=shared"; + private Connection keepAliveConnection; + + @Override + public boolean isConnected() { + try { + return keepAliveConnection != null && !keepAliveConnection.isClosed(); + } catch (SQLException e) { + return false; + } + } + + @Override + public void connect() throws SQLException { + if (!isConnected()) { + keepAliveConnection = DriverManager.getConnection(jdbcUrl); + } + } + + @Override + public void disconnect() throws SQLException { + if (keepAliveConnection != null) { + keepAliveConnection.close(); + } + } + + @Override + public Connection getConnection() throws SQLException { + if (!isConnected()) { + throw new SQLException("Not connected"); + } + return DriverManager.getConnection(jdbcUrl); + } + } + + private static class ThrowingConnectDatabase implements SQLDatabase { + @Override + public boolean isConnected() { + return false; + } + + @Override + public void connect() throws ClassNotFoundException { + throw new ClassNotFoundException("driver"); + } + + @Override + public void disconnect() { + } + + @Override + public Connection getConnection() { + throw new UnsupportedOperationException(); + } + } + + private static class ThrowingGetConnectionDatabase implements SQLDatabase { + @Override + public boolean isConnected() { + return true; + } + + @Override + public void connect() { + } + + @Override + public void disconnect() { + } + + @Override + public Connection getConnection() throws SQLException { + throw new SQLException("boom"); + } + } + + private static class ThrowingDisconnectDatabase implements SQLDatabase { + @Override + public boolean isConnected() { + return true; + } + + @Override + public void connect() { + } + + @Override + public void disconnect() throws SQLException { + throw new SQLException("boom"); + } + + @Override + public Connection getConnection() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java b/src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java new file mode 100644 index 0000000..fb7e9de --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java @@ -0,0 +1,134 @@ +package dev.noah.perplayerkit.storage; + +import dev.noah.perplayerkit.storage.exceptions.StorageConnectionException; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class StorageMigratorTest { + + private Plugin plugin; + + @BeforeEach + void setUp() { + plugin = mock(Plugin.class); + Logger logger = Logger.getLogger("StorageMigratorTest"); + logger.setUseParentHandlers(false); + logger.setLevel(Level.OFF); + when(plugin.getLogger()).thenReturn(logger); + } + + @Test + void migrateRejectsSameSourceAndDestination() { + StorageMigrator migrator = new StorageMigrator(plugin); + + StorageMigrator.MigrationResult result = migrator.migrate("sqlite", "sqlite", null); + + assertFalse(result.isSuccess()); + assertEquals(0, result.getMigratedCount()); + assertEquals(0, result.getFailedCount()); + assertEquals("Source and destination storage types are the same.", result.getErrorMessage()); + } + + @Test + void migrateReturnsNoDataWhenSourceIsEmptyAndClosesConnections() throws Exception { + StorageManager source = mock(StorageManager.class); + StorageManager destination = mock(StorageManager.class); + when(source.getAllKitIDs()).thenReturn(new HashSet<>()); + + StorageMigrator migrator = new TestableStorageMigrator(plugin, source, destination); + + StorageMigrator.MigrationResult result = migrator.migrate("sqlite", "mysql", null); + + assertTrue(result.isSuccess()); + assertEquals(0, result.getMigratedCount()); + assertEquals(0, result.getFailedCount()); + assertEquals("No data to migrate.", result.getErrorMessage()); + + verify(source).connect(); + verify(source).init(); + verify(destination).connect(); + verify(destination).init(); + verify(source).close(); + verify(destination).close(); + } + + @Test + void migrateCountsMigratedAndFailedEntries() throws Exception { + StorageManager source = mock(StorageManager.class); + StorageManager destination = mock(StorageManager.class); + + when(source.getAllKitIDs()).thenReturn(new HashSet<>(Arrays.asList("a", "b", "c", "d"))); + when(source.getKitDataByID("a")).thenReturn("data-a"); + when(source.getKitDataByID("b")).thenReturn(null); + when(source.getKitDataByID("c")).thenReturn("Error"); + when(source.getKitDataByID("d")).thenThrow(new RuntimeException("read failure")); + + StorageMigrator migrator = new TestableStorageMigrator(plugin, source, destination); + + StorageMigrator.MigrationResult result = migrator.migrate("sqlite", "redis", null); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getMigratedCount()); + assertEquals(3, result.getFailedCount()); + assertEquals(null, result.getErrorMessage()); + + verify(destination, times(1)).saveKitDataByID("a", "data-a"); + verify(source).close(); + verify(destination).close(); + } + + @Test + void migrateHandlesConnectionErrorsAndStillClosesManagers() throws Exception { + StorageManager source = mock(StorageManager.class); + StorageManager destination = mock(StorageManager.class); + doThrow(new StorageConnectionException("boom")).when(source).connect(); + + StorageMigrator migrator = new TestableStorageMigrator(plugin, source, destination); + + StorageMigrator.MigrationResult result = migrator.migrate("sqlite", "mysql", null); + + assertFalse(result.isSuccess()); + assertEquals(0, result.getMigratedCount()); + assertEquals(0, result.getFailedCount()); + assertTrue(result.getErrorMessage().startsWith("Connection error:")); + + verify(source).close(); + verify(destination).close(); + } + + private static class TestableStorageMigrator extends StorageMigrator { + private final StorageManager source; + private final StorageManager destination; + + private TestableStorageMigrator(Plugin plugin, StorageManager source, StorageManager destination) { + super(plugin); + this.source = source; + this.destination = destination; + } + + @Override + StorageManager createStorageManager(String storageType) { + return "source".equals(storageType) ? source : destination; + } + + @Override + public MigrationResult migrate(String sourceType, String destinationType, java.util.function.Consumer progressCallback) { + return super.migrate("source", "destination", progressCallback); + } + } +} diff --git a/src/test/java/dev/noah/perplayerkit/storage/StorageSelectorTest.java b/src/test/java/dev/noah/perplayerkit/storage/StorageSelectorTest.java new file mode 100644 index 0000000..2716fff --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/storage/StorageSelectorTest.java @@ -0,0 +1,78 @@ +package dev.noah.perplayerkit.storage; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class StorageSelectorTest { + + private Plugin plugin; + + @BeforeEach + void setUp() { + plugin = mock(Plugin.class); + YamlConfiguration config = new YamlConfiguration(); + + when(plugin.getDataFolder()).thenReturn(new File("target/test-plugin-data")); + when(plugin.getConfig()).thenReturn(config); + config.set("mysql.host", "localhost"); + config.set("mysql.port", "3306"); + config.set("mysql.dbname", "ppk"); + config.set("mysql.username", "user"); + config.set("mysql.password", "pass"); + config.set("mysql.useSSL", false); + + config.set("redis.host", "localhost"); + config.set("redis.port", 6379); + config.set("redis.password", ""); + } + + @Test + void yamlTypeReturnsYamlStorage() { + StorageManager manager = new StorageSelector(plugin, "yaml").getDbManager(); + + assertInstanceOf(YAMLStorage.class, manager); + } + + @Test + void ymlTypeReturnsYamlStorage() { + StorageManager manager = new StorageSelector(plugin, "yml").getDbManager(); + + assertInstanceOf(YAMLStorage.class, manager); + } + + @Test + void redisTypeReturnsRedisStorage() { + StorageManager manager = new StorageSelector(plugin, "redis").getDbManager(); + + assertInstanceOf(RedisStorage.class, manager); + } + + @Test + void mysqlTypeReturnsSqlStorage() { + StorageManager manager = new StorageSelector(plugin, "mysql").getDbManager(); + + assertInstanceOf(SQLStorage.class, manager); + } + + @Test + void sqliteTypeReturnsSqlStorage() { + StorageManager manager = new StorageSelector(plugin, "sqlite").getDbManager(); + + assertInstanceOf(SQLStorage.class, manager); + } + + @Test + void unknownTypeDefaultsToSqliteStorage() { + StorageManager manager = new StorageSelector(plugin, "something-else").getDbManager(); + + assertInstanceOf(SQLStorage.class, manager); + } +} diff --git a/src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java b/src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java new file mode 100644 index 0000000..c60d07f --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java @@ -0,0 +1,78 @@ +package dev.noah.perplayerkit.storage; + +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class YAMLStorageTest { + + private Plugin plugin; + + @BeforeEach + void setUp() { + plugin = mock(Plugin.class); + Logger logger = Logger.getLogger("YAMLStorageTest"); + logger.setUseParentHandlers(false); + logger.setLevel(Level.OFF); + when(plugin.getLogger()).thenReturn(logger); + } + + @Test + void initCreatesStorageFileWhenMissing(@TempDir Path tempDir) { + Path filePath = tempDir.resolve("storage.yml"); + YAMLStorage storage = new YAMLStorage(plugin, filePath.toString()); + + storage.init(); + + assertTrue(filePath.toFile().exists()); + assertTrue(storage.getAllKitIDs().isEmpty()); + } + + @Test + void saveLoadDeleteAndListWork(@TempDir Path tempDir) { + Path filePath = tempDir.resolve("storage.yml"); + YAMLStorage storage = new YAMLStorage(plugin, filePath.toString()); + storage.init(); + + storage.saveKitDataByID("kit-1", "payload-1"); + storage.saveKitDataByID("kit-2", "payload-2"); + + assertEquals("payload-1", storage.getKitDataByID("kit-1")); + assertTrue(storage.doesKitExistByID("kit-2")); + assertEquals(Set.of("kit-1", "kit-2"), storage.getAllKitIDs()); + + YAMLStorage reloaded = new YAMLStorage(plugin, filePath.toString()); + reloaded.init(); + assertEquals("payload-1", reloaded.getKitDataByID("kit-1")); + + reloaded.deleteKitByID("kit-1"); + assertFalse(reloaded.doesKitExistByID("kit-1")); + assertEquals("error", reloaded.getKitDataByID("kit-1")); + } + + @Test + void closePersistsCurrentState(@TempDir Path tempDir) { + Path filePath = tempDir.resolve("storage.yml"); + YAMLStorage storage = new YAMLStorage(plugin, filePath.toString()); + storage.init(); + storage.saveKitDataByID("kit-3", "payload-3"); + + storage.close(); + + YAMLStorage reloaded = new YAMLStorage(plugin, filePath.toString()); + reloaded.init(); + assertEquals("payload-3", reloaded.getKitDataByID("kit-3")); + } +} diff --git a/src/test/java/dev/noah/perplayerkit/util/CooldownManagerTest.java b/src/test/java/dev/noah/perplayerkit/util/CooldownManagerTest.java new file mode 100644 index 0000000..7901ec7 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/util/CooldownManagerTest.java @@ -0,0 +1,35 @@ +package dev.noah.perplayerkit.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CooldownManagerTest { + + @Test + void keyIsNotOnCooldownBeforeSet() { + CooldownManager cooldownManager = new CooldownManager(1); + + assertFalse(cooldownManager.isOnCooldown("alpha")); + } + + @Test + void keyIsOnCooldownImmediatelyAfterSet() { + CooldownManager cooldownManager = new CooldownManager(1); + + cooldownManager.setCooldown("alpha"); + + assertTrue(cooldownManager.isOnCooldown("alpha")); + } + + @Test + void keyExpiresAfterCooldownWindow() throws InterruptedException { + CooldownManager cooldownManager = new CooldownManager(1); + + cooldownManager.setCooldown("alpha"); + Thread.sleep(1200); + + assertFalse(cooldownManager.isOnCooldown("alpha")); + } +} diff --git a/src/test/java/dev/noah/perplayerkit/util/IDUtilTest.java b/src/test/java/dev/noah/perplayerkit/util/IDUtilTest.java new file mode 100644 index 0000000..7fdebf5 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/util/IDUtilTest.java @@ -0,0 +1,34 @@ +package dev.noah.perplayerkit.util; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class IDUtilTest { + + @Test + void getPlayerKitIdIncludesUuidAndSlot() { + UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + + assertEquals("123e4567-e89b-12d3-a456-4266141740003", IDUtil.getPlayerKitId(uuid, 3)); + } + + @Test + void getECIdIncludesUuidEcAndSlot() { + UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + + assertEquals("123e4567-e89b-12d3-a456-426614174000ec7", IDUtil.getECId(uuid, 7)); + } + + @Test + void getPublicKitIdPrefixesPublic() { + assertEquals("publicduel", IDUtil.getPublicKitId("duel")); + } + + @Test + void getKitRoomIdPrefixesKitRoom() { + assertEquals("kitroom9", IDUtil.getKitRoomId(9)); + } +} diff --git a/src/test/java/dev/noah/perplayerkit/util/PlayerUtilTest.java b/src/test/java/dev/noah/perplayerkit/util/PlayerUtilTest.java new file mode 100644 index 0000000..d9fcad3 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/util/PlayerUtilTest.java @@ -0,0 +1,58 @@ +package dev.noah.perplayerkit.util; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +class PlayerUtilTest { + + @Test + void getPlayerNameUsesOnlinePlayerNameWhenAvailable() { + UUID uuid = UUID.randomUUID(); + Player onlinePlayer = mock(Player.class); + when(onlinePlayer.getName()).thenReturn("OnlineName"); + + try (MockedStatic bukkit = mockStatic(Bukkit.class)) { + bukkit.when(() -> Bukkit.getPlayer(uuid)).thenReturn(onlinePlayer); + + assertEquals("OnlineName", PlayerUtil.getPlayerName(uuid)); + } + } + + @Test + void getPlayerNameUsesOfflinePlayerNameWhenPlayerIsNotOnline() { + UUID uuid = UUID.randomUUID(); + OfflinePlayer offlinePlayer = mock(OfflinePlayer.class); + when(offlinePlayer.getName()).thenReturn("OfflineName"); + + try (MockedStatic bukkit = mockStatic(Bukkit.class)) { + bukkit.when(() -> Bukkit.getPlayer(uuid)).thenReturn(null); + bukkit.when(() -> Bukkit.getOfflinePlayer(uuid)).thenReturn(offlinePlayer); + + assertEquals("OfflineName", PlayerUtil.getPlayerName(uuid)); + } + } + + @Test + void getPlayerNameFallsBackToUuidWhenNoNameIsKnown() { + UUID uuid = UUID.randomUUID(); + OfflinePlayer offlinePlayer = mock(OfflinePlayer.class); + when(offlinePlayer.getName()).thenReturn(null); + + try (MockedStatic bukkit = mockStatic(Bukkit.class)) { + bukkit.when(() -> Bukkit.getPlayer(uuid)).thenReturn(null); + bukkit.when(() -> Bukkit.getOfflinePlayer(uuid)).thenReturn(offlinePlayer); + + assertEquals(uuid.toString(), PlayerUtil.getPlayerName(uuid)); + } + } +} diff --git a/tools/__pycache__/notice.cpython-311.pyc b/tools/__pycache__/notice.cpython-311.pyc new file mode 100644 index 0000000..883b084 Binary files /dev/null and b/tools/__pycache__/notice.cpython-311.pyc differ diff --git a/tools/notice.py b/tools/notice.py index e4a89f0..1b940eb 100644 --- a/tools/notice.py +++ b/tools/notice.py @@ -1,8 +1,11 @@ import os +import re +import subprocess +from datetime import datetime -# Define the copyright notice to be added -COPYRIGHT_NOTICE = """/* - * Copyright 2022-2025 Noah Ross + +NOTICE_TEMPLATE = """/* + * Copyright {years} Noah Ross * * This file is part of PerPlayerKit. * @@ -20,33 +23,111 @@ * along with PerPlayerKit. If not, see . */""" -def add_copyright_to_file(file_path): +COPYRIGHT_LINE_RE = re.compile( + r"^ \* Copyright (?P\d{4}(?:-\d{4})?) Noah Ross$", + re.MULTILINE, +) + + +def get_repo_root(start_path): + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=start_path, + capture_output=True, + text=True, + check=True, + ) + except (FileNotFoundError, subprocess.CalledProcessError): + return None + + return result.stdout.strip() + + +def get_copyright_years(file_path, repo_root): + if repo_root is None: + return str(datetime.now().year) + + relative_path = os.path.relpath(file_path, repo_root) + + try: + result = subprocess.run( + [ + "git", + "-C", + repo_root, + "log", + "--follow", + "--format=%ad", + "--date=format:%Y", + "--", + relative_path, + ], + capture_output=True, + text=True, + check=True, + ) + except (FileNotFoundError, subprocess.CalledProcessError, ValueError): + return str(datetime.now().year) + + years = [line.strip() for line in result.stdout.splitlines() if line.strip()] + if not years: + return str(datetime.now().year) + + newest_year = years[0] + oldest_year = years[-1] + if newest_year == oldest_year: + return newest_year + + return f"{oldest_year}-{newest_year}" + + +def write_updated_content(file_path, content): + with open(file_path, "w", encoding="utf-8") as file: + file.write(content) + + +def add_copyright_to_file(file_path, repo_root): """ - Add the copyright notice to a Java file if it doesn't already exist. + Add or update the copyright notice on a Java file. """ - with open(file_path, 'r+') as file: + with open(file_path, "r", encoding="utf-8") as file: content = file.read() - # Check if the copyright notice is already present - if COPYRIGHT_NOTICE in content: - print(f"Copyright notice already exists in: {file_path}") + + years = get_copyright_years(file_path, repo_root) + notice = NOTICE_TEMPLATE.format(years=years) + + match = COPYRIGHT_LINE_RE.search(content) + if match: + if match.group("years") == years: + print(f"Copyright notice already up to date in: {file_path}") return - # Prepend the copyright notice - file.seek(0) - file.write(COPYRIGHT_NOTICE + "\n" + content) - print(f"Added copyright notice to: {file_path}") -def process_java_files(directory): + updated_content = COPYRIGHT_LINE_RE.sub( + f" * Copyright {years} Noah Ross", + content, + count=1, + ) + write_updated_content(file_path, updated_content) + print(f"Updated copyright notice in: {file_path}") + return + + write_updated_content(file_path, notice + "\n" + content) + print(f"Added copyright notice to: {file_path}") + + +def process_java_files(directory, repo_root): """ Recursively process all Java files in the given directory. """ for root, _, files in os.walk(directory): for file in files: - if file.endswith('.java'): + if file.endswith(".java"): file_path = os.path.join(root, file) - add_copyright_to_file(file_path) + add_copyright_to_file(file_path, repo_root) -if __name__ == "__main__": - # Replace this with the path to your project directory - project_directory = "./" - process_java_files(project_directory) +if __name__ == "__main__": + project_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + repo_root = get_repo_root(project_directory) + process_java_files(project_directory, repo_root)