diff --git a/src/main/java/me/trouper/alias/AliasContext.java b/src/main/java/me/trouper/alias/AliasContext.java index b73deb9..1a586dd 100644 --- a/src/main/java/me/trouper/alias/AliasContext.java +++ b/src/main/java/me/trouper/alias/AliasContext.java @@ -4,10 +4,7 @@ import me.trouper.alias.data.Common; import me.trouper.alias.data.DataManager; import me.trouper.alias.data.JsonSerializable; import me.trouper.alias.server.AutoRegistrar; -import me.trouper.alias.server.events.listeners.FreezeListener; -import me.trouper.alias.server.events.listeners.GuiListener; -import me.trouper.alias.server.events.listeners.SpawnListener; -import me.trouper.alias.server.events.listeners.WandListener; +import me.trouper.alias.server.events.listeners.*; import me.trouper.alias.server.systems.TaskManager; import me.trouper.alias.server.systems.Text; import me.trouper.alias.server.systems.Verbose; @@ -30,6 +27,7 @@ public class AliasContext { private final Verbose verbose; private final DisplayManager displayManager; private final FreezeManager freezeManager; + private final GuiInputListener guiInputListener; private boolean enabled = false; public AliasContext(JavaPlugin plugin, Common common) { @@ -42,6 +40,7 @@ public class AliasContext { this.verbose = new Verbose(this); this.displayManager = new DisplayManager(this); this.freezeManager = new FreezeManager(this); + this.guiInputListener = new GuiInputListener(this); } /** @@ -58,10 +57,11 @@ public class AliasContext { autoUpdater.checkUpdate(); autoRegistrar.loadAll(common.getPackageName()); - Bukkit.getPluginManager().registerEvents(new GuiListener(),getPlugin()); Bukkit.getPluginManager().registerEvents(new SpawnListener(this),getPlugin()); Bukkit.getPluginManager().registerEvents(new WandListener(this),getPlugin()); Bukkit.getPluginManager().registerEvents(new FreezeListener(this),getPlugin()); + Bukkit.getPluginManager().registerEvents(guiInputListener,getPlugin()); + guiInputListener.startTimeoutTask(); List> copy = new ArrayList<>(autoRegistrar.getSerializables()); for (JsonSerializable serializable : copy) { dataManager.load(serializable.getClass()); @@ -83,10 +83,12 @@ public class AliasContext { autoRegistrar.getSerializables().forEach(jsonSerializable -> { dataManager.save(jsonSerializable.getClass()); }); + guiInputListener.shutdown(); autoRegistrar.unregisterAll(); autoUpdater.checkUpdate(); + enabled = false; plugin.getLogger().info("Alias context shutdown complete"); } @@ -102,4 +104,5 @@ public class AliasContext { public DataManager getDataManager() { return dataManager; } public AutoUpdater getAutoUpdater() { return autoUpdater; } public FreezeManager getFreezeManager() { return freezeManager; } + public GuiInputListener getGuiInputListener() { return guiInputListener; } } \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/data/Common.java b/src/main/java/me/trouper/alias/data/Common.java index 2373229..6cd9b14 100644 --- a/src/main/java/me/trouper/alias/data/Common.java +++ b/src/main/java/me/trouper/alias/data/Common.java @@ -27,6 +27,17 @@ public class Common { this.debuggerExclusions = new HashSet<>(); } + public void update(Common common) { + this.mainColor = common.getMainColor(); + this.secondaryColor = common.getSecondaryColor(); + this.pluginName = common.getPluginName(); + this.flatPrefix = common.getFlatPrefix(); + this.flat = common.isFlat(); + this.debugMode = common.getDebugMode(); + this.debuggerExclusions.clear(); + this.debuggerExclusions.addAll(common.getDebuggerExclusions()); + } + public String getPackageName() { return packageName; } @@ -91,6 +102,11 @@ public class Common { return this.debuggerExclusions.remove(methodName); } + public void setDebuggerExclusions(Set debuggerExclusions) { + this.debuggerExclusions.clear(); + this.debuggerExclusions.addAll(debuggerExclusions); + } + public String getTempTag() { return "$/" + pluginName + "/ TEMP"; } @@ -99,4 +115,8 @@ public class Common { public String getUpdateURL() { return updateURL; } + + public boolean isFlat() { + return flat; + } } diff --git a/src/main/java/me/trouper/alias/data/DebugConfig.java b/src/main/java/me/trouper/alias/data/DebugConfig.java new file mode 100644 index 0000000..962131c --- /dev/null +++ b/src/main/java/me/trouper/alias/data/DebugConfig.java @@ -0,0 +1,14 @@ +package me.trouper.alias.data; + +import java.util.ArrayList; +import java.util.List; + +public class DebugConfig { + public boolean debugMode = false; + public List debuggerExclusions = new ArrayList<>(); + + DebugConfig() { + this.debugMode = false; + debuggerExclusions = new ArrayList<>(); + } +} diff --git a/src/main/java/me/trouper/alias/server/commands/QuickCommand.java b/src/main/java/me/trouper/alias/server/commands/QuickCommand.java index cc8e5a8..96bfb46 100644 --- a/src/main/java/me/trouper/alias/server/commands/QuickCommand.java +++ b/src/main/java/me/trouper/alias/server/commands/QuickCommand.java @@ -117,4 +117,23 @@ public interface QuickCommand extends TabExecutor, ContextAware { command.setTabCompleter((sender, command2, label, args) -> List.of()); } } + + default CompletionBuilder quickDebugArgs(CompletionBuilder b, List activeExclusions) { + return b.then( + b.arg("debug") + .then( + b.arg("toggle") + ) + .then( + b.arg("exclude") + .then( + b.arg("Class.method"))) + .then( + b.arg("include") + .then( + b.arg(activeExclusions) + ) + ) + ); + } } diff --git a/src/main/java/me/trouper/alias/server/events/custom/PlayerCreateVehicleEvent.java b/src/main/java/me/trouper/alias/server/events/custom/PlayerCreateVehicleEvent.java new file mode 100644 index 0000000..1146734 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/events/custom/PlayerCreateVehicleEvent.java @@ -0,0 +1,46 @@ +package me.trouper.alias.server.events.custom; + +import org.bukkit.entity.Player; +import org.bukkit.entity.Vehicle; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +public class PlayerCreateVehicleEvent extends Event implements Cancellable { + private static final HandlerList HANDLERS = new HandlerList(); + private final Player player; + private final Vehicle vehicle; + private boolean cancelled; + + public PlayerCreateVehicleEvent(Player player, Vehicle vehicle) { + this.player = player; + this.vehicle = vehicle; + } + + public Player getPlayer() { + return player; + } + + public Vehicle getVehicle() { + return vehicle; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/src/main/java/me/trouper/alias/server/events/listeners/GuiInputListener.java b/src/main/java/me/trouper/alias/server/events/listeners/GuiInputListener.java new file mode 100644 index 0000000..364abba --- /dev/null +++ b/src/main/java/me/trouper/alias/server/events/listeners/GuiInputListener.java @@ -0,0 +1,165 @@ +package me.trouper.alias.server.events.listeners; + +import me.trouper.alias.AliasContext; +import me.trouper.alias.server.systems.gui.QuickGui; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class GuiInputListener implements Listener { + + private final Map waitingPlayers = new ConcurrentHashMap<>(); + private BukkitRunnable timeoutTask; + private final AliasContext context; + + public GuiInputListener(AliasContext context) { + this.context = context; + } + + public void registerWaitingPlayer(Player player, QuickGui gui) { + waitingPlayers.put(player, gui); + } + + + public void unregisterWaitingPlayer(Player player) { + waitingPlayers.remove(player); + } + + public boolean isWaitingForInput(Player player) { + return waitingPlayers.containsKey(player); + } + + public QuickGui getWaitingGui(Player player) { + return waitingPlayers.get(player); + } + + public boolean handleInput(Player player, String input, QuickGui.InputSource source) { + QuickGui gui = waitingPlayers.get(player); + if (gui != null) { + boolean handled = gui.handleInput(player, input, source); + if (handled) { + waitingPlayers.remove(player); + } + return handled; + } + return false; + } + + public void cancelInput(Player player) { + QuickGui gui = waitingPlayers.get(player); + if (gui != null) { + gui.cancelInput(player); + waitingPlayers.remove(player); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + QuickGui gui = waitingPlayers.get(player); + + if (gui != null) { + event.setCancelled(true); + + context.getPlugin().getServer().getScheduler().runTask(context.getPlugin(), () -> { + boolean handled = gui.handleInput(player, event.getMessage(), QuickGui.InputSource.CHAT); + if (handled) { + waitingPlayers.remove(player); + } + }); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerCommand(PlayerCommandPreprocessEvent event) { + Player player = event.getPlayer(); + QuickGui gui = waitingPlayers.get(player); + + if (gui != null) { + String command = event.getMessage(); + + if (command.equalsIgnoreCase("/cancel") || command.equalsIgnoreCase("/c")) { + event.setCancelled(true); + gui.cancelInput(player); + waitingPlayers.remove(player); + return; + } + + event.setCancelled(true); + boolean handled = gui.handleInput(player, command, QuickGui.InputSource.COMMAND); + if (handled) { + waitingPlayers.remove(player); + } + } + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + QuickGui gui = waitingPlayers.remove(player); + if (gui != null) { + gui.cancelInput(player); + } + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onInventoryClick(InventoryClickEvent event) { + QuickGui.handleClick(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onInventoryClose(InventoryCloseEvent event) { + QuickGui.handleClose(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onInventoryDrag(InventoryDragEvent event) { + QuickGui.handleDrag(event); + } + + public void startTimeoutTask() { + timeoutTask = new BukkitRunnable() { + @Override + public void run() { + for (Map.Entry entry : waitingPlayers.entrySet()) { + entry.getValue().cleanupExpiredTimeouts(); + } + } + }; + timeoutTask.runTaskTimer(context.getPlugin(), 20L, 20L); + } + + public void shutdown() { + if (timeoutTask != null) { + timeoutTask.cancel(); + } + waitingPlayers.clear(); + } + + public static void sendInputInstructions(Player player, String prompt) { + sendInputInstructions(player, prompt, true); + } + + public static void sendInputInstructions(Player player, String prompt, boolean showCancelOption) { + player.sendMessage(Component.text("").append(Component.text("▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬", NamedTextColor.GRAY))); + player.sendMessage(MiniMessage.miniMessage().deserialize(prompt)); + if (showCancelOption) { + player.sendMessage(Component.text("Type '/cancel' to cancel input.", NamedTextColor.GRAY)); + } + player.sendMessage(Component.text("▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬", NamedTextColor.GRAY)); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/events/listeners/GuiListener.java b/src/main/java/me/trouper/alias/server/events/listeners/GuiListener.java deleted file mode 100644 index fc7b14c..0000000 --- a/src/main/java/me/trouper/alias/server/events/listeners/GuiListener.java +++ /dev/null @@ -1,37 +0,0 @@ -package me.trouper.alias.server.events.listeners; - -import me.trouper.alias.server.events.QuickListener; -import me.trouper.alias.server.systems.gui.QuickGui; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.plugin.java.JavaPlugin; - -public class GuiListener implements Listener { - - @EventHandler(priority = EventPriority.NORMAL) - public void onInventoryClick(InventoryClickEvent event) { - QuickGui.handleClick(event); - } - - @EventHandler(priority = EventPriority.NORMAL) - public void onInventoryClose(InventoryCloseEvent event) { - QuickGui.handleClose(event); - } - - @EventHandler(priority = EventPriority.NORMAL) - public void onInventoryDrag(InventoryDragEvent event) { - QuickGui.handleDrag(event); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerQuit(PlayerQuitEvent event) { - QuickGui.getRegistries().values().forEach(gui -> { - gui.getViewers().remove(event.getPlayer()); - }); - } -} diff --git a/src/main/java/me/trouper/alias/server/events/listeners/SpawnListener.java b/src/main/java/me/trouper/alias/server/events/listeners/SpawnListener.java index bb4ff42..00a4307 100644 --- a/src/main/java/me/trouper/alias/server/events/listeners/SpawnListener.java +++ b/src/main/java/me/trouper/alias/server/events/listeners/SpawnListener.java @@ -1,18 +1,21 @@ package me.trouper.alias.server.events.listeners; import me.trouper.alias.AliasContext; +import me.trouper.alias.server.events.custom.PlayerCreateVehicleEvent; import me.trouper.alias.server.events.custom.PlayerSpawnEntityEvent; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.EnderPearl; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; +import org.bukkit.entity.Vehicle; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.entity.ProjectileLaunchEvent; +import org.bukkit.event.vehicle.VehicleCreateEvent; import java.util.Iterator; import java.util.Map; @@ -93,6 +96,31 @@ public class SpawnListener implements Listener { } } + @EventHandler + public void onVehicleCreate(VehicleCreateEvent e) { + Vehicle vehicle = e.getVehicle(); + + Location loc = vehicle.getLocation(); + long now = System.currentTimeMillis(); + Player creator = null; + + for (Placed p : recentBlocks) { + if (p.time > now - 2000 && p.loc.getWorld().equals(loc.getWorld()) + && p.loc.distanceSquared(loc) < 4) { + creator = Bukkit.getPlayer(p.playerId); + break; + } + } + + if (creator == null) return; + + PlayerCreateVehicleEvent customEvent = new PlayerCreateVehicleEvent(creator, vehicle); + Bukkit.getPluginManager().callEvent(customEvent); + if (customEvent.isCancelled()) { + vehicle.remove(); + } + } + private static class Placed { final UUID playerId; final Location loc; diff --git a/src/main/java/me/trouper/alias/server/systems/Text.java b/src/main/java/me/trouper/alias/server/systems/Text.java index 6ae0579..b234635 100644 --- a/src/main/java/me/trouper/alias/server/systems/Text.java +++ b/src/main/java/me/trouper/alias/server/systems/Text.java @@ -1,6 +1,7 @@ package me.trouper.alias.server.systems; import me.trouper.alias.AliasContext; +import me.trouper.alias.utils.FormatUtils; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; @@ -23,6 +24,7 @@ public class Text { public Text(AliasContext context) { this.context = context; } + /** * Messages an audience applying pallet formatting to the text and placeholders. Placeholders are zero-indexed and curly braced. {0}, {1}, {2}... * Supports both flat messages and fancy wrapped messages based on Alias configuration. @@ -30,7 +32,7 @@ public class Text { * @param playSound If the pallet's sound should be played. * @param audience Any audience. * @param text The message to format - * @param args Qualified placeholders to color. + * @param args Qualified placeholders to color. Will format enums and components properly. */ public void messageAny(Pallet pallet, boolean playSound, Audience audience, String text, Object... args) { message( @@ -39,7 +41,16 @@ public class Text { audience, color(text), Arrays.stream(args) - .map(object -> object instanceof ComponentLike ? (ComponentLike) object : Component.text(String.valueOf(object))) + .map(object -> { + if (object instanceof ComponentLike) { + return (ComponentLike) object; + } else if (object instanceof Enum) { + String formatted = FormatUtils.formatEnum((Enum) object); + return Component.text(formatted); + } else { + return Component.text(String.valueOf(object)); + } + }) .toArray(ComponentLike[]::new) ); } @@ -50,7 +61,7 @@ public class Text { * @param pallet The colors to use for text and arguments. * @param audience Any audience. * @param text The message to format - * @param args Qualified placeholders to color. + * @param args Qualified placeholders to color. Will format enums and components properly. */ public void messageAny(Pallet pallet, Audience audience, String text, Object... args) { messageAny(pallet,true,audience,text,args); @@ -97,7 +108,16 @@ public class Text { pallet, color(text), Arrays.stream(args) - .map(object -> object instanceof ComponentLike ? (ComponentLike) object : Component.text(String.valueOf(object))) + .map(object -> { + if (object instanceof ComponentLike) { + return (ComponentLike) object; + } else if (object instanceof Enum) { + String formatted = FormatUtils.formatEnum((Enum) object); + return Component.text(formatted); + } else { + return Component.text(String.valueOf(object)); + } + }) .toArray(ComponentLike[]::new) ); } diff --git a/src/main/java/me/trouper/alias/server/systems/gui/QuickGui.java b/src/main/java/me/trouper/alias/server/systems/gui/QuickGui.java index 691fd75..9704255 100644 --- a/src/main/java/me/trouper/alias/server/systems/gui/QuickGui.java +++ b/src/main/java/me/trouper/alias/server/systems/gui/QuickGui.java @@ -23,7 +23,6 @@ import java.util.function.Consumer; public class QuickGui implements InventoryHolder { - private static final Map registry = new ConcurrentHashMap<>(); private static final MiniMessage miniMessage = MiniMessage.miniMessage(); private final Map slotActions; @@ -42,10 +41,16 @@ public class QuickGui implements InventoryHolder { private Inventory inventory; private final Set viewers; + private final Map callbacks; + private final Map waitingForInput; + private final Map inputTimeouts; + private final long defaultTimeout; + private QuickGui(Component title, int size, GuiAction globalAction, Map slotActions, Map slotItems, GuiCreateAction createAction, GuiCloseAction closeAction, GuiDragAction dragAction, - boolean preventDrag, Sound clickSound, float soundVolume, float soundPitch) { + boolean preventDrag, Sound clickSound, float soundVolume, float soundPitch, + Map callbacks, long defaultTimeout) { this.title = title; this.size = size; this.globalAction = globalAction; @@ -59,19 +64,10 @@ public class QuickGui implements InventoryHolder { this.soundVolume = soundVolume; this.soundPitch = soundPitch; this.viewers = ConcurrentHashMap.newKeySet(); - } - - public static QuickGui register(String id, QuickGui gui) { - registry.put(id, gui); - return gui; - } - - public static Optional getRegistered(String id) { - return Optional.ofNullable(registry.get(id)); - } - - public static Map getRegistries() { - return new HashMap<>(registry); + this.callbacks = new HashMap<>(callbacks); + this.waitingForInput = new ConcurrentHashMap<>(); + this.inputTimeouts = new ConcurrentHashMap<>(); + this.defaultTimeout = defaultTimeout; } public static void handleClick(InventoryClickEvent event) { @@ -136,6 +132,92 @@ public class QuickGui implements InventoryHolder { } } + public void requestInput(Player player, String callbackId) { + requestInput(player, callbackId, defaultTimeout); + } + + public void requestInput(Player player, String callbackId, long timeoutMs) { + if (!callbacks.containsKey(callbackId)) { + throw new IllegalArgumentException("Callback with ID '" + callbackId + "' not found"); + } + + waitingForInput.put(player, callbackId); + inputTimeouts.put(player, System.currentTimeMillis() + timeoutMs); + + player.closeInventory(); + } + + public boolean handleInput(Player player, String input, InputSource source) { + String callbackId = waitingForInput.get(player); + if (callbackId == null) { + return false; + } + + Long timeout = inputTimeouts.get(player); + if (timeout != null && System.currentTimeMillis() > timeout) { + waitingForInput.remove(player); + inputTimeouts.remove(player); + callbacks.get(callbackId).onTimeout(this, player); + return false; + } + + waitingForInput.remove(player); + inputTimeouts.remove(player); + + GuiCallback callback = callbacks.get(callbackId); + if (callback != null) { + callback.onInput(this, player, input, source); + return true; + } + + return false; + } + + public void cancelInput(Player player) { + String callbackId = waitingForInput.remove(player); + inputTimeouts.remove(player); + + if (callbackId != null) { + GuiCallback callback = callbacks.get(callbackId); + if (callback != null) { + callback.onCancel(this, player); + } + } + } + + public boolean isWaitingForInput(Player player) { + return waitingForInput.containsKey(player); + } + + public String getWaitingCallbackId(Player player) { + return waitingForInput.get(player); + } + + public Set getPlayersWaitingForInput() { + return new HashSet<>(waitingForInput.keySet()); + } + + public void cleanupExpiredTimeouts() { + long currentTime = System.currentTimeMillis(); + Iterator> iterator = inputTimeouts.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (currentTime > entry.getValue()) { + Player player = entry.getKey(); + String callbackId = waitingForInput.remove(player); + iterator.remove(); + + if (callbackId != null) { + GuiCallback callback = callbacks.get(callbackId); + if (callback != null) { + callback.onTimeout(this, player); + } + } + } + } + } + private int calculateSize() { if (size > 0 && size % 9 == 0) { return Math.min(size, 54); @@ -220,6 +302,8 @@ public class QuickGui implements InventoryHolder { private Sound clickSound = Sound.UI_BUTTON_CLICK; private float soundVolume = 0.5f; private float soundPitch = 1.0f; + private final Map callbacks = new HashMap<>(); + private long defaultTimeout = 30000; // 30 seconds default public GuiBuilder title(String title) { this.title = Component.text(title); @@ -288,6 +372,18 @@ public class QuickGui implements InventoryHolder { return this; } + public GuiBuilder defaultTimeout(long timeoutMs) { + this.defaultTimeout = timeoutMs; + return this; + } + + public GuiBuilder callback(String id, GuiCallback callback) { + if (id != null && callback != null) { + this.callbacks.put(id, callback); + } + return this; + } + public GuiBuilder item(int slot, ItemStack item) { return item(slot, item, null); } @@ -330,12 +426,11 @@ public class QuickGui implements InventoryHolder { return item(slot, item, action); } - public GuiBuilder fillSlots(ItemStack item, GuiAction action, int... slots) { for (int slot : slots) { if (slot >= 0 && slot < 54 && item != null) { - slotItems.put(slot,item); - slotActions.put(slot,action); + slotItems.put(slot, item); + slotActions.put(slot, action); } } return this; @@ -382,12 +477,7 @@ public class QuickGui implements InventoryHolder { Component finalTitle = title != null ? title : Component.text("Untitled GUI"); return new QuickGui(finalTitle, size, globalAction, slotActions, slotItems, createAction, closeAction, dragAction, preventDrag, - clickSound, soundVolume, soundPitch); - } - - public QuickGui buildAndRegister(String id) { - QuickGui gui = build(); - return register(id, gui); + clickSound, soundVolume, soundPitch, callbacks, defaultTimeout); } } @@ -411,50 +501,24 @@ public class QuickGui implements InventoryHolder { void onDrag(QuickGui gui, InventoryDragEvent event); } - public static class GuiUtils { + public interface GuiCallback { + void onInput(QuickGui gui, Player player, String input, InputSource source); - public static QuickGui createConfirmDialog(String title, Consumer callback) { - return create() - .titleMini("" + title) - .rows(3) - .fillBorder(Material.GRAY_STAINED_GLASS_PANE) - .itemMini(11, Material.GREEN_CONCRETE, "CONFIRM", - (gui, event) -> { - callback.accept(true); - event.getWhoClicked().closeInventory(); - }) - .itemMini(15, Material.RED_CONCRETE, "CANCEL", - (gui, event) -> { - callback.accept(false); - event.getWhoClicked().closeInventory(); - }) - .build(); + default void onTimeout(QuickGui gui, Player player) { + player.sendMessage(Component.text("Input timed out.", NamedTextColor.RED)); } - public static GuiBuilder createPaginated(String title, List items, int itemsPerPage) { - GuiBuilder builder = create() - .titleMini(title) - .rows(6) - .fillBorder(Material.GRAY_STAINED_GLASS_PANE); - - int totalPages = (int) Math.ceil((double) items.size() / itemsPerPage); - - if (totalPages > 1) { - builder.itemMini(45, Material.ARROW, "Previous Page") - .itemMini(53, Material.ARROW, "Next Page"); - } - - return builder; - } - - public static ItemStack createSeparator(NamedTextColor color) { - ItemStack pane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); - ItemMeta meta = pane.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text(" ").color(color)); - pane.setItemMeta(meta); - } - return pane; + default void onCancel(QuickGui gui, Player player) { + player.sendMessage(Component.text("Input cancelled.", NamedTextColor.YELLOW)); } } + + public enum InputSource { + CHAT, + COMMAND, + SIGN, + BOOK, + ANVIL, + CUSTOM + } } \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/systems/gui/QuickPaginatedGUI.java b/src/main/java/me/trouper/alias/server/systems/gui/QuickPaginatedGUI.java new file mode 100644 index 0000000..3052046 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/gui/QuickPaginatedGUI.java @@ -0,0 +1,436 @@ +package me.trouper.alias.server.systems.gui; + +import me.trouper.alias.utils.SoundPlayer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.*; + +public abstract class QuickPaginatedGUI { + + private static final MiniMessage miniMessage = MiniMessage.miniMessage(); + + protected static final int DEFAULT_ITEMS_PER_PAGE = 45; + protected static final int[] DEFAULT_PAGE_SLOTS = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44 + }; + protected static final int DEFAULT_PREV_SLOT = 45; + protected static final int DEFAULT_NEXT_SLOT = 53; + protected static final int DEFAULT_FILTER_SLOT = 49; + + protected static final Map currentPages = new HashMap<>(); + protected static final Map> activeFilters = new HashMap<>(); + protected static final Map chosenOperator = new HashMap<>(); + + protected abstract String getTitle(Player player); + + protected abstract List getAllItems(Player player); + + protected abstract ItemStack createDisplayItem(T item); + + protected abstract void handleItemClick(Player player, T item, InventoryClickEvent event); + + protected abstract void addFilterItems(QuickGui.GuiBuilder filterGui, Player player, Set filters); + + protected abstract void openBackGUI(Player player); + + protected int getItemsPerPage() { + return DEFAULT_ITEMS_PER_PAGE; + } + + protected int[] getPageSlots() { + return DEFAULT_PAGE_SLOTS; + } + + protected int getPreviousSlot() { + return DEFAULT_PREV_SLOT; + } + + protected int getNextSlot() { + return DEFAULT_NEXT_SLOT; + } + + protected int getFilterSlot() { + return DEFAULT_FILTER_SLOT; + } + + protected int getGuiSize() { + return 54; + } + + protected Sound getClickSound() { + return Sound.UI_BUTTON_CLICK; + } + + protected Sound getPageSound() { + return Sound.ITEM_BOOK_PAGE_TURN; + } + + protected Sound getFilterSound() { + return Sound.BLOCK_NOTE_BLOCK_BELL; + } + + public QuickGui createGUI(Player player) { + int page = currentPages.compute(player.getUniqueId(), (k, v) -> realizePage(player, v == null ? 0 : v)); + + QuickGui.GuiBuilder builder = QuickGui.create() + .titleMini(getTitle(player)) + .size(getGuiSize()) + .onGlobalClick((gui, event) -> event.setCancelled(true)) + .clickSound(getClickSound(), 0.5f, 1.0f); + + builder.item(getPreviousSlot(), createNavigationItem("Previous", page - 1), + (gui, event) -> changePage(player, -1)); + + builder.item(getNextSlot(), createNavigationItem("Next", page + 1), + (gui, event) -> changePage(player, 1)); + + builder.item(getFilterSlot(), createFilterItem(player), + (gui, event) -> { + if (event.isShiftClick()) { + cycleFilterOperator(player); + player.openInventory(createGUI(player).getInventory()); + } else { + openFilterMenu(player); + } + }); + + fillEmptySlots(builder); + + setupPageItems(builder, player, page); + + return builder.build(); + } + + private void setupPageItems(QuickGui.GuiBuilder builder, Player player, int page) { + List filteredItems = filterItems(player); + int[] pageSlots = getPageSlots(); + int itemsPerPage = pageSlots.length; + int startIndex = page * itemsPerPage; + + for (int i = 0; i < itemsPerPage; i++) { + int itemIndex = startIndex + i; + + if (itemIndex >= filteredItems.size()) { + break; + } + + T item = filteredItems.get(itemIndex); + int slot = pageSlots[i]; + + builder.item(slot, createDisplayItem(item), + (gui, event) -> handleItemClick(player, item, event)); + } + } + + private void fillEmptySlots(QuickGui.GuiBuilder builder) { + int[] navigationSlots = {46, 47, 48, 50, 51, 52}; + for (int slot : navigationSlots) { + builder.item(slot, createPlaceholderItem()); + } + } + + private void changePage(Player player, int direction) { + int current = currentPages.getOrDefault(player.getUniqueId(), 0); + + if (current == 0 && direction < 0) { + player.playSound(player.getLocation(), getPageSound(), 1.0f, 0.8f); + openBackGUI(player); + return; + } + + List filteredItems = filterItems(player); + int itemsPerPage = getItemsPerPage(); + int maxPages = (filteredItems.isEmpty() ? 0 : (filteredItems.size() - 1) / itemsPerPage) + 1; + + if (current >= maxPages && direction > 0) { + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1, 1); + return; + } + + int newPage = current + direction; + currentPages.put(player.getUniqueId(), newPage); + player.playSound(player.getLocation(), getPageSound(), 1.0f, 1.0f); + createGUI(player).open(player); + } + + + private int realizePage(Player player, int requestedPage) { + int validPage = Math.max(0, requestedPage); + List filteredItems = filterItems(player); + int maxPages = Math.max(0, (int) Math.ceil((double) filteredItems.size() / getPageSlots().length) - 1); + return Math.min(validPage, maxPages); + } + + private void openFilterMenu(Player player) { + Set filters = activeFilters.computeIfAbsent(player.getUniqueId(), k -> new HashSet<>()); + FilterOperator operator = chosenOperator.computeIfAbsent(player.getUniqueId(), k -> FilterOperator.AND); + + QuickGui.GuiBuilder filterGui = QuickGui.create() + .titleMini("Filters") + .rows(3) + .onGlobalClick((gui, event) -> event.setCancelled(true)) + .clickSound(getClickSound(), 0.5f, 1.0f); + + filterGui.item(13, createOperatorItem(operator), (gui, event) -> { + cycleFilterOperator(player); + openFilterMenu(player); + }); + + filterGui.item(26, createBackItem(), (gui, event) -> { + player.playSound(player.getLocation(), getPageSound(), 1.0f, 0.8f); + createGUI(player).open(player); + }); + + addFilterItems(filterGui, player, filters); + + player.playSound(player.getLocation(), getFilterSound(), 1.0f, 0.8f); + filterGui.build().open(player); + } + + private void cycleFilterOperator(Player player) { + FilterOperator current = chosenOperator.computeIfAbsent(player.getUniqueId(), k -> FilterOperator.AND); + FilterOperator[] values = FilterOperator.values(); + int nextIndex = (current.ordinal() + 1) % values.length; + chosenOperator.put(player.getUniqueId(), values[nextIndex]); + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, 1.2f); + } + + protected void toggleFilter(Player player, String filterKey) { + Set filters = activeFilters.computeIfAbsent(player.getUniqueId(), k -> new HashSet<>()); + + if (filters.contains(filterKey)) { + filters.remove(filterKey); + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, 0.8f); + } else { + filters.add(filterKey); + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, 1.0f); + } + + openFilterMenu(player); + } + + private List filterItems(Player player) { + List allItems = getAllItems(player); + Set filters = activeFilters.get(player.getUniqueId()); + + if (filters == null || filters.isEmpty()) { + return allItems; + } + + FilterOperator operator = chosenOperator.computeIfAbsent(player.getUniqueId(), k -> FilterOperator.AND); + return allItems.stream() + .filter(item -> applyFilters(player, item, filters, operator)) + .toList(); + } + + private boolean applyFilters(Player player, T item, Set filters, FilterOperator operator) { + boolean result = (operator == FilterOperator.AND); + for (String filter : filters) { + boolean conditionMet = testFilter(player, item, filter); + result = operator.apply(result, conditionMet); + + if (operator == FilterOperator.AND && !result) return false; + if (operator == FilterOperator.OR && result) return true; + } + + return result; + } + + protected boolean testFilter(Player player, T item, String filterKey) { + return true; + } + + protected int getFilteredCount(Player player) { + return filterItems(player).size(); + } + + protected int getFilterCount(Player player) { + Set filters = activeFilters.get(player.getUniqueId()); + return filters != null ? filters.size() : 0; + } + + private ItemStack createNavigationItem(String direction, int page) { + if (page < 0) { + return createBackItem(); + } + + ItemStack item = new ItemStack(Material.ARROW); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(direction + " Page") + .color(NamedTextColor.AQUA) + .decoration(TextDecoration.ITALIC, false)); + meta.lore(Arrays.asList( + Component.text("Page " + page) + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false) + )); + item.setItemMeta(meta); + } + return item; + } + + private ItemStack createBackItem() { + ItemStack item = new ItemStack(Material.BARRIER); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text("Back") + .color(NamedTextColor.RED) + .decoration(TextDecoration.ITALIC, false)); + item.setItemMeta(meta); + } + return item; + } + + private ItemStack createPlaceholderItem() { + ItemStack item = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(" ")); + item.setItemMeta(meta); + } + return item; + } + + private ItemStack createFilterItem(Player player) { + FilterOperator operator = chosenOperator.computeIfAbsent(player.getUniqueId(), k -> FilterOperator.AND); + int filterCount = getFilterCount(player); + + ItemStack item = new ItemStack(Material.HOPPER); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text("Filters") + .color(NamedTextColor.GOLD) + .decoration(TextDecoration.BOLD, true) + .decoration(TextDecoration.ITALIC, false)); + + List lore = new ArrayList<>(); + lore.add(Component.text("Filters Selected: " + filterCount) + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("Shift-Click to cycle filter operator") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("Current Operator: " + operator.name()) + .color(NamedTextColor.AQUA) + .decoration(TextDecoration.ITALIC, false)); + + meta.lore(lore); + item.setItemMeta(meta); + } + return item; + } + + private ItemStack createOperatorItem(FilterOperator operator) { + ItemStack item = new ItemStack(Material.COMPARATOR); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text("Filter Operator: " + operator.name()) + .color(NamedTextColor.YELLOW) + .decoration(TextDecoration.ITALIC, false)); + + List lore = new ArrayList<>(); + lore.add(Component.text("Current: " + operator.name()) + .color(NamedTextColor.AQUA) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("Click to cycle") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + + lore.add(Component.text("")); + lore.add(Component.text("AND: All conditions must be met") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("OR: At least one condition must be met") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("NAND: At least one condition must NOT be met") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("XOR: Exactly one condition must be met") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + + meta.lore(lore); + item.setItemMeta(meta); + } + return item; + } + + protected ItemStack createFilterToggleItem(String name, Material material, boolean active) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(name) + .color(active ? NamedTextColor.GREEN : NamedTextColor.RED) + .decoration(TextDecoration.ITALIC, false)); + meta.lore(Arrays.asList( + Component.text("Click to " + (active ? "disable" : "enable")) + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false) + )); + item.setItemMeta(meta); + } + return item; + } + + protected ItemStack createFilterToggleItemWithValue(String name, Material material, boolean active, String value) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(name) + .color(active ? NamedTextColor.GREEN : NamedTextColor.RED) + .decoration(TextDecoration.ITALIC, false)); + + List lore = new ArrayList<>(); + lore.add(Component.text("Value: " + value) + .color(NamedTextColor.AQUA) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("Left Click to " + (active ? "disable" : "enable")) + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("Right Click to set value") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false)); + + meta.lore(lore); + item.setItemMeta(meta); + } + return item; + } + + public static void clearPlayerData(UUID playerUUID) { + currentPages.remove(playerUUID); + activeFilters.remove(playerUUID); + chosenOperator.remove(playerUUID); + } + + public enum FilterOperator { + AND, + OR, + NAND, + XOR; + + public boolean apply(boolean currentValue, boolean newCondition) { + return switch (this) { + case AND -> currentValue & newCondition; + case OR -> currentValue | newCondition; + case NAND -> !(currentValue & newCondition); + case XOR -> currentValue ^ newCondition; + }; + } + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/ItemBuilder.java b/src/main/java/me/trouper/alias/utils/ItemBuilder.java index d773477..6dd922f 100644 --- a/src/main/java/me/trouper/alias/utils/ItemBuilder.java +++ b/src/main/java/me/trouper/alias/utils/ItemBuilder.java @@ -18,10 +18,7 @@ import org.bukkit.profile.PlayerTextures; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.function.Function; public class ItemBuilder { @@ -336,4 +333,60 @@ public class ItemBuilder { return create(Material.PLAYER_HEAD) .skullTexture(url); } + + public static ItemStack integerItem(Material mat, String nameMm, List descMm, int value) { + return ItemBuilder.of(mat) + .displayName(nameMm) + .loreMiniMessage(descMm) + .loreMiniMessage("Current Value: " + value) + .build(); + } + + public static ItemStack booleanItem(Material mat, String nameMm, List descMm, boolean value) { + String state = value ? "ON" : "OFF"; + return ItemBuilder.of(mat) + .displayName(nameMm) + .loreMiniMessage(descMm) + .loreMiniMessage("State: " + state) + .build(); + } + + public static ItemStack stringItem(Material mat, String nameMm, List descMm, String value) { + ItemBuilder b = ItemBuilder.of(mat) + .displayName(nameMm) + .loreMiniMessage(descMm); + b.loreMiniMessage("Text: " + b.miniMessage.escapeTags(value)); + return b.build(); + } + + public static ItemStack doubleItem(Material mat, String nameMm, List descMm, double value) { + return ItemBuilder.of(mat) + .displayName(nameMm) + .loreMiniMessage(descMm) + .loreMiniMessage("Value: " + value) + .build(); + } + + public static ItemStack listItem(Material mat, String nameMm, List descMm, List values) { + ItemBuilder b = ItemBuilder.of(mat) + .displayName(nameMm) + .loreMiniMessage(descMm) + .loreMiniMessage("List:"); + for (String entry : values) { + b.loreMiniMessage(" - " + b.miniMessage.escapeTags(entry)); + } + return b.build(); + } + + public static ItemStack mapItem(Material mat, String nameMm, List descMm, Map map) { + ItemBuilder b = ItemBuilder.of(mat) + .displayName(nameMm) + .loreMiniMessage(descMm) + .loreMiniMessage("Entries:"); + for (Map.Entry e : map.entrySet()) { + b.loreMiniMessage(" " + b.miniMessage.escapeTags(e.getKey()) + ": " + b.miniMessage.escapeTags(e.getValue())); + } + return b.build(); + } + }