diff --git a/src/main/java/pw/yumc/MiaoScript/BaseEvent.java b/src/main/java/pw/yumc/MiaoScript/BaseEvent.java new file mode 100644 index 0000000..9514714 --- /dev/null +++ b/src/main/java/pw/yumc/MiaoScript/BaseEvent.java @@ -0,0 +1,26 @@ +package pw.yumc.MiaoScript; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Created with IntelliJ IDEA + * + * @author 喵♂呜 + * Created on 2017/9/22 18:39. + */ +public class BaseEvent extends Event { + private static HandlerList handlerList = new HandlerList(); + + public BaseEvent() { + } + + public static HandlerList getHandlerList() { + return handlerList; + } + + @Override + public HandlerList getHandlers() { + return handlerList; + } +} diff --git a/src/main/java/pw/yumc/MiaoScript/MiaoScript.java b/src/main/java/pw/yumc/MiaoScript/MiaoScript.java index 3d0a707..8d49de7 100644 --- a/src/main/java/pw/yumc/MiaoScript/MiaoScript.java +++ b/src/main/java/pw/yumc/MiaoScript/MiaoScript.java @@ -1,12 +1,12 @@ package pw.yumc.MiaoScript; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Files; import javax.script.ScriptEngineManager; +import javax.script.ScriptException; import org.bukkit.plugin.java.JavaPlugin; @@ -32,8 +32,18 @@ public class MiaoScript extends JavaPlugin { loadEngine(); } + @Override + public void onDisable() { + try { + engine.invokeFunction("disable"); + } catch (ScriptException | NoSuchMethodException e) { + Log.w("脚本引擎关闭失败! %s:%s", e.getClass().getName(), e.getMessage()); + Log.d(e); + } + } + private void saveScript() { - P.saveFile("modules"); + P.saveFile(true, "core", "modules", "plugins"); } private void loadEngine() { @@ -45,9 +55,10 @@ public class MiaoScript extends JavaPlugin { this.engine = new MiaoScriptEngine(manager); this.engine.put("base", new Base()); this.engine.eval(new InputStreamReader(this.getResource("bios.js"))); - engine.invokeFunction("boot", this, engine); + engine.invokeFunction("boot", this); } catch (Exception e) { Log.w("脚本引擎初始化失败! %s:%s", e.getClass().getName(), e.getMessage()); + Log.d(e); } finally { currentThread.setContextClassLoader(previousClassLoader); } @@ -70,14 +81,7 @@ public class MiaoScript extends JavaPlugin { public void save(String path, String content) throws IOException { Log.d("保存文件 %s ...", path); - File file = new File(path); - if (!file.exists()) { - file.getParentFile().mkdirs(); - file.createNewFile(); - } - FileOutputStream fos = new FileOutputStream(file); - fos.write(content.getBytes("UTF-8")); - fos.close(); + Files.write(new File(path).toPath(), content.getBytes("UTF-8")); } public Class getActionBar() { diff --git a/src/main/resources/bios.js b/src/main/resources/bios.js index e552f12..51ae079 100644 --- a/src/main/resources/bios.js +++ b/src/main/resources/bios.js @@ -1,12 +1,19 @@ +'use strict'; var boot; +var disable; /** * 初始化框架引擎 */ (function () { - boot = function (plugin, engine) { - engine.put('root', plugin.getDataFolder()); - engine.put('rootDir', plugin.getDataFolder().getCanonicalPath()); - load(rootDir + '/modules/init.js'); - init(plugin, engine); - } + boot = function (plugin) { + // 开发环境下初始化 + var root = "src/main/resources"; + if (plugin !== null) { + // noinspection JSUnresolvedVariable + root = plugin.dataFolder.canonicalPath; + } + load(root + '/core/init.js'); + init(root, plugin); + disable = disablePlugins + }; })(); \ No newline at end of file diff --git a/src/main/resources/modules/ext.js b/src/main/resources/core/ext.js similarity index 100% rename from src/main/resources/modules/ext.js rename to src/main/resources/core/ext.js diff --git a/src/main/resources/core/fs.js b/src/main/resources/core/fs.js new file mode 100644 index 0000000..83cd28b --- /dev/null +++ b/src/main/resources/core/fs.js @@ -0,0 +1,95 @@ +'use strict'; + +/*global Java, base, module, exports, require, __FILE__*/ +var String = Java.type("java.lang.String"); +var File = Java.type("java.io.File"); +var Files = Java.type("java.nio.file.Files"); +var StandardCopyOption = Java.type("java.nio.file.StandardCopyOption"); + +/** + * 获得文件 + * @constructor(file) + * @constructor(dir,file) + * @returns {*} + */ +exports.file = function () { + if (exports.canonical(arguments[0])) { + return arguments[0]; + } + switch (arguments.length) { + case 1: + return new File(arguments[0]); + case 2: + return new File(exports.file(arguments[0]), arguments[1]); + } +}; +/** + * 创建目录 + * @param file + */ +exports.mkdirs = function (file) { + file.getParentFile().mkdirs(); +}; +/** + * 创建文件 + * @param file + */ +exports.create = function (file) { + file = exports.file(file); + if (!file.exists()) { + exports.mkdirs(file); + file.createNewFile(); + } +}; +/** + * 获得文件规范路径 + * @param file + * @returns {*} + */ +exports.canonical = function (file) { + // noinspection JSUnresolvedVariable + return file.canonicalPath; +}; +/** + * 复制文件 + * @param inputStream 输入流 + * @param target 目标文件 + * @param override 是否覆盖 + */ +exports.copy = function (inputStream, target, override) { + Files.copy(inputStream, target.toPath(), StandardCopyOption[override ? 'REPLACE_EXISTING' : 'ATOMIC_MOVE']); +}; +/** + * 读取文件 + * @param file 文件路径 + */ +exports.read = function (file) { + file = exports.file(file); + if (!file.exists()) { + log.w("读取文件 %s 错误 文件不存在!", file); + return; + } + // noinspection JSPrimitiveTypeWrapperUsage + return new String(Files.readAllBytes(file.toPath()), "UTF-8"); +}; +/** + * 保存内容文件 + * @param path 路径 + * @param content 内容 + * @param override 是否覆盖 + */ +exports.save = function (path, content, override) { + Files.write(new File(path).toPath(), content.getBytes("UTF-8"), StandardCopyOption[override ? 'REPLACE_EXISTING' : 'ATOMIC_MOVE']); +}; +/** + * 列出目录文件 + * @param path + */ +exports.list = function (path) { + var dir = exports.file(path); + if (dir.isDirectory()) { + return Files.list(dir.toPath()); + } + log.w("路径 %s 不是一个目录 返回空数组!"); + return []; +}; \ No newline at end of file diff --git a/src/main/resources/core/init.js b/src/main/resources/core/init.js new file mode 100644 index 0000000..7f76a8a --- /dev/null +++ b/src/main/resources/core/init.js @@ -0,0 +1,60 @@ +'use strict'; +var global = this; +/*global base*/ + +// noinspection JSUnusedLocalSymbols +function init(root, plugin) { + global.root = root; + initDir(); + loadCore(); + loadRequire(); + loadPlugins(plugin); +} + +/** + * 初始化目录 + */ +function initDir() { + // 核心目录 + global.core_dir = root + "/core"; + // 模块目录 + global.miao_module_dir = root + "/modules"; + // 插件目录 + global.plugins_dir = root + "/plugins"; +} + +/** + * 初始化核心 + */ +function loadCore() { + // 加载基础模块 + load(core_dir + '/ext.js'); + load(core_dir + '/static.js'); +} + +/** + * 初始化模块 + */ +function loadRequire() { + // 初始化加载器 + global.require = load(core_dir + '/require.js')(root, core_dir, miao_module_dir); +} + +/** + * 加载JS插件 + */ +function loadPlugins(plugin) { + // 初始化本体插件 + var self = require('modules/plugin'); + self.init(plugin, plugins_dir); + self.load(); + self.enable(); +} + +// noinspection JSUnusedLocalSymbols +/** + * 关闭插件Hook + */ +function disablePlugins() { + require('modules/plugin').disable(); +} \ No newline at end of file diff --git a/src/main/resources/core/require.js b/src/main/resources/core/require.js new file mode 100644 index 0000000..ff3db39 --- /dev/null +++ b/src/main/resources/core/require.js @@ -0,0 +1,143 @@ +/** + * 符合 CommonJS 规范的 模块化加载 + * + */ +/*global Java, base*/ +(function (parent, core_dir, miao_module_dir) { + 'use strict'; + var File = Java.type("java.io.File"); + var Files = Java.type("java.nio.file.Files"); + var String = Java.type("java.lang.String"); + + /** + * 解析模块名称为文件 + * 按照下列顺序查找 + * 当前目录 ./ + * 父目录 ../ + * 核心目录 /core + * 模块目录 /modules + * @param name 模块名称 + */ + function findModule(name) { + if (_canonical(name)) { + name = _canonical(name); + } + if (!name.match(/.*\.js/)) { + name += ".js"; + } + var jsFile = new File(name); + if (jsFile.exists()) { + return jsFile; + } + var parentFile = new File(parent, name); + if (parentFile.exists()) { + return parentFile; + } + var coreFile = new File(core_dir, name); + if (coreFile.exists()) { + return coreFile; + } + var moduleFile = new File(miao_module_dir, name); + if (moduleFile.exists()) { + return moduleFile; + } + log.w("模块 %s 加载失败! 下列目录中未找到该模块!", name); + log.w("当前目录: %s", _canonical(jsFile)); + log.w("上级目录: %s", _canonical(parentFile)); + log.w("核心目录: %s", _canonical(coreFile)); + log.w("模块目录: %s", _canonical(moduleFile)); + } + + /** + * 使用NIO读取文件内容 + * @param file 文件 + * @private + */ + function _readFile(file) { + // noinspection JSPrimitiveTypeWrapperUsage + return new String(Files.readAllBytes(file.toPath()), "UTF-8"); + } + + /** + * 预编译模块 + * @param src + * @returns {Object} + */ + function compileJs(src) { + var head = "(function (module, exports, require) {\n"; + var tail = "\n});"; + var fulljs = head + src + tail; + return eval(fulljs); + } + + /** + * 获得文件规范路径 + * @param file + * @returns {*} + * @private + */ + function _canonical(file) { + // noinspection JSUnresolvedVariable + return file.canonicalPath; + } + + /** + * 加载模块 + * @param name 模块名称 + * @param parent 父目录 + * @returns {*} + * @private + */ + function _require(name, parent) { + var file = findModule(name, parent); + // 重定向文件名称 + name = file.name.split(".")[0]; + var id = _canonical(file); + var module = cacheModules[id]; + if (module) { + return module; + } + log.d('加载模块 %s 位于 %s', name, id); + module = { + loaded: false, + id: id, + exports: {}, + require: exports(file.parentFile) + }; + var src = _readFile(file); + try { + // 预编译模块 + var compiledWrapper = compileJs(src); + compiledWrapper.apply(module.exports, [ + module, module.exports, module.require + ]); + } catch (ex) { + log.w("模块 %s 编译失败!", name); + log.w(ex); + } + log.d('模块 %s 编译成功!', name); + module.loaded = true; + cacheModules[id] = module; + return cacheModules[id]; + } + + /** + * 闭包方法 + * @param parent 父目录 + * @returns {Function} + */ + function exports(parent) { + return function (path) { + return _require(path, parent).exports; + }; + } + + // 等于 undefined 说明 parent 是一个字符串 需要转成File + // 可能更加准确的方案 + if (_canonical(parent) === undefined) { + parent = new File(parent); + } + var cacheModules = []; + log.d("初始化 require 模块组件 父目录 %s", _canonical(parent)); + return exports(parent); +}); \ No newline at end of file diff --git a/src/main/resources/modules/static.js b/src/main/resources/core/static.js similarity index 100% rename from src/main/resources/modules/static.js rename to src/main/resources/core/static.js diff --git a/src/main/resources/core/zip.js b/src/main/resources/core/zip.js new file mode 100644 index 0000000..ce3607d --- /dev/null +++ b/src/main/resources/core/zip.js @@ -0,0 +1,52 @@ +/** + * + */ +'use strict'; + +/*global Java, base, module, exports, require, __FILE__*/ + +var File = Java.type("java.io.File"); +var ZipFile = Java.type("java.util.zip.ZipFile"); +var fs = require('fs'); + +/** + * 获取文件真实名称 + * + * @param name + * 名称 + * @return string 文件名称 + */ +function getRealName(name) { + return new File(name).name; +} + +/** + * 解压文件 + * @param zipFile 压缩文件 + * @param target 目标目录(不传则为zip文件同级目录) + */ +function unzip(zipFile, target) { + if (!zipFile.exists()) { + log.w("解压文件 %s 错误 文件不存在!", zipFile); + return; + } + if (target === undefined) { + // noinspection JSUnresolvedVariable + target = new File(zipFile.parentFile.canonicalPath, zipFile.name.split(".")[0]); + } + log.d("解压文件 %s => %s", zipFile.canonicalPath, target); + var zipObj = new ZipFile(zipFile); + var e = zipObj.entries(); + while (e.hasMoreElements()) { + var entry = e.nextElement(); + if (entry.isDirectory()) { + continue; + } + var destinationFilePath = new File(target, getRealName(entry.name)); + destinationFilePath.getParentFile().mkdirs(); + fs.copy(zipObj.getInputStream(entry), destinationFilePath, true); + } + zipObj.close(); +} + +exports.unzip = unzip; \ No newline at end of file diff --git a/src/main/resources/modules/bukkit.js b/src/main/resources/modules/bukkit.js new file mode 100644 index 0000000..4ce5db4 --- /dev/null +++ b/src/main/resources/modules/bukkit.js @@ -0,0 +1,40 @@ +/** + * Bukkit基础操作 + * Created by 蒋天蓓 on 2017/2/9 0009. + */ +'use strict'; + +/*global Java, base, module, exports, require, __FILE__*/ +var Bukkit = Java.type("org.bukkit.Bukkit"); +exports.broadcast = function (message) { + Bukkit.broadcastMessage(message); +}; +/** + * 执行名称 + * @param player 玩家 + * @param command 命令 + */ +exports.command = function (player, command) { + Bukkit.dispatchCommand(player, command); +}; +/** + * 执行控制台命令 + * @param command 命令 + */ +exports.console = function (command) { + exports.command(Bukkit.getConsoleSender(), command); +}; +/** + * 玩家以OP权限执行命令 + * @param player + * @param exper + */ +exports.opcommand = function (player, exper) { + var origin = player.isOp(); + player.setOp(true); + try { + exports.command(player, exper); + } finally { + player.setOp(origin); + } +}; \ No newline at end of file diff --git a/src/main/resources/modules/event.js b/src/main/resources/modules/event.js new file mode 100644 index 0000000..5c876c9 --- /dev/null +++ b/src/main/resources/modules/event.js @@ -0,0 +1,118 @@ +'use strict'; + +/*global Java, base, module, exports, require, __FILE__*/ +var Thread = Java.type("java.lang.Thread"); +var Bukkit = Java.type("org.bukkit.Bukkit"); +var Listener = Java.type("org.bukkit.event.Listener"); +var Modifier = Java.type("java.lang.reflect.Modifier"); +var BukkitEvent = Java.type("org.bukkit.event.Event"); +var EventPriority = Java.type("org.bukkit.event.EventPriority"); +var EventExecutor = Java.type("org.bukkit.plugin.EventExecutor"); + +var mapEvent = []; + +var plugin = require('plugin').$; + +/** + * 映射事件名称 org.bukkit.event.player.PlayerLoginEvent => playerloginevent + */ +function mapEventName() { + var eventPackageDir = "org/bukkit/event"; + + var dirs = Thread.currentThread().getContextClassLoader().getResources(eventPackageDir); + while (dirs.hasMoreElements()) { + var url = dirs.nextElement(); + var protocol = url.protocol; + if (protocol === "jar") { + // noinspection JSUnresolvedVariable + var jar = url.openConnection().jarFile; + var entries = jar.entries(); + while (entries.hasMoreElements()) { + var entry = entries.nextElement(); + var name = entry.name; + if (name.startsWith(eventPackageDir) && name.endsWith(".class")) { + var i = name.replaceAll('/', '.'); + try { + var clz = base.getClass(i.substring(0, i.length - 6)); + if (isVaildEvent(clz)) { + // noinspection JSUnresolvedVariable + var simpleName = clz.simpleName.toLowerCase(); + log.d("Mapping Event [%s] => %s", clz.name, simpleName); + mapEvent[simpleName] = clz; + } + } catch (ex) { + //ignore already loaded class + } + } + } + } + } +} + +function isVaildEvent(clz) { + // noinspection JSUnresolvedVariable + return BukkitEvent.class.isAssignableFrom(clz) && Modifier.isPublic(clz.getModifiers()) && !Modifier.isAbstract(clz.getModifiers()); +} + +/** + * 添加事件监听 + * @param event + * @param exec {function} + * @param priority + * @param ignoreCancel + */ +function listen(event, exec, priority, ignoreCancel) { + var eventCls = mapEvent[event]; + if (!eventCls) { + try { + eventCls = base.getClass(eventCls); + } catch (ex) { + log.w("事件 %s 未找到!"); + } + return; + } + if (priority === undefined) { + priority = 'NORMAL' + } + if (ignoreCancel === undefined) { + ignoreCancel = false; + } + var listener = new Java.extend(Listener, {}); + // noinspection JSUnusedGlobalSymbols + /** + * @param event Event type to register + * @param listener Listener to register + * @param priority Priority to register this event at + * @param executor EventExecutor to register + * @param plugin Plugin to register + * @param ignoreCancel + */ + Bukkit.getPluginManager().registerEvent( + eventCls, + listener, + EventPriority[priority], + new Java.extend(EventExecutor, { + execute: function (listener, event) { + exec(event); + } + }), + plugin, + ignoreCancel); + return { + event: eventCls, + listener: listener + } +} + +// 映射事件名称 +mapEventName(); + +exports.on = listen; +/** + * 取消事件监听 + * @param listener 监听结果 + */ +exports.off = function (listener) { + // noinspection JSUnresolvedVariable + listener.event.handlerList.unregister(listener.listener); +}; \ No newline at end of file diff --git a/src/main/resources/modules/init.js b/src/main/resources/modules/init.js deleted file mode 100644 index 5f9ba86..0000000 --- a/src/main/resources/modules/init.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; -/*global require*/ -var global = this; -load(rootDir + '/modules/ext.js'); -load(rootDir + '/modules/static.js'); - -function init(plugin, engine) { - log.d("Version: %s", plugin.getDescription().getVersion()); -} \ No newline at end of file diff --git a/src/main/resources/modules/plugin.js b/src/main/resources/modules/plugin.js new file mode 100644 index 0000000..67be00c --- /dev/null +++ b/src/main/resources/modules/plugin.js @@ -0,0 +1,90 @@ +'use strict'; + +/*global Java, base, module, exports, require, __FILE__*/ +var zip = require("core/zip"); +var fs = require('core/fs'); + +/** + * 载入插件 + * @param path + */ +function loadPlugins(path) { + path = fs.file(path); + log.i("开始扫描 %s 下的插件...", path); + updatePlugins(); + var files = []; + fs.list(path).forEach(function (file) { + files.push(file.toFile()); + }); + loadZipPlugin(files); + loadJsPlugin(files); +} + +/** + * 更新插件 + * @param path + */ +function updatePlugins(path) { + var dir = fs.file(path, "update"); + fs.list(dir).forEach(function (file) { + + }) +} + +/** + * ZIP类型插件预加载 + * @param files + */ +function loadZipPlugin(files) { + // // TODO ZIP类型插件加载 + // files.filter(function (file) { + // return file.name.endsWith(".zip"); + // }).forEach(function (file) { + // zip.unzip(fs.file(plugins_dir, file)); + // var dir = new File(plugins_dir, file.name.split(".")[0]); + // // TODO 添加文件夹类型的插件兼容 + // }); +} + +/** + * JS类型插件预加载 + */ +function loadJsPlugin(files) { + files.filter(function (file) { + return file.name.endsWith(".js") + }).forEach(function (file) { + var p = require(file); + log.d(JSON.stringify(p)); + if (!p.description || !p.description.name) { + log.w("文件 %s 不存在 description 描述信息 无法加载插件!"); + } else { + exports.plugins.push(p); + } + }) +} + +exports.$ = undefined; +exports.plugins = []; +exports.init = function (plugin, path) { + if (plugin !== null) { + exports.$ = plugin; + log.i("Init MiaoScript Engine Version: %s", plugin.description.version); + require('./event'); + } + loadPlugins(path); +}; +exports.load = function () { + exports.plugins.forEach(function (p) { + p.load(); + }) +}; +exports.enable = function () { + exports.plugins.forEach(function (p) { + p.enable(); + }) +}; +exports.disable = function () { + exports.plugins.forEach(function (p) { + p.disable(); + }) +}; \ No newline at end of file diff --git a/src/main/resources/plugins/hello.js b/src/main/resources/plugins/hello.js new file mode 100644 index 0000000..9b02672 --- /dev/null +++ b/src/main/resources/plugins/hello.js @@ -0,0 +1,25 @@ +/** + * Hello Wrold 测试插件 + */ +'use strict'; + +var event = require('modules/event'); +var joinCancel; +/*global Java, base, module, exports, require*/ +exports.description = { + name: 'HelloWorld' +}; +exports.load = function () { + log.i('载入 Hello Wrold 测试插件!'); +}; +exports.enable = function () { + log.i('启用 Hello Wrold 测试插件!'); + joinCancel = event.on('playerloginevent', function (event) { + // noinspection JSUnresolvedVariable + event.player.sendMessage('§a欢迎来到 §bMiaoScript §a的世界!'); + }); +}; +exports.disable = function () { + log.i('卸载 Hello Wrold 测试插件!'); + event.off(joinCancel); +}; \ No newline at end of file diff --git a/src/main/test/java/pw/yumc/MiaoScript/MiaoScriptTest.java b/src/main/test/java/pw/yumc/MiaoScript/MiaoScriptTest.java index 58e89ef..94bef1a 100644 --- a/src/main/test/java/pw/yumc/MiaoScript/MiaoScriptTest.java +++ b/src/main/test/java/pw/yumc/MiaoScript/MiaoScriptTest.java @@ -26,10 +26,12 @@ public class MiaoScriptTest { try { ScriptEngineManager manager = new ScriptEngineManager(); this.engine = new MiaoScriptEngine(manager); + this.engine.put("base", new MiaoScript.Base()); this.engine.eval(new FileReader("src/main/resources/bios.js")); - engine.invokeFunction("boot", this, engine); + engine.invokeFunction("boot", null, engine); } catch (Exception e) { Log.w("脚本引擎初始化失败! %s:%s", e.getClass().getName(), e.getMessage()); + e.printStackTrace(); } finally { currentThread.setContextClassLoader(previousClassLoader); }