From bddd7c73b638d9b7d7cc239d4bba1f4f2ad4c846 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Sat, 7 Mar 2026 15:40:08 -0500 Subject: [PATCH 01/28] GUI refactor --- .../java/dev/noah/perplayerkit/gui/GUI.java | 216 +++++++----------- .../noah/perplayerkit/gui/GuiLayoutUtils.java | 62 +++++ .../noah/perplayerkit/gui/GuiMenuFactory.java | 65 ++++++ 3 files changed, 210 insertions(+), 133 deletions(-) create mode 100644 src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java create mode 100644 src/main/java/dev/noah/perplayerkit/gui/GuiMenuFactory.java diff --git a/src/main/java/dev/noah/perplayerkit/gui/GUI.java b/src/main/java/dev/noah/perplayerkit/gui/GUI.java index 03fb6f4..6390521 100644 --- a/src/main/java/dev/noah/perplayerkit/gui/GUI.java +++ b/src/main/java/dev/noah/perplayerkit/gui/GUI.java @@ -34,7 +34,6 @@ 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 +45,7 @@ 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.*; public class GUI { private final Plugin plugin; @@ -75,7 +75,7 @@ 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(); + return GuiMenuFactory.createPublicKitRoomMenu(); } public static boolean removeKitDeletionFlag(Player player) { @@ -83,96 +83,72 @@ public static boolean removeKitDeletionFlag(Player player) { } 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); + Menu menu = GuiMenuFactory.createECMenu(slot); - for (int i = 0; i < 9; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); - - } - 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 +156,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 +188,29 @@ 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); + Menu menu = GuiMenuFactory.createInspectEcMenu(slot, playerName); - for (int i = 0; i < 9; i++) { - menu.getSlot(i).setItem(ItemUtil.createGlassPane()); - - } - 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(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 +219,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 +269,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 +313,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 +329,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(CLEAR_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(CLEAR_SLOT), id); menu.open(p); @@ -390,8 +341,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 +373,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); } @@ -664,31 +614,31 @@ public void addEditLoadEC(Slot slot, int i) { } public Menu createKitMenu(int slot) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Kit: " + slot).build(); + return GuiMenuFactory.createKitMenu(slot); } public Menu createPublicKitMenu(String id) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Public Kit: " + id).build(); + return GuiMenuFactory.createPublicKitMenu(id); } public Menu createECMenu(int slot) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Enderchest: " + slot).build(); + return GuiMenuFactory.createECMenu(slot); } public Menu createInspectMenu(int slot, String playerName) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Inspecting " + playerName + "'s kit " + slot).build(); + return GuiMenuFactory.createInspectMenu(slot, playerName); } public Menu createInspectEcMenu(int slot, String playerName) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Inspecting " + playerName + "'s enderchest " + slot).build(); + return GuiMenuFactory.createInspectEcMenu(slot, playerName); } public Menu createMainMenu(Player p) { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + p.getName() + "'s Kits").build(); + return GuiMenuFactory.createMainMenu(p); } public Menu createKitRoom() { - return ChestMenu.builder(6).title(StyleManager.get().getPrimaryColor() + "Kit Room").redraw(true).build(); + return GuiMenuFactory.createKitRoomMenu(); } public void allowModification(Slot slot) { @@ -705,4 +655,4 @@ private String getPlayerName(UUID 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..c6e013b --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.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.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 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(); + } +} From 76cbeb065e7d6b841027c06b0c52f76bb7b5e2b8 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Sat, 7 Mar 2026 15:42:31 -0500 Subject: [PATCH 02/28] simple tests --- pom.xml | 11 ++++++ .../util/CooldownManagerTest.java | 35 +++++++++++++++++++ .../noah/perplayerkit/util/IDUtilTest.java | 34 ++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/test/java/dev/noah/perplayerkit/util/CooldownManagerTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/util/IDUtilTest.java diff --git a/pom.xml b/pom.xml index e14ca17..f1711d7 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + @@ -169,5 +174,11 @@ 2.11.6 provided + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + 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)); + } +} From 321b3d079d31ded875548e0f3307fdf52d74f840 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Sat, 7 Mar 2026 16:05:35 -0500 Subject: [PATCH 03/28] storage tests --- pom.xml | 12 ++ .../perplayerkit/storage/StorageMigrator.java | 8 +- .../storage/RedisStorageTest.java | 72 +++++++ .../perplayerkit/storage/SQLStorageTest.java | 195 ++++++++++++++++++ .../storage/StorageMigratorTest.java | 129 ++++++++++++ .../storage/StorageSelectorTest.java | 78 +++++++ .../perplayerkit/storage/YAMLStorageTest.java | 73 +++++++ 7 files changed, 565 insertions(+), 2 deletions(-) create mode 100644 src/test/java/dev/noah/perplayerkit/storage/RedisStorageTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/storage/SQLStorageTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/storage/StorageSelectorTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java diff --git a/pom.xml b/pom.xml index f1711d7..539915f 100644 --- a/pom.xml +++ b/pom.xml @@ -180,5 +180,17 @@ 5.11.4 test + + org.mockito + mockito-core + 5.15.2 + test + + + org.xerial + sqlite-jdbc + 3.49.1.0 + test + 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/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..f725bab --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java @@ -0,0 +1,129 @@ +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 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); + when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("StorageMigratorTest")); + } + + @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..91acd68 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java @@ -0,0 +1,73 @@ +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 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); + when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("YAMLStorageTest")); + } + + @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")); + } +} From 60ab993d43c64f9ab6371a0a77582f46459319d2 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Sat, 7 Mar 2026 16:25:04 -0500 Subject: [PATCH 04/28] refactor commands and add tests --- .../commands/AbstractInspectCommand.java | 191 ++++++++++++++++++ .../commands/AbstractShareSlotCommand.java | 74 +++++++ .../commands/AbstractShortSlotCommand.java | 84 ++++++++ .../perplayerkit/commands/CommandGuards.java | 58 ++++++ .../perplayerkit/commands/CopyKitCommand.java | 25 +-- .../commands/DeleteKitCommand.java | 62 +++--- .../commands/EnderchestCommand.java | 16 +- .../commands/InspectEcCommand.java | 166 +++------------ .../commands/InspectKitCommand.java | 170 +++------------- .../commands/MainMenuCommand.java | 11 +- .../commands/PerPlayerKitCommand.java | 176 +++++++++------- .../commands/PublicKitCommand.java | 22 +- .../perplayerkit/commands/RegearCommand.java | 181 ++++++++++------- .../commands/SavePublicKitCommand.java | 30 +-- .../commands/ShareECKitCommand.java | 49 +---- .../commands/ShareKitCommand.java | 49 +---- .../perplayerkit/commands/ShortECCommand.java | 37 +--- .../commands/ShortKitCommand.java | 38 +--- .../commands/SlotArgumentParser.java | 39 ++++ .../perplayerkit/commands/SwapKitCommand.java | 9 +- .../commands/extracommands/HealCommand.java | 6 +- .../commands/extracommands/RepairCommand.java | 5 +- .../AbstractShareSlotCommandTest.java | 98 +++++++++ .../AbstractShortSlotCommandTest.java | 97 +++++++++ .../commands/CommandGuardsTest.java | 59 ++++++ .../commands/SlotArgumentParserTest.java | 29 +++ 26 files changed, 1071 insertions(+), 710 deletions(-) create mode 100644 src/main/java/dev/noah/perplayerkit/commands/AbstractInspectCommand.java create mode 100644 src/main/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommand.java create mode 100644 src/main/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommand.java create mode 100644 src/main/java/dev/noah/perplayerkit/commands/CommandGuards.java create mode 100644 src/main/java/dev/noah/perplayerkit/commands/SlotArgumentParser.java create mode 100644 src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java diff --git a/src/main/java/dev/noah/perplayerkit/commands/AbstractInspectCommand.java b/src/main/java/dev/noah/perplayerkit/commands/AbstractInspectCommand.java new file mode 100644 index 0000000..661d3c2 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/AbstractInspectCommand.java @@ -0,0 +1,191 @@ +/* + * 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.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.ERROR_PREFIX; +import static dev.noah.perplayerkit.commands.InspectCommandUtil.MAX_SLOT; +import static dev.noah.perplayerkit.commands.InspectCommandUtil.MIN_SLOT; +import static dev.noah.perplayerkit.commands.InspectCommandUtil.getPlayerName; +import static dev.noah.perplayerkit.commands.InspectCommandUtil.mm; +import static dev.noah.perplayerkit.commands.InspectCommandUtil.resolvePlayerIdentifierAsync; +import static dev.noah.perplayerkit.commands.InspectCommandUtil.showUsage; + +public abstract class AbstractInspectCommand implements CommandExecutor, TabCompleter { + protected final 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) { + 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, usageCommand()); + return true; + } + + int slot = parseSlot(args[1], player); + if (slot == -1) { + return true; + } + + CompletableFuture future = resolvePlayerIdentifierAsync(args[0]) + .thenCompose(targetUuid -> { + if (targetUuid == null) { + 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); + } + + Player targetPlayer = Bukkit.getPlayer(targetUuid); + return CompletableFuture.runAsync(() -> { + if (targetPlayer == null) { + KitManager.get().loadPlayerDataFromDB(targetUuid); + } + }).thenRun(() -> Bukkit.getScheduler().runTask(plugin, () -> showInspectResult(player, targetUuid, slot))); + }); + + future.exceptionally(ex -> { + Bukkit.getScheduler().runTask(plugin, () -> { + plugin.getLogger().severe(loadErrorLogMessage() + ": " + ex.getMessage()); + BroadcastManager.get().sendComponentMessage(player, + ERROR_PREFIX.append(mm.deserialize(loadErrorUserMessage()))); + 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; + } + + 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<>(); + } + + 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, UUID targetUuid, int slot) { + if (hasData(targetUuid, slot)) { + openInspectGui(inspector, targetUuid, slot); + return; + } + + String targetName = getPlayerName(targetUuid); + BroadcastManager.get().sendComponentMessage(inspector, + ERROR_PREFIX.append(mm.deserialize(missingDataMessage(targetName, slot)))); + SoundManager.playFailure(inspector); + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommand.java b/src/main/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommand.java new file mode 100644 index 0000000..de7379a --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommand.java @@ -0,0 +1,74 @@ +/* + * 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.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 org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; + +public abstract class AbstractShareSlotCommand implements CommandExecutor { + + private static final int COOLDOWN_SECONDS = 5; + private final CooldownManager cooldownManager = new CooldownManager(COOLDOWN_SECONDS); + private final String missingSlotMessage; + private final BiConsumer shareAction; + + 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) { + Player player = CommandGuards.requirePlayer(sender); + if (player == null) { + return true; + } + + if (args.length < 1) { + player.sendMessage(ChatColor.RED + missingSlotMessage); + SoundManager.playFailure(player); + return true; + } + + if (cooldownManager.isOnCooldown(player)) { + player.sendMessage(ChatColor.RED + "Please don't spam the command (5 second cooldown)"); + SoundManager.playFailure(player); + return true; + } + + 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; + } + + shareAction.accept(player, slot); + cooldownManager.setCooldown(player); + return true; + } +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommand.java b/src/main/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommand.java new file mode 100644 index 0000000..3852fa5 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommand.java @@ -0,0 +1,84 @@ +/* + * 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 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.Locale; + +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; + } + + if (DisabledCommand.isBlockedInWorld(player)) { + return true; + } + + 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/CommandGuards.java b/src/main/java/dev/noah/perplayerkit/commands/CommandGuards.java new file mode 100644 index 0000000..7e252a4 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/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; + +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/CopyKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/CopyKitCommand.java index 2488c41..90b1aba 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/CopyKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/CopyKitCommand.java @@ -18,7 +18,6 @@ */ package dev.noah.perplayerkit.commands; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.KitShareManager; import org.bukkit.ChatColor; import org.bukkit.command.Command; @@ -31,24 +30,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/DeleteKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java index 9c0cebe..21f0d66 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java @@ -18,7 +18,6 @@ */ 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; @@ -33,46 +32,41 @@ 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)) { + Player player = CommandGuards.requirePlayer(sender, ChatColor.RED + "Only Players can use this!"); + if (player == null) { + return true; + } - 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); - } + UUID uuid = player.getUniqueId(); + if (args.length != 1) { + player.sendMessage(ChatColor.RED + "Usage: /deletekit "); + SoundManager.playFailure(player); + return true; + } - } else { - player.sendMessage(ChatColor.RED + "Kit " + slot + " doesnt exist!"); - SoundManager.playFailure(player); - } + Integer slot = SlotArgumentParser.parseSlot(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)) { + player.sendMessage(ChatColor.RED + "Kit " + slot + " doesnt exist!"); + SoundManager.playFailure(player); + return true; + } - } else { - player.sendMessage(ChatColor.RED + "Usage: /deletekit "); - SoundManager.playFailure(player); - } + if (kitManager.deleteKit(uuid, slot)) { + player.sendMessage(ChatColor.GREEN + "Kit " + slot + " deleted!"); + SoundManager.playSuccess(player); } else { - sender.sendMessage(ChatColor.RED + "Only Players can use this!"); - if (sender instanceof Player s) SoundManager.playFailure(s); - + 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/EnderchestCommand.java index f8d6c0e..b5c4545 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/EnderchestCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/EnderchestCommand.java @@ -19,13 +19,12 @@ package dev.noah.perplayerkit.commands; 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,17 +33,12 @@ 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; } @@ -70,5 +64,3 @@ public void viewOnlyEC(Player p) { SoundManager.playOpenGui(p); } } - - diff --git a/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java b/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java index e4ac35f..ace907b 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java @@ -20,161 +20,43 @@ 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 class InspectEcCommand extends AbstractInspectCommand { public InspectEcCommand(Plugin plugin) { - this.plugin = plugin; + super(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); - } - }); - }); - }); + protected String usageCommand() { + return "inspectec"; + } - // 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; - }); + @Override + protected boolean hasData(UUID targetUuid, int slot) { + return KitManager.get().hasEC(targetUuid, slot); + } - return true; + @Override + protected void openInspectGui(Player inspector, UUID targetUuid, int slot) { + GUI gui = new GUI(plugin); + gui.InspectEc(inspector, targetUuid, slot); } @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(); - } + protected String missingDataMessage(String targetName, int slot) { + return "" + targetName + " does not have an enderchest in slot " + slot + ""; + } - 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()); - } + @Override + protected String loadErrorLogMessage() { + return "Error loading enderchest data"; + } - return new ArrayList<>(); + @Override + protected String loadErrorUserMessage() { + return "An error occurred while loading enderchest data. See console for details."; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java index 47d2c27..82348a4 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java @@ -20,167 +20,43 @@ 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 InspectKitCommand implements CommandExecutor, TabCompleter { - private final Plugin plugin; +public class InspectKitCommand extends AbstractInspectCommand { public InspectKitCommand(Plugin plugin) { - this.plugin = plugin; + super(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, "inspectkit"); - 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().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); - } - }); - }); - }); - - // 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); - }); - return null; - }); - - return true; + protected String usageCommand() { + return "inspectkit"; } @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(); + protected boolean hasData(UUID targetUuid, int slot) { + return KitManager.get().hasKit(targetUuid, slot); + } - // Add online player names - List completions = new ArrayList<>(Bukkit.getOnlinePlayers().stream() - .map(Player::getName) - .filter(name -> name.toLowerCase().startsWith(input)) - .toList()); + @Override + protected void openInspectGui(Player inspector, UUID targetUuid, int slot) { + GUI gui = new GUI(plugin); + gui.InspectKit(inspector, targetUuid, slot); + } - // 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) - .map(UUID::toString) - .filter(uuid -> uuid.startsWith(input)) - .toList()); - } + @Override + protected String missingDataMessage(String targetName, int slot) { + return "" + targetName + " does not have a kit in slot " + slot + ""; + } - return completions; - } else if (args.length == 2) { - // Return slot numbers for second argument - return IntStream.rangeClosed(MIN_SLOT, MAX_SLOT) - .mapToObj(String::valueOf) - .filter(slot -> slot.startsWith(args[1])) - .collect(Collectors.toList()); - } + @Override + protected String loadErrorLogMessage() { + return "Error loading kit data"; + } - return new ArrayList<>(); + @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/MainMenuCommand.java b/src/main/java/dev/noah/perplayerkit/commands/MainMenuCommand.java index 63acebf..1b4bbee 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/MainMenuCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/MainMenuCommand.java @@ -18,7 +18,6 @@ */ package dev.noah.perplayerkit.commands; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.gui.GUI; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -29,21 +28,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/PerPlayerKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/PerPlayerKitCommand.java index a89415c..afd9121 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/PerPlayerKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/PerPlayerKitCommand.java @@ -38,8 +38,9 @@ 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){ + private final Plugin plugin; + + public PerPlayerKitCommand(Plugin plugin) { this.plugin = plugin; } @@ -55,78 +56,9 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command 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; + return handleImport(sender, args); 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; + return handleMigrate(sender, args); default: sender.sendMessage(ChatColor.RED + "Invalid subcommand!"); return true; @@ -134,26 +66,116 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command } } + 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"); + } + + 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) { + if (args.length == 1) { return List.of("about", "import", "migrate"); } - if(args.length == 2 && args[0].equalsIgnoreCase("import")) { + if (args.length == 2 && args[0].equalsIgnoreCase("import")) { return List.of("kitsx"); } - if(args.length == 2 && args[0].equalsIgnoreCase("migrate")) { + 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")) { + if (args.length == 3 && args[0].equalsIgnoreCase("migrate")) { String sourceType = args[1].toLowerCase(); return STORAGE_TYPES.stream() .filter(type -> !type.equals(sourceType)) diff --git a/src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java index 47d51d4..8ae6b4d 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java @@ -18,7 +18,6 @@ */ package dev.noah.perplayerkit.commands; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.KitManager; import dev.noah.perplayerkit.gui.GUI; import org.bukkit.command.Command; @@ -35,51 +34,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/RegearCommand.java b/src/main/java/dev/noah/perplayerkit/commands/RegearCommand.java index feec00e..eb9c129 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/RegearCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/RegearCommand.java @@ -4,7 +4,6 @@ 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 +27,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 +55,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 +83,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 +120,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 +134,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/SavePublicKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/SavePublicKitCommand.java index f159d3c..ea9eb20 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/SavePublicKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/SavePublicKitCommand.java @@ -18,7 +18,6 @@ */ package dev.noah.perplayerkit.commands; -import dev.noah.perplayerkit.util.DisabledCommand; import dev.noah.perplayerkit.ItemFilter; import dev.noah.perplayerkit.KitManager; import org.bukkit.ChatColor; @@ -38,33 +37,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]; 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"); + player.sendMessage(ChatColor.RED + "Public kit " + kidId + " 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 @@ -83,11 +75,11 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command boolean success = kitManager.savePublicKit(kidId, data); if (success) { kitManager.savePublicKitToDB(kidId); - p.sendMessage("Saved kit " + kidId); - SoundManager.playSuccess(p); + player.sendMessage("Saved kit " + kidId); + SoundManager.playSuccess(player); } else { - p.sendMessage("Error saving kit " + kidId); - SoundManager.playFailure(p); + player.sendMessage("Error saving kit " + kidId); + SoundManager.playFailure(player); } return true; diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java index c9384a8..2ae72ae 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java @@ -18,56 +18,11 @@ */ package dev.noah.perplayerkit.commands; -import com.google.common.primitives.Ints; import dev.noah.perplayerkit.KitShareManager; -import dev.noah.perplayerkit.util.CooldownManager; -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 ShareECKitCommand implements CommandExecutor { - - private final CooldownManager shareECCommandCooldown; +public class ShareECKitCommand extends AbstractShareSlotCommand { public ShareECKitCommand() { - this.shareECCommandCooldown = new CooldownManager(5); - } - - @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 (args.length < 1) { - player.sendMessage(ChatColor.RED + "Error, you must select a EC slot to share"); - SoundManager.playFailure(player); - return true; - } - - if (shareECCommandCooldown.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) { - player.sendMessage(ChatColor.RED + "Select a valid kit slot"); - SoundManager.playFailure(player); - return true; - } - - KitShareManager.get().shareEC(player, slot); - shareECCommandCooldown.setCooldown(player); - - return true; + super("Error, you must select a EC slot to share", (player, slot) -> KitShareManager.get().shareEC(player, slot)); } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java index 52c4b89..c3052ce 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java @@ -18,56 +18,11 @@ */ package dev.noah.perplayerkit.commands; -import com.google.common.primitives.Ints; import dev.noah.perplayerkit.KitShareManager; -import dev.noah.perplayerkit.util.CooldownManager; -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 { - - private final CooldownManager shareKitCommandCooldown; +public class ShareKitCommand extends AbstractShareSlotCommand { public ShareKitCommand() { - this.shareKitCommandCooldown = new CooldownManager(5); - } - - @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 (args.length < 1) { - player.sendMessage(ChatColor.RED + "Error, you must select a kit slot to share"); - SoundManager.playFailure(player); - return true; - } - - if (shareKitCommandCooldown.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) { - player.sendMessage(ChatColor.RED + "Select a valid kit slot"); - SoundManager.playFailure(player); - return true; - } - - KitShareManager.get().shareKit(player, slot); - shareKitCommandCooldown.setCooldown(player); - - return true; + 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/ShortECCommand.java b/src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java index 476bc70..2e53d1c 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java @@ -18,42 +18,17 @@ */ 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 extends AbstractShortSlotCommand { -public class ShortECCommand implements CommandExecutor { + public ShortECCommand() { + super("ec", "enderchest"); + } @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; + protected void executeForSlot(Player player, int slot) { + KitManager.get().loadEnderchest(player, slot); } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java index 20f864e..bc865fd 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java @@ -18,43 +18,17 @@ */ 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 ShortKitCommand extends AbstractShortSlotCommand { -public class ShortKitCommand implements CommandExecutor { + public ShortKitCommand() { + super("k", "kit"); + } @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(); - - // 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 { - player.sendMessage("Invalid command label."); - } - - return true; + protected void executeForSlot(Player player, int slot) { + KitManager.get().loadKit(player, slot); } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/SlotArgumentParser.java b/src/main/java/dev/noah/perplayerkit/commands/SlotArgumentParser.java new file mode 100644 index 0000000..b4623a3 --- /dev/null +++ b/src/main/java/dev/noah/perplayerkit/commands/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; + +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/SwapKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/SwapKitCommand.java index 06815c6..b91ae1c 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/SwapKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/SwapKitCommand.java @@ -18,7 +18,6 @@ */ package dev.noah.perplayerkit.commands; -import com.google.common.primitives.Ints; import dev.noah.perplayerkit.KitManager; import org.bukkit.ChatColor; import dev.noah.perplayerkit.util.SoundManager; @@ -34,8 +33,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 +44,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.parseSlot(args[0]); + Integer slot2 = SlotArgumentParser.parseSlot(args[1]); if (slot1 == null || slot2 == null) { player.sendMessage(ChatColor.RED + "Usage: /swapkit "); diff --git a/src/main/java/dev/noah/perplayerkit/commands/extracommands/HealCommand.java b/src/main/java/dev/noah/perplayerkit/commands/extracommands/HealCommand.java index 1fe9775..de98fdf 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/extracommands/HealCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/extracommands/HealCommand.java @@ -18,6 +18,7 @@ */ package dev.noah.perplayerkit.commands.extracommands; +import dev.noah.perplayerkit.commands.CommandGuards; import dev.noah.perplayerkit.util.BroadcastManager; import dev.noah.perplayerkit.util.PlayerUtil; import org.bukkit.ChatColor; @@ -31,9 +32,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/extracommands/RepairCommand.java b/src/main/java/dev/noah/perplayerkit/commands/extracommands/RepairCommand.java index 389554e..d51573d 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/extracommands/RepairCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/extracommands/RepairCommand.java @@ -18,6 +18,7 @@ */ package dev.noah.perplayerkit.commands.extracommands; +import dev.noah.perplayerkit.commands.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/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java b/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java new file mode 100644 index 0000000..7fe5329 --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java @@ -0,0 +1,98 @@ +package dev.noah.perplayerkit.commands; + +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..024e09b --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java @@ -0,0 +1,97 @@ +package dev.noah.perplayerkit.commands; + +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..eccc26a --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java @@ -0,0 +1,59 @@ +package dev.noah.perplayerkit.commands; + +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..72e6f2b --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java @@ -0,0 +1,29 @@ +package dev.noah.perplayerkit.commands; + +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)); + } +} From 0cdce4da0bbb9ab874f2ac3f34b32818d2ba94c2 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Sat, 7 Mar 2026 16:34:04 -0500 Subject: [PATCH 05/28] more command refactors --- .../dev/noah/perplayerkit/PerPlayerKit.java | 26 ++++++++-- .../{ => admin}/AboutCommandListener.java | 2 +- .../commands/{ => admin}/KitRoomCommand.java | 52 +++++++++++-------- .../{ => admin}/PerPlayerKitCommand.java | 2 +- .../{ => admin}/SavePublicKitCommand.java | 17 +++--- .../ECSlotTabCompleter.java | 2 +- .../KitSlotTabCompleter.java | 2 +- .../commands/{ => core}/CommandGuards.java | 2 +- .../{ => core}/SlotArgumentParser.java | 2 +- .../HealCommand.java | 5 +- .../{ => features}/RegearCommand.java | 3 +- .../RepairCommand.java | 4 +- .../{ => inspect}/AbstractInspectCommand.java | 16 +++--- .../{ => inspect}/InspectCommandUtil.java | 2 +- .../{ => inspect}/InspectEcCommand.java | 2 +- .../{ => inspect}/InspectKitCommand.java | 2 +- .../commands/{ => kits}/DeleteKitCommand.java | 4 +- .../{ => kits}/EnderchestCommand.java | 11 ++-- .../commands/{ => kits}/MainMenuCommand.java | 3 +- .../commands/{ => kits}/PublicKitCommand.java | 3 +- .../commands/{ => kits}/SwapKitCommand.java | 4 +- .../{ => share}/AbstractShareSlotCommand.java | 4 +- .../commands/{ => share}/CopyKitCommand.java | 3 +- .../{ => share}/ShareECKitCommand.java | 2 +- .../commands/{ => share}/ShareKitCommand.java | 2 +- .../AbstractShortSlotCommand.java | 2 +- .../{ => shortcuts}/ShortECCommand.java | 2 +- .../{ => shortcuts}/ShortKitCommand.java | 2 +- .../AbstractShareSlotCommandTest.java | 1 + .../AbstractShortSlotCommandTest.java | 1 + .../commands/CommandGuardsTest.java | 1 + .../commands/SlotArgumentParserTest.java | 1 + 32 files changed, 114 insertions(+), 73 deletions(-) rename src/main/java/dev/noah/perplayerkit/commands/{ => admin}/AboutCommandListener.java (98%) rename src/main/java/dev/noah/perplayerkit/commands/{ => admin}/KitRoomCommand.java (60%) rename src/main/java/dev/noah/perplayerkit/commands/{ => admin}/PerPlayerKitCommand.java (99%) rename src/main/java/dev/noah/perplayerkit/commands/{ => admin}/SavePublicKitCommand.java (87%) rename src/main/java/dev/noah/perplayerkit/commands/{tabcompleters => completion}/ECSlotTabCompleter.java (96%) rename src/main/java/dev/noah/perplayerkit/commands/{tabcompleters => completion}/KitSlotTabCompleter.java (96%) rename src/main/java/dev/noah/perplayerkit/commands/{ => core}/CommandGuards.java (97%) rename src/main/java/dev/noah/perplayerkit/commands/{ => core}/SlotArgumentParser.java (96%) rename src/main/java/dev/noah/perplayerkit/commands/{extracommands => features}/HealCommand.java (92%) rename src/main/java/dev/noah/perplayerkit/commands/{ => features}/RegearCommand.java (98%) rename src/main/java/dev/noah/perplayerkit/commands/{extracommands => features}/RepairCommand.java (93%) rename src/main/java/dev/noah/perplayerkit/commands/{ => inspect}/AbstractInspectCommand.java (91%) rename src/main/java/dev/noah/perplayerkit/commands/{ => inspect}/InspectCommandUtil.java (99%) rename src/main/java/dev/noah/perplayerkit/commands/{ => inspect}/InspectEcCommand.java (97%) rename src/main/java/dev/noah/perplayerkit/commands/{ => inspect}/InspectKitCommand.java (97%) rename src/main/java/dev/noah/perplayerkit/commands/{ => kits}/DeleteKitCommand.java (94%) rename src/main/java/dev/noah/perplayerkit/commands/{ => kits}/EnderchestCommand.java (88%) rename src/main/java/dev/noah/perplayerkit/commands/{ => kits}/MainMenuCommand.java (93%) rename src/main/java/dev/noah/perplayerkit/commands/{ => kits}/PublicKitCommand.java (95%) rename src/main/java/dev/noah/perplayerkit/commands/{ => kits}/SwapKitCommand.java (94%) rename src/main/java/dev/noah/perplayerkit/commands/{ => share}/AbstractShareSlotCommand.java (94%) rename src/main/java/dev/noah/perplayerkit/commands/{ => share}/CopyKitCommand.java (94%) rename src/main/java/dev/noah/perplayerkit/commands/{ => share}/ShareECKitCommand.java (95%) rename src/main/java/dev/noah/perplayerkit/commands/{ => share}/ShareKitCommand.java (95%) rename src/main/java/dev/noah/perplayerkit/commands/{ => shortcuts}/AbstractShortSlotCommand.java (98%) rename src/main/java/dev/noah/perplayerkit/commands/{ => shortcuts}/ShortECCommand.java (95%) rename src/main/java/dev/noah/perplayerkit/commands/{ => shortcuts}/ShortKitCommand.java (95%) 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/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/PerPlayerKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java similarity index 99% rename from src/main/java/dev/noah/perplayerkit/commands/PerPlayerKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java index afd9121..3f63130 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/PerPlayerKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.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.storage.StorageMigrator; import dev.noah.perplayerkit.util.importutil.KitsXImporter; 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 87% rename from src/main/java/dev/noah/perplayerkit/commands/SavePublicKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java index ea9eb20..6594208 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/SavePublicKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.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.admin; 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; @@ -48,10 +49,10 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - String kidId = args[0]; + String kitId = args[0]; - if (KitManager.get().getPublicKitList().stream().noneMatch(kit -> kit.id.equals(kidId))) { - player.sendMessage(ChatColor.RED + "Public kit " + kidId + " does not exist"); + 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; } @@ -72,13 +73,13 @@ 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); - player.sendMessage("Saved kit " + kidId); + kitManager.savePublicKitToDB(kitId); + player.sendMessage("Saved kit " + kitId); SoundManager.playSuccess(player); } else { - player.sendMessage("Error saving kit " + kidId); + player.sendMessage("Error saving kit " + kitId); SoundManager.playFailure(player); } 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/CommandGuards.java b/src/main/java/dev/noah/perplayerkit/commands/core/CommandGuards.java similarity index 97% rename from src/main/java/dev/noah/perplayerkit/commands/CommandGuards.java rename to src/main/java/dev/noah/perplayerkit/commands/core/CommandGuards.java index 7e252a4..c6c0b02 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/CommandGuards.java +++ b/src/main/java/dev/noah/perplayerkit/commands/core/CommandGuards.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.core; import dev.noah.perplayerkit.util.DisabledCommand; import org.bukkit.command.CommandSender; diff --git a/src/main/java/dev/noah/perplayerkit/commands/SlotArgumentParser.java b/src/main/java/dev/noah/perplayerkit/commands/core/SlotArgumentParser.java similarity index 96% rename from src/main/java/dev/noah/perplayerkit/commands/SlotArgumentParser.java rename to src/main/java/dev/noah/perplayerkit/commands/core/SlotArgumentParser.java index b4623a3..fbe012d 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/SlotArgumentParser.java +++ b/src/main/java/dev/noah/perplayerkit/commands/core/SlotArgumentParser.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.core; import com.google.common.primitives.Ints; import org.jetbrains.annotations.Nullable; 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 92% 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 de98fdf..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,12 +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.CommandGuards; +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; 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 98% rename from src/main/java/dev/noah/perplayerkit/commands/RegearCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/features/RegearCommand.java index eb9c129..68af438 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/RegearCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/features/RegearCommand.java @@ -1,6 +1,7 @@ -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; 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 93% 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 d51573d..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,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.extracommands; +package dev.noah.perplayerkit.commands.features; -import dev.noah.perplayerkit.commands.CommandGuards; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.util.BroadcastManager; import dev.noah.perplayerkit.util.PlayerUtil; import org.bukkit.command.Command; diff --git a/src/main/java/dev/noah/perplayerkit/commands/AbstractInspectCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java similarity index 91% rename from src/main/java/dev/noah/perplayerkit/commands/AbstractInspectCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java index 661d3c2..20ccf68 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/AbstractInspectCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.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.inspect; import dev.noah.perplayerkit.KitManager; import dev.noah.perplayerkit.util.BroadcastManager; @@ -38,13 +38,13 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static dev.noah.perplayerkit.commands.InspectCommandUtil.ERROR_PREFIX; -import static dev.noah.perplayerkit.commands.InspectCommandUtil.MAX_SLOT; -import static dev.noah.perplayerkit.commands.InspectCommandUtil.MIN_SLOT; -import static dev.noah.perplayerkit.commands.InspectCommandUtil.getPlayerName; -import static dev.noah.perplayerkit.commands.InspectCommandUtil.mm; -import static dev.noah.perplayerkit.commands.InspectCommandUtil.resolvePlayerIdentifierAsync; -import static dev.noah.perplayerkit.commands.InspectCommandUtil.showUsage; +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.getPlayerName; +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; public abstract class AbstractInspectCommand implements CommandExecutor, TabCompleter { protected final Plugin plugin; diff --git a/src/main/java/dev/noah/perplayerkit/commands/InspectCommandUtil.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java similarity index 99% rename from src/main/java/dev/noah/perplayerkit/commands/InspectCommandUtil.java rename to src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java index b9c7bf4..b18a647 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/InspectCommandUtil.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.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.inspect; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; diff --git a/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectEcCommand.java similarity index 97% rename from src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/inspect/InspectEcCommand.java index ace907b..f85d127 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/InspectEcCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectEcCommand.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.inspect; import dev.noah.perplayerkit.KitManager; import dev.noah.perplayerkit.gui.GUI; diff --git a/src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectKitCommand.java similarity index 97% rename from src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/inspect/InspectKitCommand.java index 82348a4..810d631 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/InspectKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectKitCommand.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.inspect; import dev.noah.perplayerkit.KitManager; import dev.noah.perplayerkit.gui.GUI; diff --git a/src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java similarity index 94% rename from src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java index 21f0d66..e969c4c 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/DeleteKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java @@ -16,9 +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 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; 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 88% rename from src/main/java/dev/noah/perplayerkit/commands/EnderchestCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/EnderchestCommand.java index b5c4545..49bf713 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/EnderchestCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/EnderchestCommand.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; +package dev.noah.perplayerkit.commands.kits; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.gui.ItemUtil; import dev.noah.perplayerkit.util.StyleManager; import dev.noah.perplayerkit.util.SoundManager; @@ -42,7 +43,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - public void viewOnlyEC(Player p) { + public void viewOnlyEC(Player player) { ItemStack fill = ItemUtil.createGlassPane(); @@ -56,11 +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 93% rename from src/main/java/dev/noah/perplayerkit/commands/MainMenuCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/MainMenuCommand.java index 1b4bbee..1a8e14b 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/MainMenuCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/MainMenuCommand.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; +package dev.noah.perplayerkit.commands.kits; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.gui.GUI; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; 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 95% rename from src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/PublicKitCommand.java index 8ae6b4d..b365bb9 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/PublicKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/PublicKitCommand.java @@ -16,9 +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.KitManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; import dev.noah.perplayerkit.gui.GUI; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; 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 94% rename from src/main/java/dev/noah/perplayerkit/commands/SwapKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java index b91ae1c..8cd2fcf 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/SwapKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java @@ -16,9 +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 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; diff --git a/src/main/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommand.java b/src/main/java/dev/noah/perplayerkit/commands/share/AbstractShareSlotCommand.java similarity index 94% rename from src/main/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/share/AbstractShareSlotCommand.java index de7379a..96e7c4a 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/share/AbstractShareSlotCommand.java @@ -16,8 +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.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; 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 94% rename from src/main/java/dev/noah/perplayerkit/commands/CopyKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/share/CopyKitCommand.java index 90b1aba..e171378 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/CopyKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/share/CopyKitCommand.java @@ -16,9 +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.KitShareManager; +import dev.noah.perplayerkit.commands.core.CommandGuards; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java similarity index 95% rename from src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java index 2ae72ae..5aeb11b 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShareECKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.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.share; import dev.noah.perplayerkit.KitShareManager; diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/share/ShareKitCommand.java similarity index 95% rename from src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/share/ShareKitCommand.java index c3052ce..7dc567b 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShareKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/share/ShareKitCommand.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.share; import dev.noah.perplayerkit.KitShareManager; diff --git a/src/main/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommand.java b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/AbstractShortSlotCommand.java similarity index 98% rename from src/main/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/shortcuts/AbstractShortSlotCommand.java index 3852fa5..ad0a027 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/AbstractShortSlotCommand.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.shortcuts; import dev.noah.perplayerkit.util.DisabledCommand; import org.bukkit.command.Command; diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortECCommand.java similarity index 95% rename from src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortECCommand.java index 2e53d1c..0a86b27 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShortECCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortECCommand.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.shortcuts; import dev.noah.perplayerkit.KitManager; import org.bukkit.entity.Player; diff --git a/src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortKitCommand.java similarity index 95% rename from src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java rename to src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortKitCommand.java index bc865fd..368ee32 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/ShortKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/shortcuts/ShortKitCommand.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.shortcuts; import dev.noah.perplayerkit.KitManager; import org.bukkit.entity.Player; diff --git a/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java b/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java index 7fe5329..a888e15 100644 --- a/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java +++ b/src/test/java/dev/noah/perplayerkit/commands/AbstractShareSlotCommandTest.java @@ -1,5 +1,6 @@ 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; diff --git a/src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java b/src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java index 024e09b..0208c0d 100644 --- a/src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java +++ b/src/test/java/dev/noah/perplayerkit/commands/AbstractShortSlotCommandTest.java @@ -1,5 +1,6 @@ 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; diff --git a/src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java b/src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java index eccc26a..17a88a2 100644 --- a/src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java +++ b/src/test/java/dev/noah/perplayerkit/commands/CommandGuardsTest.java @@ -1,5 +1,6 @@ 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; diff --git a/src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java b/src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java index 72e6f2b..7d81ec1 100644 --- a/src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java +++ b/src/test/java/dev/noah/perplayerkit/commands/SlotArgumentParserTest.java @@ -1,5 +1,6 @@ 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; From 9caf501ab2cd7222dff2880a980ff50c50808137 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Sat, 7 Mar 2026 16:38:37 -0500 Subject: [PATCH 06/28] fix noisy tests --- pom.xml | 16 +++++++++++++- .../perplayerkit/storage/RedisStorage.java | 22 ++++++++++++------- .../storage/StorageMigratorTest.java | 7 +++++- .../perplayerkit/storage/YAMLStorageTest.java | 7 +++++- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 539915f..c235c2b 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 @@ -78,6 +83,9 @@ 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 + @@ -183,7 +191,13 @@ org.mockito mockito-core - 5.15.2 + ${mockito.version} + test + + + org.slf4j + slf4j-nop + 1.7.36 test diff --git a/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java b/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java index 4b02472..8eb48f1 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,15 @@ 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().fine("Redis " + operation + " failed: " + exception.getMessage()); + } } diff --git a/src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java b/src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java index f725bab..fb7e9de 100644 --- a/src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java +++ b/src/test/java/dev/noah/perplayerkit/storage/StorageMigratorTest.java @@ -7,6 +7,8 @@ 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; @@ -24,7 +26,10 @@ class StorageMigratorTest { @BeforeEach void setUp() { plugin = mock(Plugin.class); - when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("StorageMigratorTest")); + Logger logger = Logger.getLogger("StorageMigratorTest"); + logger.setUseParentHandlers(false); + logger.setLevel(Level.OFF); + when(plugin.getLogger()).thenReturn(logger); } @Test diff --git a/src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java b/src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java index 91acd68..c60d07f 100644 --- a/src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java +++ b/src/test/java/dev/noah/perplayerkit/storage/YAMLStorageTest.java @@ -7,6 +7,8 @@ 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; @@ -21,7 +23,10 @@ class YAMLStorageTest { @BeforeEach void setUp() { plugin = mock(Plugin.class); - when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("YAMLStorageTest")); + Logger logger = Logger.getLogger("YAMLStorageTest"); + logger.setUseParentHandlers(false); + logger.setLevel(Level.OFF); + when(plugin.getLogger()).thenReturn(logger); } @Test From 829c906554396f11dbb5e66ffef483b00b2671df Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 10:58:51 -0400 Subject: [PATCH 07/28] Improve Redis error logging and remove unused Maven repo - Log Redis operation failures at `SEVERE` instead of `FINE` - Include exception string and full stack trace in Redis error logs - Remove the `papermc-repo` Maven repository entry from `pom.xml` --- pom.xml | 4 ---- .../java/dev/noah/perplayerkit/storage/RedisStorage.java | 6 +++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index c235c2b..f92de2d 100644 --- a/pom.xml +++ b/pom.xml @@ -98,10 +98,6 @@ - - papermc-repo - https://papermc.io/repo/repository/maven-public/ - sonatype https://oss.sonatype.org/content/groups/public/ diff --git a/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java b/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java index 8eb48f1..ca11158 100644 --- a/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java +++ b/src/main/java/dev/noah/perplayerkit/storage/RedisStorage.java @@ -148,6 +148,10 @@ private void logRedisFailure(String operation, Exception exception) { if (plugin == null || plugin.getLogger() == null) { return; } - plugin.getLogger().fine("Redis " + operation + " failed: " + exception.getMessage()); + plugin.getLogger().severe("Redis " + operation + " failed: " + exception.getMessage()); + plugin.getLogger().severe(exception.toString()); + for (StackTraceElement element : exception.getStackTrace()) { + plugin.getLogger().severe("\tat " + element); + } } } From 8aa8d83690fa0f0e9fa2256e6791da548c271e68 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 11:00:41 -0400 Subject: [PATCH 08/28] Limit CI push trigger to main branch - Run CI on push events only for `main` - Keep pull request CI checks for all branches --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: [ "**" ] From b2cc93c235cbec9262370c7da53c299b9e9e8d71 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 11:02:55 -0400 Subject: [PATCH 09/28] Stop import command when KitsX files are missing - Return early after reporting missing import files - Prevent running the importer when required data is absent --- .../noah/perplayerkit/commands/admin/PerPlayerKitCommand.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java index 3f63130..74a7223 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java @@ -82,6 +82,7 @@ private boolean handleImport(CommandSender sender, String[] args) { 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(); From cc00860462bf820f8614b7504c9907bd4455a8ce Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 11:10:06 -0400 Subject: [PATCH 10/28] Handle missing inspect names and tighten UUID resolution - Reuse a single "player not found" path in inspect command handling - Return null when a UUID cannot be mapped to a known name instead of showing the UUID - Resolve identifiers via cached offline players first, then Mojang, without offline UUID fallback --- .../inspect/AbstractInspectCommand.java | 19 ++-- .../commands/inspect/InspectCommandUtil.java | 95 ++++++++++++------- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java index 20ccf68..b9bc73a 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java @@ -95,12 +95,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command CompletableFuture future = resolvePlayerIdentifierAsync(args[0]) .thenCompose(targetUuid -> { if (targetUuid == null) { - 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); - }); + Bukkit.getScheduler().runTask(plugin, () -> showPlayerNotFound(player)); return CompletableFuture.completedFuture(null); } @@ -184,8 +179,20 @@ private void showInspectResult(Player inspector, UUID targetUuid, int slot) { } String targetName = getPlayerName(targetUuid); + if (targetName == null) { + showPlayerNotFound(inspector); + return; + } + 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 index b18a647..79f0e22 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java @@ -31,7 +31,6 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -48,7 +47,7 @@ private InspectCommandUtil() { /** * 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. + * and finally searches cached offline players and Mojang asynchronously. * * @param identifier Player name or UUID string * @return CompletableFuture containing UUID if found, null otherwise @@ -68,53 +67,30 @@ public static CompletableFuture resolvePlayerIdentifierAsync(String identi 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 + UUID cachedOfflinePlayer = findCachedOfflinePlayerUuid(identifier); + if (cachedOfflinePlayer != null) { + return cachedOfflinePlayer; } - // Fallback for offline/cracked-mode servers: compute deterministic offline UUID - return UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)); + + return lookupPlayerUuidFromMojang(identifier); }); } /** - * Gets a player's name from their UUID, falling back to UUID string if name is not available. + * Gets a player's name from their UUID. * * @param uuid Player UUID - * @return Player name or UUID string + * @return Player name, or null if the UUID cannot be associated with a known player name */ - public static String getPlayerName(@NotNull UUID uuid) { + public static @Nullable 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(); + return offlinePlayer.getName(); } /** @@ -128,4 +104,55 @@ public static void showUsage(@NotNull Player player, @NotNull String commandName ERROR_PREFIX.append( mm.deserialize("Usage: /" + commandName + " "))); } + + private static @Nullable UUID findCachedOfflinePlayerUuid(@NotNull String identifier) { + for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { + String name = offlinePlayer.getName(); + if (name != null && name.equalsIgnoreCase(identifier)) { + return offlinePlayer.getUniqueId(); + } + } + 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; + } + } } From fb7ea5a4aa03e67587aaef7795d4048800a702d5 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 11:13:08 -0400 Subject: [PATCH 11/28] Fix grammar in EC slot share error message --- .../dev/noah/perplayerkit/commands/share/ShareECKitCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java index 5aeb11b..6951e73 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/share/ShareECKitCommand.java @@ -23,6 +23,6 @@ public class ShareECKitCommand extends AbstractShareSlotCommand { public ShareECKitCommand() { - super("Error, you must select a EC slot to share", (player, slot) -> KitShareManager.get().shareEC(player, slot)); + super("Error, you must select an EC slot to share", (player, slot) -> KitShareManager.get().shareEC(player, slot)); } } From ba313b7cd11437d61b9cf8c3208e63e3544825ef Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 11:14:14 -0400 Subject: [PATCH 12/28] Update src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../noah/perplayerkit/commands/admin/PerPlayerKitCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java index 74a7223..1931086 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java @@ -184,6 +184,6 @@ public List onTabComplete(@NotNull CommandSender sender, @NotNull Comman .collect(Collectors.toList()); } - return null; + return List.of(); } } From 577d482888c2af624f68a632bfd48a2cadca2758 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 12:30:44 -0400 Subject: [PATCH 13/28] Refactor player lookup utilities and add inspect/player tests - move player name resolution into `PlayerUtil` with UUID fallback - add offline-mode UUID fallback in inspect identifier resolution - remove unused GUI wrapper methods and imports - add unit tests for inspect UUID selection and player name resolution --- .../inspect/AbstractInspectCommand.java | 7 +-- .../commands/inspect/InspectCommandUtil.java | 43 +++++++------- .../java/dev/noah/perplayerkit/gui/GUI.java | 51 +--------------- .../noah/perplayerkit/util/PlayerUtil.java | 19 ++++++ .../inspect/InspectCommandUtilTest.java | 50 ++++++++++++++++ .../perplayerkit/util/PlayerUtilTest.java | 58 +++++++++++++++++++ 6 files changed, 151 insertions(+), 77 deletions(-) create mode 100644 src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java create mode 100644 src/test/java/dev/noah/perplayerkit/util/PlayerUtilTest.java diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java index b9bc73a..ec71dbd 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java @@ -41,10 +41,10 @@ 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.getPlayerName; 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 abstract class AbstractInspectCommand implements CommandExecutor, TabCompleter { protected final Plugin plugin; @@ -179,11 +179,6 @@ private void showInspectResult(Player inspector, UUID targetUuid, int slot) { } String targetName = getPlayerName(targetUuid); - if (targetName == null) { - showPlayerNotFound(inspector); - return; - } - BroadcastManager.get().sendComponentMessage(inspector, ERROR_PREFIX.append(mm.deserialize(missingDataMessage(targetName, slot)))); SoundManager.playFailure(inspector); diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java index 79f0e22..0d9a9b2 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java @@ -31,8 +31,10 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; public class InspectCommandUtil { public static final int MIN_SLOT = 1; @@ -69,30 +71,11 @@ public static CompletableFuture resolvePlayerIdentifierAsync(String identi return CompletableFuture.supplyAsync(() -> { UUID cachedOfflinePlayer = findCachedOfflinePlayerUuid(identifier); - if (cachedOfflinePlayer != null) { - return cachedOfflinePlayer; - } - - return lookupPlayerUuidFromMojang(identifier); + return selectResolvedUuid(identifier, cachedOfflinePlayer, + () -> lookupPlayerUuidFromMojang(identifier), Bukkit.getOnlineMode()); }); } - /** - * Gets a player's name from their UUID. - * - * @param uuid Player UUID - * @return Player name, or null if the UUID cannot be associated with a known player name - */ - public static @Nullable String getPlayerName(@NotNull UUID uuid) { - Player onlinePlayer = Bukkit.getPlayer(uuid); - if (onlinePlayer != null) { - return onlinePlayer.getName(); - } - - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); - return offlinePlayer.getName(); - } - /** * Shows command usage message to the player. * @@ -155,4 +138,22 @@ public static void showUsage(@NotNull Player player, @NotNull String commandName return null; } } + + static @Nullable UUID selectResolvedUuid(@NotNull String identifier, @Nullable UUID cachedOfflinePlayer, + @NotNull Supplier mojangLookup, boolean onlineMode) { + if (cachedOfflinePlayer != null) { + return cachedOfflinePlayer; + } + + UUID mojangUuid = mojangLookup.get(); + if (mojangUuid != null) { + return mojangUuid; + } + + if (!onlineMode) { + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)); + } + + return null; + } } diff --git a/src/main/java/dev/noah/perplayerkit/gui/GUI.java b/src/main/java/dev/noah/perplayerkit/gui/GUI.java index 6390521..f040407 100644 --- a/src/main/java/dev/noah/perplayerkit/gui/GUI.java +++ b/src/main/java/dev/noah/perplayerkit/gui/GUI.java @@ -24,15 +24,12 @@ 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 java.util.HashMap; @@ -46,6 +43,7 @@ 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,10 +72,6 @@ public static void addLoadPublicKit(Slot slot, String id) { }); } - public static Menu createPublicKitMenu() { - return GuiMenuFactory.createPublicKitRoomMenu(); - } - public static boolean removeKitDeletionFlag(Player player) { return kitDeletionFlag.remove(player.getUniqueId()); } @@ -612,47 +606,4 @@ public void addEditLoadEC(Slot slot, int i) { } }); } - - public Menu createKitMenu(int slot) { - return GuiMenuFactory.createKitMenu(slot); - } - - public Menu createPublicKitMenu(String id) { - return GuiMenuFactory.createPublicKitMenu(id); - } - - public Menu createECMenu(int slot) { - return GuiMenuFactory.createECMenu(slot); - } - - public Menu createInspectMenu(int slot, String playerName) { - return GuiMenuFactory.createInspectMenu(slot, playerName); - } - - public Menu createInspectEcMenu(int slot, String playerName) { - return GuiMenuFactory.createInspectEcMenu(slot, playerName); - } - - public Menu createMainMenu(Player p) { - return GuiMenuFactory.createMainMenu(p); - } - - public Menu createKitRoom() { - return GuiMenuFactory.createKitRoomMenu(); - } - - 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(); - } } 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/inspect/InspectCommandUtilTest.java b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java new file mode 100644 index 0000000..c0378cb --- /dev/null +++ b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java @@ -0,0 +1,50 @@ +package dev.noah.perplayerkit.commands.inspect; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +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"); + }, false); + + assertEquals(cachedUuid, resolvedUuid); + } + + @Test + void selectResolvedUuidUsesMojangResultWhenCacheMisses() { + UUID mojangUuid = UUID.randomUUID(); + + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", null, () -> mojangUuid, true); + + assertEquals(mojangUuid, resolvedUuid); + } + + @Test + void selectResolvedUuidUsesOfflineFallbackWhenServerIsOfflineMode() { + String identifier = "TargetPlayer"; + + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid(identifier, null, () -> null, false); + + assertEquals(UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)), + resolvedUuid); + } + + @Test + void selectResolvedUuidReturnsNullWhenServerIsOnlineModeAndNoMatchExists() { + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", null, () -> null, true); + + assertNull(resolvedUuid); + } +} 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)); + } + } +} From 1c0e47163fe34ac19864a35e89a7795902cdb7bf Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 13:41:07 -0400 Subject: [PATCH 14/28] Use cached offline player lookup for inspect UUID resolution - replace offline player iteration with reflective `getOfflinePlayerIfCached` lookup - add test to verify `Bukkit.getOfflinePlayers()` is never called --- .../commands/inspect/InspectCommandUtil.java | 32 +++++++++++++++---- .../inspect/InspectCommandUtilTest.java | 23 +++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java index 0d9a9b2..6f864f7 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java @@ -31,6 +31,7 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -89,13 +90,32 @@ public static void showUsage(@NotNull Player player, @NotNull String commandName } private static @Nullable UUID findCachedOfflinePlayerUuid(@NotNull String identifier) { - for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { - String name = offlinePlayer.getName(); - if (name != null && name.equalsIgnoreCase(identifier)) { - return offlinePlayer.getUniqueId(); - } + 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; } - return null; } private static @Nullable UUID lookupPlayerUuidFromMojang(@NotNull String identifier) { diff --git a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java index c0378cb..4008b98 100644 --- a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java +++ b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java @@ -1,12 +1,19 @@ 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.nio.charset.StandardCharsets; 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 { @@ -47,4 +54,20 @@ void selectResolvedUuidReturnsNullWhenServerIsOnlineModeAndNoMatchExists() { 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()); + } + } } From 548cf405d573df112927be25d418125a11833371 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 14:16:59 -0400 Subject: [PATCH 15/28] Harden inspect async flow and add player name fallbacks - Re-resolve sender/target on main thread before sending inspect results - Load target data only when offline to avoid unnecessary DB work - Fallback to UUID when target name is unavailable in inspect GUIs/messages --- .../inspect/AbstractInspectCommand.java | 51 +++++++++++++++---- .../java/dev/noah/perplayerkit/gui/GUI.java | 3 ++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java index ec71dbd..92c1ef5 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java @@ -92,27 +92,53 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } + UUID senderUuid = player.getUniqueId(); CompletableFuture future = resolvePlayerIdentifierAsync(args[0]) .thenCompose(targetUuid -> { if (targetUuid == null) { - Bukkit.getScheduler().runTask(plugin, () -> showPlayerNotFound(player)); + Bukkit.getScheduler().runTask(plugin, () -> { + Player currentSender = Bukkit.getPlayer(senderUuid); + if (currentSender == null) { + return; + } + showPlayerNotFound(currentSender); + }); return CompletableFuture.completedFuture(null); } - Player targetPlayer = Bukkit.getPlayer(targetUuid); - return CompletableFuture.runAsync(() -> { - if (targetPlayer == null) { - KitManager.get().loadPlayerDataFromDB(targetUuid); - } - }).thenRun(() -> Bukkit.getScheduler().runTask(plugin, () -> showInspectResult(player, targetUuid, slot))); + 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); + })); + }); }); future.exceptionally(ex -> { Bukkit.getScheduler().runTask(plugin, () -> { plugin.getLogger().severe(loadErrorLogMessage() + ": " + ex.getMessage()); - BroadcastManager.get().sendComponentMessage(player, + Player currentSender = Bukkit.getPlayer(senderUuid); + if (currentSender == null) { + return; + } + + BroadcastManager.get().sendComponentMessage(currentSender, ERROR_PREFIX.append(mm.deserialize(loadErrorUserMessage()))); - SoundManager.playFailure(player); + SoundManager.playFailure(currentSender); }); return null; }); @@ -172,13 +198,16 @@ private int parseSlot(String slotArg, Player player) { } } - private void showInspectResult(Player inspector, UUID targetUuid, int slot) { + private void showInspectResult(Player inspector, @Nullable Player targetPlayer, UUID targetUuid, int slot) { if (hasData(targetUuid, slot)) { openInspectGui(inspector, targetUuid, slot); return; } - String targetName = getPlayerName(targetUuid); + 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); diff --git a/src/main/java/dev/noah/perplayerkit/gui/GUI.java b/src/main/java/dev/noah/perplayerkit/gui/GUI.java index f040407..9738705 100644 --- a/src/main/java/dev/noah/perplayerkit/gui/GUI.java +++ b/src/main/java/dev/noah/perplayerkit/gui/GUI.java @@ -182,6 +182,9 @@ 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); + if (playerName == null) { + playerName = target.toString(); + } Menu menu = GuiMenuFactory.createInspectEcMenu(slot, playerName); setGlassPaneRange(menu, 0, EC_CONTENT_START); From f48c60427645dcfac2c6fd8208ce721e7549dbb3 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 14:17:38 -0400 Subject: [PATCH 16/28] Inline async player data load call in inspect command - Collapse the offline target data-load lambda to a single-line `runAsync` call - Keep inspect flow unchanged while improving readability around async composition --- .../perplayerkit/commands/inspect/AbstractInspectCommand.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java index 92c1ef5..82676c8 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java @@ -113,8 +113,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return targetOnlineFuture.thenCompose(targetOnline -> { CompletableFuture loadFuture = targetOnline ? CompletableFuture.completedFuture(null) - : CompletableFuture.runAsync(() -> - KitManager.get().loadPlayerDataFromDB(targetUuid)); + : CompletableFuture.runAsync(() -> KitManager.get().loadPlayerDataFromDB(targetUuid)); return loadFuture.thenRun(() -> Bukkit.getScheduler().runTask(plugin, () -> { Player currentSender = Bukkit.getPlayer(senderUuid); From c003acc91a315bae2d82b87cc4563af5fc47794e Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 14:20:17 -0400 Subject: [PATCH 17/28] Fix admin kit tab completion fallback behavior - Return `null` instead of an empty list when no completions are available - Restrict `/savepublickit` tab suggestions to the first argument only --- .../perplayerkit/commands/admin/PerPlayerKitCommand.java | 2 +- .../perplayerkit/commands/admin/SavePublicKitCommand.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java index 1931086..74a7223 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/PerPlayerKitCommand.java @@ -184,6 +184,6 @@ public List onTabComplete(@NotNull CommandSender sender, @NotNull Comman .collect(Collectors.toList()); } - return List.of(); + return null; } } diff --git a/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java index 6594208..e485ac9 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java @@ -92,6 +92,10 @@ 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) { + return KitManager.get().getPublicKitList().stream().map(kit -> kit.id).toList(); + } + + return null; } } From 0a509ab7e49e3b9e806ae96be2a3b2cdabe33a36 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Mon, 9 Mar 2026 14:21:06 -0400 Subject: [PATCH 18/28] Prioritize offline UUID fallback before Mojang lookup - Return offline-mode UUID immediately when server is in offline mode - Update test to assert Mojang lookup is never consulted in offline mode --- .../perplayerkit/commands/inspect/InspectCommandUtil.java | 8 ++++---- .../commands/inspect/InspectCommandUtilTest.java | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java index 6f864f7..e6eed0f 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java @@ -165,15 +165,15 @@ public static void showUsage(@NotNull Player player, @NotNull String commandName return cachedOfflinePlayer; } + if (!onlineMode) { + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)); + } + UUID mojangUuid = mojangLookup.get(); if (mojangUuid != null) { return mojangUuid; } - if (!onlineMode) { - return UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)); - } - return null; } } diff --git a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java index 4008b98..f936b95 100644 --- a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java +++ b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java @@ -42,7 +42,9 @@ void selectResolvedUuidUsesMojangResultWhenCacheMisses() { void selectResolvedUuidUsesOfflineFallbackWhenServerIsOfflineMode() { String identifier = "TargetPlayer"; - UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid(identifier, null, () -> null, false); + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid(identifier, null, () -> { + throw new RuntimeException("mojang should not be consulted"); + }, false); assertEquals(UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)), resolvedUuid); From d990732a7cf740221d623a428453f316c75488d8 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 13:59:30 -0400 Subject: [PATCH 19/28] Update notice script to derive copyright years from git - Compute per-file year or year range from git history and update existing headers - Resolve repo root automatically and process Java files from project root - Stop adding armor/offhand indicators in the load-kit GUI view --- .../java/dev/noah/perplayerkit/gui/GUI.java | 1 - tools/__pycache__/notice.cpython-311.pyc | Bin 0 -> 6187 bytes tools/notice.py | 121 +++++++++++++++--- 3 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 tools/__pycache__/notice.cpython-311.pyc diff --git a/src/main/java/dev/noah/perplayerkit/gui/GUI.java b/src/main/java/dev/noah/perplayerkit/gui/GUI.java index 9738705..e09c4db 100644 --- a/src/main/java/dev/noah/perplayerkit/gui/GUI.java +++ b/src/main/java/dev/noah/perplayerkit/gui/GUI.java @@ -326,7 +326,6 @@ public Menu ViewPublicKitMenu(Player p, String id) { menu.getSlot(i + 9).setItem(kit[i]); } - setArmorAndOffhandIndicators(menu); menu.getSlot(CLEAR_SLOT).setItem(createItem(Material.APPLE, 1, "LOAD KIT")); menu.getSlot(BACK_SLOT).setItem(createItem(Material.OAK_DOOR, 1, "BACK")); addPublicKitMenu(menu.getSlot(BACK_SLOT)); diff --git a/tools/__pycache__/notice.cpython-311.pyc b/tools/__pycache__/notice.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..883b0840005685c29f4363f049570ef67f7b8592 GIT binary patch literal 6187 zcmb_gYitzP6~41G`=0f#*Nb0um?*P38Zo zDav>@O_|`|B+>Kq6ip$4o?=W$q~^>;Ap!$Wmg3Vb6GFG)&X*~xV15Tp*#rwvyI=+C z5Ntr5f*mL;x&+4+gBu4$G*?yZmx7r-UA`xW=V=KDiPg7Yt+(OMm*+iAd10kC(Obzg zRRgW8=oM=!*9v<>jT^Vm`7IGX;-hq%z^kGfniu(h!7@7zIJg7c2|2QeLo>4~_x7U5 zV+YSQI%$ zo>pT#7JIlwIm!k3Fo#7Uq^LM_IjVw4mE*%ghm5&-SqM!pl7?tl5HT59ofSD%#PbRn zkvt5JT;Terr$sDtgJM|3yu^)0FH4~yHyjFzVMXM4m?CLWW<`O!yhwTxNvFtE#tT#7 zPRTF?uZHCCAx;bd3+5I?tiUt(QgNt)z(X8Nt&La7?2S3e5z@~GvtHyR*v?Yd?RKHO zSb+X z$)mum&dR_bE{J(iq4|guf~CS1V?L}dlIfov7&~zq`1@ZS8XlUMBr7;IG%+$TK5lHq zxiPMv8|@#P7&>ubxPOcry)ZUCSG`^x&i zH&`-EFdza?G~5r?fZRlOq;7gUSE|o|(fx`_07#MF9r_kP)9n&GC8==p9;7lLqEOUg z1^(xVe4|X8_9DzsXcFLW9L3OE^dyQ=32Ls?GgXpV>_h%zCknUET{ zV{yR`=BtQtx8Dy(6!WWcL;_8cCVY%e2V;U>733po6pI1Su}DZgEGkh+RS58M+!MDd%aYOo#|uZ;9$C~~Gol*6VnhyLSypikVS`&)0J4N~ zCcC+Iqq=FOCCk2~8CTZRm~GmXt**;@>vK+v*Kr>Kc}U2zW!duhQ5ABvd>qg0>eZab za)>h5mPyuKv#c(wIT~6YKYpCEAl7^B*j;DKy0azipF{p?}iUf__ooP5*)F zrh|slfJRcBa2tB^GPK@A32Lczv!#ux@EJH1rN;g#Pa zlz_1-okG$CTD!@a&6T7Hlwe9w0xd9tS+EG!glWbu*vRjW{TArl?ULRl>F^+Eu(v#t zT5^=IBE!9tcWQl^W10qSZlfwJ=6~6TplNcGRS1rEP=cN#hhmhJt61CoU%A64w^&M& zNifR)@thKj;A|)jpEScWyXo1IFefa9P{WikkE4X;LnOEo<`2=wG+1rb2qxwoH~CNK zv?R~OZGQi>EJ<=K&XO9Q=hb7ayb$;J{lupq%QyA3PC^&|gpbAqtuQ%WYzI;SWaKD` z6+L(p$W1f@it@&RB!Ei_o}Vt28~0iLcjJ4{bnW0c`V{#nOcV}m-qhZyGg2rl7AzeT z6?)?61z)WO<@rcod_S>t;v|fCBw*lkd#@x1c}eMmN|A90Ix2o3OGsnS=dV@auP6V0 z=Ib+G4gYNzT9zlSPhFjQ_tLdXC5{Ks0bycs-~JoQ6Uy(a(rG{>Oe|8T!*Wb#$e6lG zR&>U|CMy;tQo%&6F|lk+)C?01(;ct#QdBe$4iggqR0ChE8cXz9bvp@}@(>5mTYxX! z1rj0hSjfM@)uf-G)-o4WMUf)VzreuJe)UNl;ob2ScERjwZG?Q~cgyo6|`{kVG z93t@ARwmm_31$TsLEg4J^18+-IK0&KujlXX>s#N~m)`eEX5TB&lxELo*z-#RS*OR4 zx{bhZyf3U$D{V`!WuanP@j=Dvy>VsTx?8jE&bk{lTjMuX4Vu#j*XnEee~s}e#5TUN z@9sZ{zB<@Xz1l&4-A-r)$P>UdK>nvtxmf{h;Q9ef!STcU+a<6+jFP5=DOCy+pj)W` zOhbD)3xL?&_*H^8X&py<&<~Kp#7y~sfCLB>{Ky+{^*R-MVvefQ{_am9*cZb=2&uy} zV0#P%oMGJ@!y#1!aM%Y!>(-zgR>d$ld7`KanJK7PUcc%L&OkImz(oL?G&u3gz;gj^ z{K`Q_ z$#ZA;1>Q)YAs2z7Qy3GR#+^VQEyq6cKuEl;oQ)R*@DdhzVG#!5RGA~@2uWlQ7k5E= zRA!Dl%2~^|1Y;oCHwPihRpZSU@1SST~jt=)v4Ct18@S#_qI5#>uHZ*v8A}~BOG7uPpR|@#k;H++* z!E!XBn`fI~wXVPJ{RW-aEK*aM?-_tA*fBo)E-p zA&9l9t^BrJD}dEju16N+;!^gtBWd<Fn=`SXM zz?Ei)GVGAX4rN*I@~L}`2k$m^tT%RKS^w&Q#`^PDmaWNpYjRf9)OjB`ODkS^q^|3tf5&uN|3r0fgVY)vSEW-e=hq2T zi8v?G8>5%Nc_mFrnndyNJP7f&IaSK7Kv<~=TU;&fe0Ki54f;Z?UWpn>i%O!xLP=P* zW?U4ycKX_R!36G^j-NEle@qNU;ad}=-HTiynT8J?QW+(b;%A~6U#2Y52#v*6t@tj@%E=?Zc7fL$nZMpBDpx{B~Ml2g$Q;j;$BIZ$>4t4-+gR5Utb!1(czvEV3^dzfF~D4Hd2| zYSt>RoTa+Py5!F7Lv_1uyt;BG%k*X$%R9sO&2*jVAp!!Ob1X~k2IMG`mOa#g94e7v z%0xl%RU(c?>TnK~NRN$z11phw#IVcpb-F>L8?rkZKeB$}{=mIvx?}y!{ag1JraxQ% z;{K!iAGUP&`AqlublX^_Z7jWGJhNk5V<3NMzTwhZI?|rbjHh#HD9hAqaJ|sB=G%e} N07*. */""" -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) From 97f116b13c59634850bc2a2aecb8c5e339a590bf Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 14:02:52 -0400 Subject: [PATCH 20/28] Always fall back to offline UUID when inspect lookup misses - Resolve identifiers to a UUID in all cases by using OfflinePlayer fallback after Mojang miss - Remove `onlineMode` branching from `selectResolvedUuid` - Update tests to cover Mojang-miss fallback behavior and non-null resolution --- .../commands/inspect/InspectCommandUtil.java | 17 +++++++---------- .../inspect/InspectCommandUtilTest.java | 17 ++++------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java index e6eed0f..2ace0ff 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java @@ -50,10 +50,11 @@ private InspectCommandUtil() { /** * 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. + * and finally searches cached offline players and Mojang asynchronously before + * falling back to the deterministic offline-mode UUID. * * @param identifier Player name or UUID string - * @return CompletableFuture containing UUID if found, null otherwise + * @return CompletableFuture containing the resolved UUID */ public static CompletableFuture resolvePlayerIdentifierAsync(String identifier) { // First try to parse as UUID @@ -73,7 +74,7 @@ public static CompletableFuture resolvePlayerIdentifierAsync(String identi return CompletableFuture.supplyAsync(() -> { UUID cachedOfflinePlayer = findCachedOfflinePlayerUuid(identifier); return selectResolvedUuid(identifier, cachedOfflinePlayer, - () -> lookupPlayerUuidFromMojang(identifier), Bukkit.getOnlineMode()); + () -> lookupPlayerUuidFromMojang(identifier)); }); } @@ -159,21 +160,17 @@ public static void showUsage(@NotNull Player player, @NotNull String commandName } } - static @Nullable UUID selectResolvedUuid(@NotNull String identifier, @Nullable UUID cachedOfflinePlayer, - @NotNull Supplier mojangLookup, boolean onlineMode) { + static @NotNull UUID selectResolvedUuid(@NotNull String identifier, @Nullable UUID cachedOfflinePlayer, + @NotNull Supplier mojangLookup) { if (cachedOfflinePlayer != null) { return cachedOfflinePlayer; } - if (!onlineMode) { - return UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)); - } - UUID mojangUuid = mojangLookup.get(); if (mojangUuid != null) { return mojangUuid; } - return null; + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)); } } diff --git a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java index f936b95..d3fe865 100644 --- a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java +++ b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java @@ -24,7 +24,7 @@ void selectResolvedUuidPrefersCachedOfflinePlayer() { UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", cachedUuid, () -> { throw new AssertionError("Mojang lookup should not run when cached player exists"); - }, false); + }); assertEquals(cachedUuid, resolvedUuid); } @@ -33,30 +33,21 @@ void selectResolvedUuidPrefersCachedOfflinePlayer() { void selectResolvedUuidUsesMojangResultWhenCacheMisses() { UUID mojangUuid = UUID.randomUUID(); - UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", null, () -> mojangUuid, true); + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", null, () -> mojangUuid); assertEquals(mojangUuid, resolvedUuid); } @Test - void selectResolvedUuidUsesOfflineFallbackWhenServerIsOfflineMode() { + void selectResolvedUuidUsesOfflineFallbackWhenMojangLookupMisses() { String identifier = "TargetPlayer"; - UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid(identifier, null, () -> { - throw new RuntimeException("mojang should not be consulted"); - }, false); + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid(identifier, null, () -> null); assertEquals(UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)), resolvedUuid); } - @Test - void selectResolvedUuidReturnsNullWhenServerIsOnlineModeAndNoMatchExists() { - UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", null, () -> null, true); - - assertNull(resolvedUuid); - } - @Test void findCachedOfflinePlayerUuidDoesNotScanOfflinePlayers() throws Exception { Method method = InspectCommandUtil.class.getDeclaredMethod("findCachedOfflinePlayerUuid", String.class); From 5b8a1d7c3063bcf22182faefa3eeaf9846ec29a9 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 14:03:58 -0400 Subject: [PATCH 21/28] Return null when no public kit IDs for tab completion - Store public kit IDs before returning tab-complete results - Return `null` instead of an empty list when no public kits exist --- .../noah/perplayerkit/commands/admin/SavePublicKitCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java index e485ac9..5c7cbd3 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/admin/SavePublicKitCommand.java @@ -93,7 +93,8 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command @Override public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (args.length == 1) { - return KitManager.get().getPublicKitList().stream().map(kit -> kit.id).toList(); + List ids = KitManager.get().getPublicKitList().stream().map(kit -> kit.id).toList(); + return ids.isEmpty() ? null : ids; } return null; From 56e9387915cc3ca9aa46dd231b1da96669652bb4 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 14:06:09 -0400 Subject: [PATCH 22/28] Use ConcurrentHashMap for kit storage in KitManager - Replace `HashMap` with `ConcurrentHashMap` for `kitByKitIDMap` - Generalize field type to `Map` for cleaner abstraction --- src/main/java/dev/noah/perplayerkit/KitManager.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/KitManager.java b/src/main/java/dev/noah/perplayerkit/KitManager.java index 745afe3..175fe90 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; } @@ -544,4 +545,4 @@ private void applyKitLoadEffects(Player player, boolean isEnderChest) { } } } -} \ No newline at end of file +} From ba275196e8f8952fe262d42ea737bd729c4dc3da Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 14:41:55 -0400 Subject: [PATCH 23/28] Use dedicated slot constant for public kit loading - Add `LOAD_PUBLIC_KIT_SLOT` in layout constants - Wire LOAD KIT button and handler to the new slot constant in `GUI` --- src/main/java/dev/noah/perplayerkit/gui/GUI.java | 4 ++-- src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/gui/GUI.java b/src/main/java/dev/noah/perplayerkit/gui/GUI.java index e09c4db..cdadc09 100644 --- a/src/main/java/dev/noah/perplayerkit/gui/GUI.java +++ b/src/main/java/dev/noah/perplayerkit/gui/GUI.java @@ -326,10 +326,10 @@ public Menu ViewPublicKitMenu(Player p, String id) { menu.getSlot(i + 9).setItem(kit[i]); } - menu.getSlot(CLEAR_SLOT).setItem(createItem(Material.APPLE, 1, "LOAD KIT")); + 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(CLEAR_SLOT), id); + addLoadPublicKit(menu.getSlot(LOAD_PUBLIC_KIT_SLOT), id); menu.open(p); diff --git a/src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java b/src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java index c6e013b..e225370 100644 --- a/src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java +++ b/src/main/java/dev/noah/perplayerkit/gui/GuiLayoutUtils.java @@ -34,6 +34,7 @@ public final class GuiLayoutUtils { 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; From b8ad55e83132f0a6bdfe570ae8f80c52bd3979d1 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 15:00:53 -0400 Subject: [PATCH 24/28] Use command permissions for inspect and show armor indicators - Check inspect command access via the command's configured permission - Apply armor/offhand indicator setup when opening the public kit load menu --- .../commands/inspect/AbstractInspectCommand.java | 9 +++++++-- src/main/java/dev/noah/perplayerkit/gui/GUI.java | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java index 82676c8..b43ae99 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java @@ -74,7 +74,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - if (!player.hasPermission("perplayerkit.inspect")) { + if (!hasCommandPermission(player, command)) { BroadcastManager.get().sendComponentMessage(player, ERROR_PREFIX.append( mm.deserialize("You don't have permission to use this command."))); @@ -150,7 +150,7 @@ 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) || !hasCommandPermission(sender, command)) { return List.of(); } @@ -180,6 +180,11 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return new ArrayList<>(); } + private boolean hasCommandPermission(@NotNull CommandSender sender, @NotNull Command command) { + String permission = command.getPermission(); + return permission == null || permission.isBlank() || sender.hasPermission(permission); + } + private int parseSlot(String slotArg, Player player) { try { int slot = Integer.parseInt(slotArg); diff --git a/src/main/java/dev/noah/perplayerkit/gui/GUI.java b/src/main/java/dev/noah/perplayerkit/gui/GUI.java index cdadc09..52a5afe 100644 --- a/src/main/java/dev/noah/perplayerkit/gui/GUI.java +++ b/src/main/java/dev/noah/perplayerkit/gui/GUI.java @@ -326,6 +326,7 @@ public Menu ViewPublicKitMenu(Player p, String id) { menu.getSlot(i + 9).setItem(kit[i]); } + 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)); From a28c846cdce93ab75879601d3ac256cc44affac4 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 15:36:24 -0400 Subject: [PATCH 25/28] Stop inspect offline UUID fallback and centralize kit caching - Add `cacheKit` helper in `KitManager` and route cache writes through it - Remove deterministic offline-mode UUID fallback in inspect resolution - Update inspect tests to expect `null` when all UUID lookups miss --- .../dev/noah/perplayerkit/KitManager.java | 29 +++++++++++++------ .../commands/inspect/InspectCommandUtil.java | 15 +++++----- .../inspect/InspectCommandUtilTest.java | 10 ++----- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/KitManager.java b/src/main/java/dev/noah/perplayerkit/KitManager.java index 175fe90..7fa8296 100644 --- a/src/main/java/dev/noah/perplayerkit/KitManager.java +++ b/src/main/java/dev/noah/perplayerkit/KitManager.java @@ -59,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; } @@ -105,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)); @@ -150,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)); @@ -193,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; @@ -213,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; @@ -238,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; } @@ -270,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 { @@ -437,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(Serializer.itemStackArrayFromBase64(data))); } catch (IOException ignored) { } } @@ -447,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(Serializer.itemStackArrayFromBase64(data))); } catch (IOException ignored) { } } @@ -487,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); } diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java index 2ace0ff..b801904 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtil.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; @@ -50,13 +49,13 @@ private InspectCommandUtil() { /** * 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 before - * falling back to the deterministic offline-mode UUID. + * and finally searches cached offline players and Mojang asynchronously. * * @param identifier Player name or UUID string - * @return CompletableFuture containing the resolved UUID + * @return CompletableFuture containing the resolved UUID, or null when the player + * could not be identified from local cache or Mojang */ - public static CompletableFuture resolvePlayerIdentifierAsync(String identifier) { + public static CompletableFuture<@Nullable UUID> resolvePlayerIdentifierAsync(String identifier) { // First try to parse as UUID try { UUID uuid = UUID.fromString(identifier); @@ -160,8 +159,8 @@ public static void showUsage(@NotNull Player player, @NotNull String commandName } } - static @NotNull UUID selectResolvedUuid(@NotNull String identifier, @Nullable UUID cachedOfflinePlayer, - @NotNull Supplier mojangLookup) { + static @Nullable UUID selectResolvedUuid(@NotNull String identifier, @Nullable UUID cachedOfflinePlayer, + @NotNull Supplier mojangLookup) { if (cachedOfflinePlayer != null) { return cachedOfflinePlayer; } @@ -171,6 +170,6 @@ public static void showUsage(@NotNull Player player, @NotNull String commandName return mojangUuid; } - return UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)); + return null; } } diff --git a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java index d3fe865..ae66883 100644 --- a/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java +++ b/src/test/java/dev/noah/perplayerkit/commands/inspect/InspectCommandUtilTest.java @@ -6,7 +6,6 @@ import org.mockito.MockedStatic; import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -39,13 +38,10 @@ void selectResolvedUuidUsesMojangResultWhenCacheMisses() { } @Test - void selectResolvedUuidUsesOfflineFallbackWhenMojangLookupMisses() { - String identifier = "TargetPlayer"; + void selectResolvedUuidReturnsNullWhenAllLookupsMiss() { + UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid("TargetPlayer", null, () -> null); - UUID resolvedUuid = InspectCommandUtil.selectResolvedUuid(identifier, null, () -> null); - - assertEquals(UUID.nameUUIDFromBytes(("OfflinePlayer:" + identifier).getBytes(StandardCharsets.UTF_8)), - resolvedUuid); + assertNull(resolvedUuid); } @Test From 3da6daa1626869ae13afcbf2abd643977aee8585 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 15:48:27 -0400 Subject: [PATCH 26/28] Remove redundant inspect permission checks - Let command execution and tab completion proceed without local permission gating - Delete unused `hasCommandPermission` helper in `AbstractInspectCommand` --- .../commands/inspect/AbstractInspectCommand.java | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java index b43ae99..3b320df 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/inspect/AbstractInspectCommand.java @@ -74,14 +74,6 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - if (!hasCommandPermission(player, command)) { - 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, usageCommand()); return true; @@ -150,7 +142,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player) || !hasCommandPermission(sender, command)) { + if (!(sender instanceof Player)) { return List.of(); } @@ -180,11 +172,6 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return new ArrayList<>(); } - private boolean hasCommandPermission(@NotNull CommandSender sender, @NotNull Command command) { - String permission = command.getPermission(); - return permission == null || permission.isBlank() || sender.hasPermission(permission); - } - private int parseSlot(String slotArg, Player player) { try { int slot = Integer.parseInt(slotArg); From 68a332849bb3f8f36fc5502ea62729df0a26a6a9 Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 17:34:25 -0400 Subject: [PATCH 27/28] Reuse deserialized kit when caching - Avoid deserializing Base64 item arrays twice in kit and EC cache paths --- src/main/java/dev/noah/perplayerkit/KitManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/KitManager.java b/src/main/java/dev/noah/perplayerkit/KitManager.java index 7fa8296..85e58aa 100644 --- a/src/main/java/dev/noah/perplayerkit/KitManager.java +++ b/src/main/java/dev/noah/perplayerkit/KitManager.java @@ -447,7 +447,7 @@ public void loadPlayerDataFromDB(UUID uuid) { try { ItemStack[] kit = Serializer.itemStackArrayFromBase64(data); cacheKit(IDUtil.getPlayerKitId(uuid, slot), - ItemFilter.get().filterItemStack(Serializer.itemStackArrayFromBase64(data))); + ItemFilter.get().filterItemStack(kit)); } catch (IOException ignored) { } } @@ -458,7 +458,7 @@ public void loadPlayerDataFromDB(UUID uuid) { try { ItemStack[] kit = Serializer.itemStackArrayFromBase64(data); cacheKit(IDUtil.getECId(uuid, slot), - ItemFilter.get().filterItemStack(Serializer.itemStackArrayFromBase64(data))); + ItemFilter.get().filterItemStack(kit)); } catch (IOException ignored) { } } From 1f9bd0dad9b4769195090555dfacfb4968d805dc Mon Sep 17 00:00:00 2001 From: Noah Ross Date: Tue, 10 Mar 2026 17:49:29 -0400 Subject: [PATCH 28/28] Validate delete/swap kit slots are within 1-9 - Use `parseSlotInRange(..., 1, 9)` in `/deletekit` and `/swapkit` - Reject out-of-range slot arguments before command execution --- .../dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java | 2 +- .../dev/noah/perplayerkit/commands/kits/SwapKitCommand.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java index e969c4c..6e9a33c 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/DeleteKitCommand.java @@ -46,7 +46,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - Integer slot = SlotArgumentParser.parseSlot(args[0]); + Integer slot = SlotArgumentParser.parseSlotInRange(args[0], 1, 9); KitManager kitManager = KitManager.get(); if (slot == null) { player.sendMessage(ChatColor.RED + "Usage: /deletekit "); diff --git a/src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java b/src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java index 8cd2fcf..3fc02da 100644 --- a/src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java +++ b/src/main/java/dev/noah/perplayerkit/commands/kits/SwapKitCommand.java @@ -46,8 +46,8 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - Integer slot1 = SlotArgumentParser.parseSlot(args[0]); - Integer slot2 = SlotArgumentParser.parseSlot(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 ");