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)