Back

ScriptAPIImpl.java

java
src/main/java/com/scriptslab/core/script/ScriptAPIImpl.java
package com.scriptslab.core.script;

import com.scriptslab.ScriptsLabPlugin;
import com.scriptslab.api.item.CustomItem;
import com.scriptslab.core.item.CustomItemImpl;
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.Event;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

/**
 * API exposed to JavaScript scripts.
 * Provides safe access to plugin functionality.
 */
public final class ScriptAPIImpl implements Listener {
    
    private final ScriptsLabPlugin plugin;
    private final LegacyComponentSerializer serializer;
    private final java.util.Map<Class<? extends Event>, org.bukkit.plugin.EventExecutor> eventExecutors;
    private final java.util.Map<Class<? extends Event>, java.util.List<Runnable>> handlersMap;
    private java.util.Map<String, Object> commandCodeMap;
    private java.util.Map<String, String> commandSourceMap;

    // New subsystems
    private final ScriptScheduler scriptScheduler;
    private final ScriptStorage scriptStorage;
    private ScriptGUI scriptGUI;
    
    public ScriptAPIImpl(ScriptsLabPlugin plugin) {
        this.plugin = plugin;
        this.serializer = LegacyComponentSerializer.legacyAmpersand();
        this.eventExecutors = new java.util.HashMap<>();
        this.handlersMap = new java.util.HashMap<>();
        this.commandCodeMap = new java.util.HashMap<>();
        this.commandSourceMap = new java.util.HashMap<>();
        this.scriptScheduler = new ScriptScheduler(plugin);
        this.scriptStorage = new ScriptStorage(plugin);
        // ScriptGUI is initialized lazily after plugin is fully loaded
    }

    /** Called after plugin is fully initialized to set up GUI listener. */
    public void initGUI() {
        if (scriptGUI == null) {
            scriptGUI = new ScriptGUI(plugin);
        }
    }

    public ScriptScheduler getScheduler() { return scriptScheduler; }
    public ScriptStorage getStorage() { return scriptStorage; }
    public ScriptGUI getGUI() { return scriptGUI; }
    
    public void storeCommand(String name, Object executor) {
        commandCodeMap.put(name, executor);
    }
    
    public void storeCommandSource(String name, String jsCode) {
        commandSourceMap.put(name, jsCode);
    }
    
    public Object getCommand(String name) {
        return commandCodeMap.get(name);
    }
    
    public String getCommandSource(String name) {
        return commandSourceMap.get(name);
    }
    
    public java.util.function.BiConsumer<org.bukkit.command.CommandSender, String[]> createCommand(String jsCode) {
        return (sender, args) -> {
            String code = jsCode
                .replace("{sender}", "sender")
                .replace("{args}", "args");
            plugin.getScriptEngine().execute(code);
        };
    }
    
    // === Player Management ===
    
    public Player getPlayer(String name) {
        return Bukkit.getPlayer(name);
    }
    
    public Player getPlayer(UUID uuid) {
        return Bukkit.getPlayer(uuid);
    }
    
    public List<Player> getOnlinePlayers() {
        return List.copyOf(Bukkit.getOnlinePlayers());
    }
    
    public void sendMessage(Player player, String message) {
        Component component = serializer.deserialize(message);
        player.sendMessage(component);
    }
    
    public void broadcast(String message) {
        Component component = serializer.deserialize(message);
        Bukkit.broadcast(component);
    }
    
    // === Item Management ===
    
    public void registerItem(String id, String materialName, String displayName, String... lore) {
        try {
            Material material = Material.valueOf(materialName.toUpperCase());
            
            CustomItem item = new CustomItemImpl.Builder(id)
                    .material(material)
                    .displayName(displayName)
                    .lore(Arrays.asList(lore))
                    .build();
            
            plugin.getItemManager().registerItem(item).join();
            
        } catch (IllegalArgumentException e) {
            plugin.getLogger().warning("Invalid material: " + materialName);
        }
    }
    
    public void giveItem(Player player, String itemId, int amount) {
        log("giveItem called: " + itemId + " to " + player.getName());
        java.util.Optional<ItemStack> result = plugin.getItemManager().createItemStack(itemId, amount);
        if (result.isPresent()) {
            player.getInventory().addItem(result.get());
            log("Item given successfully!");
        } else {
            log("Item NOT found: " + itemId);
        }
    }
    
