Added Paginated gui and callbacks.

This commit is contained in:
wolf
2025-07-09 21:40:09 -04:00
parent 06a9aca5e0
commit 370bcb1ad1
12 changed files with 946 additions and 115 deletions

View File

@@ -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<JsonSerializable<?>> 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; }
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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<String> debuggerExclusions = new ArrayList<>();
DebugConfig() {
this.debugMode = false;
debuggerExclusions = new ArrayList<>();
}
}

View File

@@ -117,4 +117,23 @@ public interface QuickCommand extends TabExecutor, ContextAware {
command.setTabCompleter((sender, command2, label, args) -> List.of());
}
}
default CompletionBuilder quickDebugArgs(CompletionBuilder b, List<String> 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)
)
)
);
}
}

View File

@@ -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;
}
}

View File

@@ -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<Player, QuickGui> 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<Player, QuickGui> 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));
}
}

View File

@@ -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());
});
}
}

View File

@@ -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;

View File

@@ -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)
);
}

View File

@@ -23,7 +23,6 @@ import java.util.function.Consumer;
public class QuickGui implements InventoryHolder {
private static final Map<String, QuickGui> registry = new ConcurrentHashMap<>();
private static final MiniMessage miniMessage = MiniMessage.miniMessage();
private final Map<Integer, GuiAction> slotActions;
@@ -42,10 +41,16 @@ public class QuickGui implements InventoryHolder {
private Inventory inventory;
private final Set<Player> viewers;
private final Map<String, GuiCallback> callbacks;
private final Map<Player, String> waitingForInput;
private final Map<Player, Long> inputTimeouts;
private final long defaultTimeout;
private QuickGui(Component title, int size, GuiAction globalAction,
Map<Integer, GuiAction> slotActions, Map<Integer, ItemStack> slotItems,
GuiCreateAction createAction, GuiCloseAction closeAction, GuiDragAction dragAction,
boolean preventDrag, Sound clickSound, float soundVolume, float soundPitch) {
boolean preventDrag, Sound clickSound, float soundVolume, float soundPitch,
Map<String, GuiCallback> 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<QuickGui> getRegistered(String id) {
return Optional.ofNullable(registry.get(id));
}
public static Map<String, QuickGui> 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<Player> getPlayersWaitingForInput() {
return new HashSet<>(waitingForInput.keySet());
}
public void cleanupExpiredTimeouts() {
long currentTime = System.currentTimeMillis();
Iterator<Map.Entry<Player, Long>> iterator = inputTimeouts.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Player, Long> 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<String, GuiCallback> 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<Boolean> callback) {
return create()
.titleMini("<green>" + title)
.rows(3)
.fillBorder(Material.GRAY_STAINED_GLASS_PANE)
.itemMini(11, Material.GREEN_CONCRETE, "<green><bold>CONFIRM",
(gui, event) -> {
callback.accept(true);
event.getWhoClicked().closeInventory();
})
.itemMini(15, Material.RED_CONCRETE, "<red><bold>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<ItemStack> 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, "<yellow>Previous Page")
.itemMini(53, Material.ARROW, "<yellow>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
}
}

View File

@@ -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<T> {
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<UUID, Integer> currentPages = new HashMap<>();
protected static final Map<UUID, Set<String>> activeFilters = new HashMap<>();
protected static final Map<UUID, FilterOperator> chosenOperator = new HashMap<>();
protected abstract String getTitle(Player player);
protected abstract List<T> 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<String> 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<T> 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<T> 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<T> 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<String> filters = activeFilters.computeIfAbsent(player.getUniqueId(), k -> new HashSet<>());
FilterOperator operator = chosenOperator.computeIfAbsent(player.getUniqueId(), k -> FilterOperator.AND);
QuickGui.GuiBuilder filterGui = QuickGui.create()
.titleMini("<gold><bold>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<String> 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<T> filterItems(Player player) {
List<T> allItems = getAllItems(player);
Set<String> 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<String> 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<String> 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<Component> 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<Component> 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<Component> 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;
};
}
}
}

View File

@@ -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<String> descMm, int value) {
return ItemBuilder.of(mat)
.displayName(nameMm)
.loreMiniMessage(descMm)
.loreMiniMessage("<dark_green><bold>Current Value</bold><white>: <gray>" + value)
.build();
}
public static ItemStack booleanItem(Material mat, String nameMm, List<String> descMm, boolean value) {
String state = value ? "<green>ON" : "<red>OFF";
return ItemBuilder.of(mat)
.displayName(nameMm)
.loreMiniMessage(descMm)
.loreMiniMessage("<dark_gray>State: " + state)
.build();
}
public static ItemStack stringItem(Material mat, String nameMm, List<String> descMm, String value) {
ItemBuilder b = ItemBuilder.of(mat)
.displayName(nameMm)
.loreMiniMessage(descMm);
b.loreMiniMessage("<dark_gray>Text: <white>" + b.miniMessage.escapeTags(value));
return b.build();
}
public static ItemStack doubleItem(Material mat, String nameMm, List<String> descMm, double value) {
return ItemBuilder.of(mat)
.displayName(nameMm)
.loreMiniMessage(descMm)
.loreMiniMessage("<dark_gray>Value: <white>" + value)
.build();
}
public static ItemStack listItem(Material mat, String nameMm, List<String> descMm, List<String> values) {
ItemBuilder b = ItemBuilder.of(mat)
.displayName(nameMm)
.loreMiniMessage(descMm)
.loreMiniMessage("<dark_gray>List:");
for (String entry : values) {
b.loreMiniMessage(" <white>- " + b.miniMessage.escapeTags(entry));
}
return b.build();
}
public static ItemStack mapItem(Material mat, String nameMm, List<String> descMm, Map<String, String> map) {
ItemBuilder b = ItemBuilder.of(mat)
.displayName(nameMm)
.loreMiniMessage(descMm)
.loreMiniMessage("<dark_gray>Entries:");
for (Map.Entry<String, String> e : map.entrySet()) {
b.loreMiniMessage(" <white>" + b.miniMessage.escapeTags(e.getKey()) + ": <gray>" + b.miniMessage.escapeTags(e.getValue()));
}
return b.build();
}
}