feat: 允许使用JavaScript监听事件

This commit is contained in:
2024-08-08 07:21:50 +08:00
parent f2ed45b524
commit 41146a74be
9 changed files with 379 additions and 190 deletions

View File

@@ -2,11 +2,10 @@
plugins {
id("java")
id("com.github.johnrengelman.shadow") version "8+"
}
group = "cn.hamster3.mc.plugin"
version = "1.0.6"
version = "1.1.0"
description = "为Minecraft服务器导入 Nashorn 引擎来执行 JavaScript 脚本"
repositories {
@@ -23,9 +22,6 @@ dependencies {
annotationProcessor("org.projectlombok:lombok:1.18.30")
compileOnly("org.spigotmc:spigot-api:1.18.2-R0.1-SNAPSHOT")
// https://mvnrepository.com/artifact/org.openjdk.nashorn/nashorn-core
implementation("org.openjdk.nashorn:nashorn-core:15.4")
}
tasks {
@@ -43,9 +39,6 @@ tasks {
jar {
destinationDirectory = rootProject.layout.buildDirectory
}
shadowJar {
destinationDirectory = rootProject.layout.buildDirectory
}
withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
}
@@ -54,7 +47,4 @@ tasks {
from(rootProject.file("LICENSE"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
build {
dependsOn(shadowJar)
}
}

View File

@@ -1,208 +1,55 @@
package cn.hamster3.mc.plugin.script;
import cn.hamster3.mc.plugin.script.command.ScriptCommand;
import cn.hamster3.mc.plugin.script.core.ScriptManager;
import cn.hamster3.mc.plugin.script.listener.MainListener;
import lombok.Getter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.List;
@SuppressWarnings("CallToPrintStackTrace")
public class HamsterScriptPlugin extends JavaPlugin {
private static File codeFolder;
@Getter
private static ScriptEngine scriptEngine;
@Getter
private static Invocable invocable;
private static HamsterScriptPlugin instance;
private boolean enableEvalCommand;
private Map<String, String> importClass;
@Override
public void onLoad() {
instance = this;
}
@Override
public void onEnable() {
reload();
}
private void reload() {
saveDefaultConfig();
reloadConfig();
FileConfiguration config = getConfig();
enableEvalCommand = config.getBoolean("enable-eval-command", false);
scriptEngine = new ScriptEngineManager(getClassLoader()).getEngineByName("JavaScript");
invocable = (Invocable) scriptEngine;
Logger logger = getLogger();
importClass = new HashMap<>();
ConfigurationSection importConfig = config.getConfigurationSection("import");
if (importConfig != null) {
for (String simpleName : importConfig.getKeys(false)) {
String className = importConfig.getString(simpleName);
importClass.put(simpleName, className);
try {
Class<?> clazz = Class.forName(className);
scriptEngine.put(simpleName, clazz);
scriptEngine.eval(String.format("%s = %s.static;", simpleName, simpleName));
logger.info("已导入 " + className);
} catch (ClassNotFoundException e) {
logger.log(Level.WARNING, "导入 " + className + " 失败:未找到这个类", e);
} catch (Exception e) {
logger.log(Level.WARNING, "导入 " + className + " 失败", e);
}
ScriptManager.init();
for (HandlerList list : HandlerList.getHandlerLists()) {
list.register(new RegisteredListener(
MainListener.INSTANCE,
(listener, event) -> MainListener.INSTANCE.onEvent(event),
EventPriority.NORMAL,
this,
false
));
}
}
codeFolder = new File(getDataFolder(), "code");
if (codeFolder.mkdirs()) {
getLogger().info("创建代码文件夹: " + codeFolder.getAbsolutePath());
try {
Files.copy(
Objects.requireNonNull(getResource("code/example.js")),
new File(codeFolder, "example.js").toPath(),
StandardCopyOption.REPLACE_EXISTING
);
} catch (IOException ignored) {
}
}
public ClassLoader getPluginClassLoader() {
return getClassLoader();
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("hamster.script.admin")) {
sender.sendMessage("§c你没有这个权限");
return true;
}
if (args.length < 1) {
sender.sendMessage("§c请输入命令");
return true;
}
switch (args[0]) {
case "run": {
if (args.length < 2) {
sender.sendMessage("§c请输入 JavaScript 文件名称");
return true;
}
File file = new File(codeFolder, args[1]);
if (!file.exists() && !args[1].endsWith(".js")) {
file = new File(codeFolder, args[1] + ".js");
}
if (!file.exists()) {
sender.sendMessage("§c未找到 JavaScript 文件: " + file.getAbsolutePath());
return true;
}
String code;
try {
List<String> lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
code = String.join("\n", lines);
} catch (IOException e) {
sender.sendMessage("§c读取 JavaScript 文件内容 " + file.getAbsolutePath() + " 时出错!");
e.printStackTrace();
return true;
}
long start = System.currentTimeMillis();
try {
Bindings bindings = scriptEngine.createBindings();
for (String simpleName : importClass.keySet()) {
bindings.put(simpleName, scriptEngine.get(simpleName));
}
String[] scriptArgs = Arrays.copyOfRange(args, 2, args.length);
bindings.put("sender", sender);
bindings.put("args", scriptArgs);
Object eval = scriptEngine.eval(code, bindings);
long time = System.currentTimeMillis() - start;
sender.sendMessage("§aJavaScript 代码执行完成, 耗时: " + time + " 毫秒");
sender.sendMessage("§a返回值: §f" + eval);
} catch (Exception e) {
long time = System.currentTimeMillis() - start;
sender.sendMessage("§cJavaScript 代码执行出错, 耗时: " + time + " 毫秒");
sender.sendMessage("§c异常原因: " + e.getMessage());
e.printStackTrace();
}
break;
}
case "eval": {
if (!enableEvalCommand) {
sender.sendMessage("§c当前不允许直接执行 JavaScript");
return true;
}
if (args.length < 2) {
sender.sendMessage("§c请输入 JavaScript 代码内容");
return true;
}
StringBuilder builder = new StringBuilder();
for (int i = 1; i < args.length; i++) {
builder.append(args[i]).append(" ");
}
String code = builder.toString();
long start = System.currentTimeMillis();
try {
Bindings bindings = scriptEngine.createBindings();
for (String simpleName : importClass.keySet()) {
bindings.put(simpleName, scriptEngine.get(simpleName));
}
bindings.put("sender", sender);
Object eval = scriptEngine.eval(code, bindings);
long time = System.currentTimeMillis() - start;
sender.sendMessage("§aJavaScript 代码执行完成, 耗时: " + time + " 毫秒");
sender.sendMessage("§a返回值: §f" + eval);
} catch (Exception e) {
long time = System.currentTimeMillis() - start;
sender.sendMessage("§cJavaScript 代码执行出错, 耗时: " + time + " 毫秒");
sender.sendMessage("§c异常原因: " + e.getMessage());
e.printStackTrace();
}
break;
}
case "reload": {
reload();
sender.sendMessage("§a插件重载完成");
break;
}
default: {
sender.sendMessage("§c未找到该命令");
break;
}
}
return true;
return ScriptCommand.INSTANCE.onCommand(sender, command, label, args);
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (args.length == 1) {
List<String> list = new ArrayList<>();
list.add("run");
list.add("eval");
list.add("reload");
String startWith = args[0].toLowerCase();
list.removeIf(o -> !o.startsWith(startWith));
return list;
}
if (args[0].equalsIgnoreCase("run")) {
File[] files = codeFolder.listFiles();
if (files != null) {
String startWith = args[1].toLowerCase();
return Arrays.stream(files)
.map(File::getName)
.filter(o -> o.toLowerCase().startsWith(startWith))
.collect(Collectors.toList());
}
}
return Collections.emptyList();
return ScriptCommand.INSTANCE.onTabComplete(sender, command, alias, args);
}
}

View File

@@ -0,0 +1,119 @@
package cn.hamster3.mc.plugin.script.command;
import cn.hamster3.mc.plugin.script.HamsterScriptPlugin;
import cn.hamster3.mc.plugin.script.core.ScriptManager;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class ScriptCommand implements TabExecutor {
public static final ScriptCommand INSTANCE = new ScriptCommand();
private ScriptCommand() {
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!sender.hasPermission("hamster.script.admin")) {
sender.sendMessage("§c你没有这个权限");
return true;
}
if (args.length < 1) {
sender.sendMessage("§c请输入命令");
return true;
}
switch (args[0]) {
case "run": {
if (args.length < 2) {
sender.sendMessage("§c请输入 JavaScript 文件名称");
return true;
}
File file = new File(ScriptManager.getCodeFolder(), args[1]);
if (!file.exists() && !args[1].endsWith(".js")) {
file = new File(ScriptManager.getCodeFolder(), args[1] + ".js");
}
if (!file.exists()) {
sender.sendMessage("§c未找到 JavaScript 文件: " + file.getAbsolutePath());
return true;
}
String code;
try {
List<String> lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
code = String.join("\n", lines);
} catch (IOException e) {
sender.sendMessage("§c读取 JavaScript 文件内容 " + file.getAbsolutePath() + " 时出错!");
HamsterScriptPlugin.getInstance().getLogger().log(Level.SEVERE, "", e);
return true;
}
String[] scriptArgs = Arrays.copyOfRange(args, 2, args.length);
ScriptManager.eval(sender, code, scriptArgs);
break;
}
case "eval": {
if (!ScriptManager.isEnableEvalCommand()) {
sender.sendMessage("§c当前不允许直接执行 JavaScript");
return true;
}
if (args.length < 2) {
sender.sendMessage("§c请输入 JavaScript 代码内容");
return true;
}
StringBuilder builder = new StringBuilder();
for (int i = 1; i < args.length; i++) {
builder.append(args[i]).append(" ");
}
String code = builder.toString();
ScriptManager.eval(sender, code);
break;
}
case "reload": {
ScriptManager.init();
sender.sendMessage("§a插件重载完成");
break;
}
default: {
sender.sendMessage("§c未找到该命令");
break;
}
}
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length == 1) {
ArrayList<String> list = new ArrayList<>();
list.add("run");
list.add("eval");
list.add("reload");
String startWith = args[0].toLowerCase();
list.removeIf(o -> !o.toLowerCase().startsWith(startWith));
return list;
}
if (args[0].equalsIgnoreCase("run")) {
File[] files = ScriptManager.getCodeFolder().listFiles();
if (files != null) {
String startWith = args[1].toLowerCase();
return Arrays.stream(files)
.map(File::getName)
.filter(o -> o.toLowerCase().startsWith(startWith))
.collect(Collectors.toList());
}
}
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,148 @@
package cn.hamster3.mc.plugin.script.core;
import cn.hamster3.mc.plugin.script.HamsterScriptPlugin;
import cn.hamster3.mc.plugin.script.data.EventCode;
import lombok.Getter;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
import javax.script.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ScriptManager {
@Getter
private static File codeFolder;
@Getter
private static ScriptEngine scriptEngine;
@Getter
private static Invocable invocable;
@Getter
private static boolean enableEvalCommand;
@Getter
private static boolean enableEventCode;
@Getter
private static Map<String, String> importClass;
@Getter
private static List<EventCode> eventCodes;
public static void init() {
HamsterScriptPlugin plugin = HamsterScriptPlugin.getInstance();
plugin.saveDefaultConfig();
plugin.reloadConfig();
FileConfiguration config = plugin.getConfig();
enableEvalCommand = config.getBoolean("enable-eval-command", false);
enableEventCode = config.getBoolean("enable-event-code", false);
scriptEngine = new ScriptEngineManager(plugin.getPluginClassLoader()).getEngineByName("JavaScript");
invocable = (Invocable) scriptEngine;
Logger logger = plugin.getLogger();
importClass = new HashMap<>();
ConfigurationSection importConfig = config.getConfigurationSection("import");
if (importConfig != null) {
for (String simpleName : importConfig.getKeys(false)) {
String className = importConfig.getString(simpleName);
importClass.put(simpleName, className);
try {
Class<?> clazz = Class.forName(className);
scriptEngine.put(simpleName, clazz);
scriptEngine.eval(String.format("%s = %s.static;", simpleName, simpleName));
logger.info("已导入 " + className);
} catch (ClassNotFoundException e) {
logger.log(Level.WARNING, "导入 " + className + " 失败:未找到这个类", e);
} catch (Exception e) {
logger.log(Level.WARNING, "导入 " + className + " 失败", e);
}
}
}
codeFolder = new File(plugin.getDataFolder(), "code");
if (codeFolder.mkdirs()) {
plugin.getLogger().info("创建代码文件夹: " + codeFolder.getAbsolutePath());
try {
Files.copy(
Objects.requireNonNull(plugin.getResource("code/example.js")),
new File(codeFolder, "example.js").toPath()
);
} catch (IOException e) {
HamsterScriptPlugin.getInstance().getLogger().log(Level.SEVERE, "", e);
}
}
File eventFolder = new File(plugin.getDataFolder(), "event");
if (eventFolder.mkdirs()) {
plugin.getLogger().info("创建事件文件夹: " + codeFolder.getAbsolutePath());
try {
Files.copy(
Objects.requireNonNull(plugin.getResource("event/example.js")),
new File(eventFolder, "example.js").toPath()
);
} catch (IOException e) {
HamsterScriptPlugin.getInstance().getLogger().log(Level.SEVERE, "", e);
}
}
eventCodes = loadEventCodes(eventFolder);
}
private static List<EventCode> loadEventCodes(@NotNull File file) {
ArrayList<EventCode> list = new ArrayList<>();
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null) {
for (File subFile : files) {
List<EventCode> loaded = loadEventCodes(subFile);
list.addAll(loaded);
}
}
return list;
}
try {
EventCode code = new EventCode(file);
list.add(code);
} catch (Exception e) {
HamsterScriptPlugin.getInstance().getLogger().log(Level.WARNING, "加载事件代码 " + file.getAbsolutePath() + " 失败", e);
}
return list;
}
public static Bindings createBindings() {
Bindings bindings = ScriptManager.getScriptEngine().createBindings();
for (String simpleName : importClass.keySet()) {
bindings.put(simpleName, ScriptManager.getScriptEngine().get(simpleName));
}
return bindings;
}
public static Object eval(@NotNull String code) throws ScriptException {
Bindings bindings = createBindings();
return ScriptManager.getScriptEngine().eval(code, bindings);
}
public static void eval(@NotNull CommandSender sender, @NotNull String code, @NotNull String... args) {
long start = System.currentTimeMillis();
try {
Bindings bindings = createBindings();
bindings.put("sender", sender);
bindings.put("args", args);
Object eval = ScriptManager.getScriptEngine().eval(code, bindings);
long time = System.currentTimeMillis() - start;
sender.sendMessage("§aJavaScript 代码执行完成, 耗时: " + time + " 毫秒");
sender.sendMessage("§a返回值: §f" + eval);
} catch (Exception e) {
long time = System.currentTimeMillis() - start;
sender.sendMessage("§cJavaScript 代码执行出错, 耗时: " + time + " 毫秒");
sender.sendMessage("§c异常原因: " + e.getMessage());
HamsterScriptPlugin.getInstance().getLogger().log(Level.SEVERE, "", e);
}
}
}

View File

@@ -0,0 +1,33 @@
package cn.hamster3.mc.plugin.script.data;
import cn.hamster3.mc.plugin.script.core.ScriptManager;
import lombok.Getter;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
import javax.script.ScriptException;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@Getter
public class EventCode {
@NotNull
private final String id;
@NotNull
private final Class<?> clazz;
@NotNull
private final Object object;
public EventCode(@NotNull File file) throws IOException, ScriptException, NoSuchMethodException, ClassNotFoundException {
String code = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
object = ScriptManager.eval(code);
id = ScriptManager.getInvocable().invokeMethod(object, "getID").toString();
clazz = Class.forName(ScriptManager.getInvocable().invokeMethod(object, "getEventType").toString());
}
public void invoke(@NotNull Event event) throws ScriptException, NoSuchMethodException {
ScriptManager.getInvocable().invokeMethod(object, "invoke", event);
}
}

View File

@@ -0,0 +1,33 @@
package cn.hamster3.mc.plugin.script.listener;
import cn.hamster3.mc.plugin.script.HamsterScriptPlugin;
import cn.hamster3.mc.plugin.script.core.ScriptManager;
import cn.hamster3.mc.plugin.script.data.EventCode;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.util.logging.Level;
public class MainListener implements Listener {
public static final MainListener INSTANCE = new MainListener();
private MainListener() {
}
@EventHandler(ignoreCancelled = true)
public void onEvent(Event event) {
if (!ScriptManager.isEnableEventCode()) {
return;
}
for (EventCode code : ScriptManager.getEventCodes()) {
if (code.getClazz().isAssignableFrom(event.getClass())) {
try {
code.invoke(event);
} catch (Exception e) {
HamsterScriptPlugin.getInstance().getLogger().log(Level.WARNING, "处理事件代码 " + code.getId() + " 失败", e);
}
}
}
}
}

View File

@@ -1,6 +1,9 @@
# 是否允许通过指令执行 JavaScript 代码
enable-eval-command: false
# 是否启用事件监听执行代码
enable-event-code: false
# 导入的 Java 代码
import:
Math: java.lang.Math

View File

@@ -0,0 +1,13 @@
(function () {
return {
getID: function () {
return "test";
},
getEventType: function () {
return "org.bukkit.event.player.AsyncPlayerChatEvent";
},
invoke: function (event) {
print(event.getPlayer().getName() + " says: " + event.getMessage());
},
};
})();

View File

@@ -18,6 +18,9 @@ Plugin:
# HamsterScript 需要访问其他插件的类路径
join-classpath: true
libraries:
- org.openjdk.nashorn:nashorn-core:15.4
commands:
hamster-script:
aliases: [ hscript, scripts, script ]