    public void giveItem(Player player, String itemId) {
        giveItem(player, itemId, 1);
    }
    
    // === Event Management ===
    
    /**
     * Register event handler by event name (string).
     * Usage: Events.on('PlayerJoinEvent', function(event) { ... });
     */
    public void on(String eventName, Object handler) {
        try {
            // Try to find the event class
            Class<?> eventClass = null;
            
            // Common event packages
            String[] packages = {
                "org.bukkit.event.player.",
                "org.bukkit.event.entity.",
                "org.bukkit.event.block.",
                "org.bukkit.event.world.",
                "org.bukkit.event.server.",
                "org.bukkit.event.inventory."
            };
            
            for (String pkg : packages) {
                try {
                    eventClass = Class.forName(pkg + eventName);
                    break;
                } catch (ClassNotFoundException ignored) {
                }
            }
            
            if (eventClass == null) {
                log("Warning: Event class not found: " + eventName);
                return;
            }
            
            @SuppressWarnings("unchecked")
            Class<? extends Event> eventType = (Class<? extends Event>) eventClass;
            on(eventType, () -> {
                // Handler will be called - but we need to pass event to JS function
                // This is simplified - full implementation would pass event object
            });
            
        } catch (Exception e) {
            log("Error registering event " + eventName + ": " + e.getMessage());
        }
    }
    
    @SuppressWarnings("unchecked")
    public void on(Class<? extends Event> eventClass, Runnable handler) {
        java.util.List<Runnable> handlers = (java.util.List<Runnable>) handlersMap.get(eventClass);
        if (handlers == null) {
            handlers = new java.util.ArrayList<>();
            handlersMap.put(eventClass, handlers);
        }
        handlers.add(handler);
        
        if (!eventExecutors.containsKey(eventClass)) {
            org.bukkit.plugin.EventExecutor executor = (listener, event) -> {
                java.util.List<Runnable> eventHandlers = (java.util.List<Runnable>) handlersMap.get(event.getClass());
                if (eventHandlers != null) {
                    for (Runnable r : eventHandlers) {
                        try { r.run(); } catch (Exception e) { plugin.getLogger().warning("Event error: " + e.getMessage()); }
                    }
                }
            };
            
            plugin.getServer().getPluginManager().registerEvent(
                eventClass, this, org.bukkit.event.EventPriority.NORMAL, executor, plugin
            );
            eventExecutors.put(eventClass, executor);
            log("Registered event: " + eventClass.getSimpleName());
        }
    }
    
    // === Scheduler ===

    public int runLater(Object callback, long delayTicks) {
        return scriptScheduler.runLater(callback, delayTicks);
    }

    public int runTimer(Object callback, long delayTicks, long periodTicks) {
        return scriptScheduler.runTimer(callback, delayTicks, periodTicks);
    }

    public int runAsync(Object callback) {
        return scriptScheduler.runAsync(callback);
    }

    public void cancelTask(int taskId) {
        scriptScheduler.cancel(taskId);
    }

    public void cancelAllTasks() {
        scriptScheduler.cancelAll();
    }
    
    // === Storage API ===

    /**
     * Opens a persistent storage namespace.
     * Usage: var db = Storage.open('mydata');
     */
    public ScriptStorage.Namespace openStorage(String name) {
        return scriptStorage.open(name);
    }

    // === GUI API ===

    /**
     * Creates a new GUI menu.
     * Usage: var gui = GUI.create('Title', 27);
     */
    public ScriptGUI.GUIInstance createGUI(String title, int size) {
        if (scriptGUI == null) initGUI();
        return scriptGUI.create(title, size);
    }

    // === Logging ===
    
    public void log(String message) {
        plugin.getLogger().info("[Script] " + message);
    }
    
    public void warn(String message) {
        plugin.getLogger().warning("[Script] " + message);
    }
    
    public void error(String message) {
        plugin.getLogger().severe("[Script] " + message);
    }
    
    // === Command Registration ===
    
    /**
     * Alias for registerCommand - for convenience in scripts.
     * Usage: Commands.register('mycommand', function(sender, args) { ... });
     */
    public void register(String name, Object executor) {
        registerCommand(name, executor, null);
    }
    
    /**
     * Alias for registerCommand with permission.
     * Usage: Commands.register('mycommand', function(sender, args) { ... }, 'permission');
     */
    public void register(String name, Object executor, String permission) {
        registerCommand(name, executor, permission);
    }
    
