Back
ScriptGUI.java
javasrc/main/java/com/scriptslab/core/script/ScriptGUI.java
package com.scriptslab.core.script;
import com.scriptslab.ScriptsLabPlugin;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.graalvm.polyglot.Value;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/**
* GUI / Inventory API for JavaScript scripts.
* Allows creating interactive chest menus.
*
* Usage in JS:
* var gui = GUI.create('My Menu', 27);
* gui.setItem(0, 'DIAMOND', '&bClick me!', function(player) {
* player.sendMessage('Clicked!');
* });
* gui.open(player);
* gui.onClose(function(player) { Console.log(player.getName() + ' closed'); });
*/
public final class ScriptGUI implements Listener {
private final ScriptsLabPlugin plugin;
private final Logger logger;
private final LegacyComponentSerializer serializer;
// Maps open inventory -> GUI instance (for click/close handling)
private final Map<UUID, GUIInstance> openGUIs;
public ScriptGUI(ScriptsLabPlugin plugin) {
this.plugin = plugin;
this.logger = Logger.getLogger("ScriptGUI");
this.serializer = LegacyComponentSerializer.legacyAmpersand();
this.openGUIs = new ConcurrentHashMap<>();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/**
* Creates a new GUI with the given title and size.
*
* @param title display title (supports & color codes)
* @param size inventory size (must be multiple of 9, max 54)
* @return GUIInstance builder
*/
public GUIInstance create(String title, int size) {
int clampedSize = Math.max(9, Math.min(54, (size / 9) * 9));
return new GUIInstance(title, clampedSize);
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) return;
GUIInstance gui = openGUIs.get(player.getUniqueId());
if (gui == null) return;
// Only handle clicks in the top inventory (the GUI itself)
if (!event.getInventory().equals(event.getClickedInventory())) return;
event.setCancelled(true);
int slot = event.getSlot();
Value handler = gui.clickHandlers.get(slot);
if (handler != null && handler.canExecute()) {
try {
handler.execute(player);
} catch (Exception e) {
logger.warning("[ScriptGUI] Click handler error at slot " + slot + ": " + e.getMessage());
}
}
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player)) return;
GUIInstance gui = openGUIs.remove(player.getUniqueId());
if (gui == null) return;
if (gui.closeHandler != null && gui.closeHandler.canExecute()) {
// Schedule to next tick to avoid issues with close event
plugin.getServer().getScheduler().runTask(plugin, () -> {
try {
gui.closeHandler.execute(player);
} catch (Exception e) {
logger.warning("[ScriptGUI] Close handler error: " + e.getMessage());
}
});
}
}
/**
* Represents a GUI menu instance.
*/
public final class GUIInstance {
private final String title;
private final int size;
private final Inventory inventory;
private final Map<Integer, Value> clickHandlers;
private Value closeHandler;
GUIInstance(String title, int size) {
this.title = title;
this.size = size;
this.clickHandlers = new HashMap<>();
Component titleComponent = serializer.deserialize(title);
this.inventory = Bukkit.createInventory(null, size, titleComponent);
}
/**
* Sets an item in a slot with a click handler.
*
* @param slot slot index (0-based)
* @param material material name (e.g. "DIAMOND")
* @param name display name (supports & color codes)
* @param handler JS function(player) called on click (can be null)
* @param lore optional lore lines
*/
public GUIInstance setItem(int slot, String material, String name, Object handler, String... lore) {
if (slot < 0 || slot >= size) return this;
try {
Material mat = Material.valueOf(material.toUpperCase());
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.displayName(serializer.deserialize(name));
if (lore.length > 0) {
List<Component> loreComponents = new ArrayList<>();
for (String line : lore) {
loreComponents.add(serializer.deserialize(line));
}
meta.lore(loreComponents);
}
item.setItemMeta(meta);
}
inventory.setItem(slot, item);
if (handler instanceof Value v && v.canExecute()) {
clickHandlers.put(slot, v);
}
} catch (IllegalArgumentException e) {
logger.warning("[ScriptGUI] Invalid material: " + material);
}
return this;
}
/**
* Sets an item without a click handler (decorative).
*/
public GUIInstance setItem(int slot, String material, String name, String... lore) {
return setItem(slot, material, name, null, lore);
}
/**
* Fills empty slots with a filler item (e.g. gray glass pane).
*/
public GUIInstance fill(String material, String name) {
for (int i = 0; i < size; i++) {
if (inventory.getItem(i) == null) {
setItem(i, material, name);
}
}
return this;
}
/**
* Sets a close handler.
*
* @param handler JS function(player) called when GUI is closed
*/
public GUIInstance onClose(Object handler) {
if (handler instanceof Value v && v.canExecute()) {
this.closeHandler = v;
}
return this;
}
/**
* Clears a slot.
*/
public GUIInstance clearSlot(int slot) {
inventory.setItem(slot, null);
clickHandlers.remove(slot);
return this;
}
/**
* Opens this GUI for a player.
*/
public void open(Player player) {
openGUIs.put(player.getUniqueId(), this);
player.openInventory(inventory);
}
/**
* Closes this GUI for a player.
*/
public void close(Player player) {
player.closeInventory();
}
/**
* Updates an item in a slot without changing the handler.
*/
public GUIInstance updateItem(int slot, String material, String name, String... lore) {
return setItem(slot, material, name, clickHandlers.get(slot), lore);
}
public int getSize() {
return size;
}
public String getTitle() {
return title;
}
}
}