    public void registerCommand(String name, Object executor) {
        registerCommand(name, executor, null);
    }
    
    /**
     * Registers a command from script - accepts JavaScript function.
     */
    public void registerCommand(String name, Object executor, String permission) {
        try {
            final String cmdName = name;
            
            // Store the executor function for later invocation
            storeCommand(cmdName, executor);
            
            plugin.getCommandManager().registerScriptCommand(name, (sender, args) -> {
                try {
                    // Get the stored executor
                    Object storedExecutor = getCommand(cmdName);
                    if (storedExecutor == null) {
                        sender.sendMessage("§cCommand executor not found!");
                        return;
                    }
                    
                    // Check if sender is a player
                    org.bukkit.entity.Player player = null;
                    if (sender instanceof org.bukkit.entity.Player) {
                        player = (org.bukkit.entity.Player) sender;
                    }
                    
                    // Build JavaScript code to invoke the stored function
                    String argArray = makeArray(args);
                    
                    // Create sender wrapper object
                    String senderWrapper;
                    if (player != null) {
                        String playerName = player.getName();
                        senderWrapper = "{ " +
                            "isPlayer: function() { return true; }, " +
                            "getName: function() { return '" + playerName + "'; }, " +
                            "sendMessage: function(m) { var p = org.bukkit.Bukkit.getPlayer('" + playerName + "'); if(p) { p.sendMessage(m); } }, " +
                            "getInventory: function() { return org.bukkit.Bukkit.getPlayer('" + playerName + "').getInventory(); }, " +
                            "hasPermission: function(perm) { return org.bukkit.Bukkit.getPlayer('" + playerName + "').hasPermission(perm); }, " +
                            "getLocation: function() { return org.bukkit.Bukkit.getPlayer('" + playerName + "').getLocation(); }, " +
                            "getWorld: function() { return org.bukkit.Bukkit.getPlayer('" + playerName + "').getWorld(); }, " +
                            "setAllowFlight: function(v) { org.bukkit.Bukkit.getPlayer('" + playerName + "').setAllowFlight(v); }, " +
                            "getAllowFlight: function() { return org.bukkit.Bukkit.getPlayer('" + playerName + "').getAllowFlight(); }, " +
                            "setHealth: function(v) { org.bukkit.Bukkit.getPlayer('" + playerName + "').setHealth(v); }, " +
                            "getMaxHealth: function() { return org.bukkit.Bukkit.getPlayer('" + playerName + "').getMaxHealth(); }, " +
                            "setFoodLevel: function(v) { org.bukkit.Bukkit.getPlayer('" + playerName + "').setFoodLevel(v); }, " +
                            "setSaturation: function(v) { org.bukkit.Bukkit.getPlayer('" + playerName + "').setSaturation(v); }, " +
                            "getActivePotionEffects: function() { return org.bukkit.Bukkit.getPlayer('" + playerName + "').getActivePotionEffects(); }, " +
                            "removePotionEffect: function(t) { org.bukkit.Bukkit.getPlayer('" + playerName + "').removePotionEffect(t); } " +
                            "}";
                    } else {
                        senderWrapper = "{ " +
                            "isPlayer: function() { return false; }, " +
                            "getName: function() { return '" + sender.getName() + "'; }, " +
                            "sendMessage: function(m) { /* console sender */ } " +
                            "}";
                    }
                    
                    // Get the stored function from the command map and invoke it
                    String code = "(function() { " +
                            "var executor = API.getCommand('" + cmdName + "'); " +
                            "if (executor && typeof executor === 'function') { " +
                            "  var sender = " + senderWrapper + "; " +
                            "  var args = " + argArray + "; " +
                            "  executor(sender, args); " +
                            "} else { " +
                            "  Console.error('Executor is not a function for command: " + cmdName + "'); " +
                            "} " +
                            "})();";
                    
                    plugin.getScriptEngine().execute(code).exceptionally(ex -> {
                        sender.sendMessage("§cError: " + ex.getMessage());
                        log("Error /" + cmdName + ": " + ex.getMessage());
                        ex.printStackTrace();
                        return null;
                    });
                } catch (Exception e) {
                    sender.sendMessage("§cError: " + e.getMessage());
                    log("Error: " + e.getMessage());
                    e.printStackTrace();
                }
            }, permission).join();
            
            log("Registered command: /" + name);
        } catch (Exception e) {
            log("Failed to register command: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    private String makeArray(String[] arr) {
        if (arr == null || arr.length == 0) return "[]";
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < arr.length; i++) {
            String s = arr[i].replace("\\", "\\\\").replace("'", "\\'");
            sb.append("'").append(s).append("'");
            if (i < arr.length - 1) sb.append(", ");
        }
        sb.append("]");
        return sb.toString();
    }
    
    // === Inventory Management ===
    
    /**
     * Gives an item to a player.
     */
    public void giveItemStack(Player player, ItemStack item) {
        player.getInventory().addItem(item);
    }
    
    /**
     * Removes an item from player inventory.
     */
    public void removeItem(Player player, Material material, int amount) {
        player.getInventory().removeItem(new ItemStack(material, amount));
    }
    
    /**
     * Clears player inventory.
     */
    public void clearInventory(Player player) {
        player.getInventory().clear();
    }
    
    // === World Management ===
    
    /**
     * Gets a world by name.
     */
    public org.bukkit.World getWorld(String name) {
        return Bukkit.getWorld(name);
    }
    
    /**
     * Gets all worlds.
     */
    public List<org.bukkit.World> getWorlds() {
        return Bukkit.getWorlds();
    }
    
    // === Metrics ===
    
    /**
     * Records a metric.
     */
    public void recordMetric(String name, double value) {
        plugin.getMetricsCollector().record(name, value);
    }
    
    /**
     * Increments a counter.
     */
    public void incrementCounter(String name) {
        plugin.getMetricsCollector().increment(name);
    }
    
    // === Utility ===
    
    public org.bukkit.Server getServer() {
        return Bukkit.getServer();
    }
    
    public ScriptsLabPlugin getPlugin() {
        return plugin;
    }
    
    // === Config Access ===
    
    public boolean getConfigBoolean(String path, boolean defaultValue) {
        return plugin.getConfig().getBoolean(path, defaultValue);
    }
    
    public String getConfigString(String path, String defaultValue) {
        return plugin.getConfig().getString(path, defaultValue);
    }
    
    public int getConfigInt(String path, int defaultValue) {
        return plugin.getConfig().getInt(path, defaultValue);
    }
    
    // === Item Creation ===
    
    public void createItem(String id, String materialName, String displayName, String... lore) {
        registerItem(id, materialName, displayName, lore);
    }
    
    // === Attribute Helper ===
    
    /**
     * Add attribute modifier to item meta.
     * Usage: API.addAttribute(meta, 'GENERIC_ATTACK_DAMAGE', 'my_modifier', 10.0, 'ADD_NUMBER', 'HAND');
     */
    public void addAttribute(org.bukkit.inventory.meta.ItemMeta meta, String attributeName, String modifierName, double value, String operation, String slot) {
        try {
            // Get attribute
            org.bukkit.attribute.Attribute attribute = org.bukkit.attribute.Attribute.valueOf(attributeName);
            
            // Get operation
            org.bukkit.attribute.AttributeModifier.Operation op = 
                org.bukkit.attribute.AttributeModifier.Operation.valueOf(operation);
            
            // Get slot
            org.bukkit.inventory.EquipmentSlot equipSlot = 
                org.bukkit.inventory.EquipmentSlot.valueOf(slot);
            
            // Create modifier
            org.bukkit.attribute.AttributeModifier modifier = new org.bukkit.attribute.AttributeModifier(
                java.util.UUID.randomUUID(),
                modifierName,
                value,
                op,
                equipSlot
            );
            
            // Add to meta
            meta.addAttributeModifier(attribute, modifier);
            
            log("Added attribute " + attributeName + " with value " + value);
            
        } catch (Exception e) {
            log("Failed to add attribute: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    // === Event Registration Helper ===
    
    /**
     * Register event handler with JavaScript function.
     * Usage from JS: API.registerEvent('EntityDamageByEntityEvent', function(event) { ... });
     * Also supports Paper events: API.registerEvent('PlayerJumpEvent', function(event) { ... });
     */
    public void registerEvent(String eventClassName, Object handler) {
        try {
            Class<?> eventClass = findEventClass(eventClassName);

            if (eventClass == null) {
                log("Event class not found: " + eventClassName);
                return;
            }

            @SuppressWarnings("unchecked")
            Class<? extends Event> eventType = (Class<? extends Event>) eventClass;

            // Store handler
            storeEventHandler(eventClassName, handler);

            // Create event executor that calls the JS function on the main thread
            org.bukkit.plugin.EventExecutor executor = (listener, event) -> {
                if (eventType.isInstance(event)) {
                    plugin.getServer().getScheduler().runTask(plugin, () -> {
                        try {
                            Object storedHandler = getEventHandler(eventClassName);
                            if (storedHandler instanceof org.graalvm.polyglot.Value v && v.canExecute()) {
                                v.execute(event);
                            }
                        } catch (Exception e) {
                            log("Error in event handler " + eventClassName + ": " + e.getMessage());
                        }
                    });
                }
            };

            // Register event
            plugin.getServer().getPluginManager().registerEvent(
                    eventType,
                    this,
                    org.bukkit.event.EventPriority.NORMAL,
                    executor,
                    plugin
            );

            log("Registered event handler: " + eventClassName);

        } catch (Exception e) {
            log("Failed to register event " + eventClassName + ": " + e.getMessage());
            e.printStackTrace();
        }
    }

    /** Searches for an event class across all known Bukkit and Paper packages. */
    private Class<?> findEventClass(String eventClassName) {
        String[] packages = {
            "org.bukkit.event.player.",
            "org.bukkit.event.entity.",
            "org.bukkit.event.block.",
            "org.bukkit.event.world.",
            "org.bukkit.event.server.",
            "org.bukkit.event.inventory.",
            "org.bukkit.event.vehicle.",
            "org.bukkit.event.weather.",
            "org.bukkit.event.hanging.",
            "io.papermc.paper.event.player.",
            "io.papermc.paper.event.entity.",
            "io.papermc.paper.event.block.",
            "io.papermc.paper.event.world.",
            "io.papermc.paper.event.server.",
            "com.destroystokyo.paper.event.player.",
            "com.destroystokyo.paper.event.entity.",
            "com.destroystokyo.paper.event.server."
        };

        for (String pkg : packages) {
            try {
                return Class.forName(pkg + eventClassName);
            } catch (ClassNotFoundException ignored) {
            }
        }

        // Try fully-qualified name as fallback
        try {
            return Class.forName(eventClassName);
        } catch (ClassNotFoundException ignored) {
        }

        return null;
    }
    
    private final java.util.Map<String, Object> eventHandlers = new java.util.HashMap<>();

    public void storeEventHandler(String name, Object handler) {
        eventHandlers.put(name, handler);
    }

    public Object getEventHandler(String name) {
        return eventHandlers.get(name);
    }
    
    // === Safe Bukkit API Wrappers (Thread-Safe) ===
    
    /**
     * Safely add potion effect to player (thread-safe).
     * Usage from JS: API.addPotionEffectSync(player, effectType, duration, amplifier, ambient, particles);
     */
    public void addPotionEffectSync(org.bukkit.entity.Player player, org.bukkit.potion.PotionEffectType effectType, int duration, int amplifier, boolean ambient, boolean particles) {
        if (plugin.getServer().isPrimaryThread()) {
            // Already on main thread
            player.addPotionEffect(new org.bukkit.potion.PotionEffect(effectType, duration, amplifier, ambient, particles));
        } else {
            // Schedule on main thread
            plugin.getServer().getScheduler().runTask(plugin, () -> {
                player.addPotionEffect(new org.bukkit.potion.PotionEffect(effectType, duration, amplifier, ambient, particles));
            });
        }
    }
    
    /**
     * Safely strike lightning (thread-safe).
     * Usage from JS: API.strikeLightningSync(location);
     */
    public void strikeLightningSync(org.bukkit.Location location) {
        if (plugin.getServer().isPrimaryThread()) {
            // Already on main thread
            location.getWorld().strikeLightning(location);
        } else {
            // Schedule on main thread
            plugin.getServer().getScheduler().runTask(plugin, () -> {
                location.getWorld().strikeLightning(location);
            });
        }
    }
    
    /**
     * Safely remove potion effect (thread-safe).
     * Usage from JS: API.removePotionEffectSync(player, effectType);
     */
    public void removePotionEffectSync(org.bukkit.entity.Player player, org.bukkit.potion.PotionEffectType effectType) {
        if (plugin.getServer().isPrimaryThread()) {
            // Already on main thread
            player.removePotionEffect(effectType);
        } else {
            // Schedule on main thread
            plugin.getServer().getScheduler().runTask(plugin, () -> {
                player.removePotionEffect(effectType);
            });
        }
    }
}