创建插件
本章节将指导你如何创建一个完整的 MSL 插件,包括插件的基本结构、API 使用方法以及最佳实践。
插件基础
前置插件
对于前置组件以及公用库的开发,MSL建议不要直接编写通用MSL插件代码,而是自行开发一个node.js模块(并按需上传至npm或github中)。若其他插件需要使用该模块,则直接通过plugin_require即可引入。若要使用一定的api接口,可让插件传参。
MSL 插件是一个放置在 plugins 目录下的 JavaScript 文件,以 .js 为扩展名,文件名即为插件名。插件在 MSL 启动时会自动加载,并在一个隔离的沙箱环境中运行。
若要了解更详细的API,请查阅API参考。
创建你的第一个插件
在 plugins 目录下创建一个名为 my-first-plugin.js 的文件:
javascript
plugin_onEvent("playerJoin", (time, player) => {
plugin_log("INFO", `玩家 ${player} 在 ${time} 加入了服务器`);
plugin_executeCommand(`say 欢迎来到服务器,${player}!`);
});保存文件后,使用 msl reload 命令重载插件,你的插件就会生效了。
插件沙箱环境
MSL 插件运行在一个隔离的沙箱环境中,这意味着:
- 无需 require:所有
plugin_*函数已自动注入,可直接使用 - 受限访问:插件无法直接访问MSL运行时数据
- 错误隔离:插件崩溃基本不会导致MSL崩溃,即使MSL崩溃,MC服务器自身也不会退出运行。
可用的全局函数
以下是插件中可直接使用的全局函数:
关于日志
直接使用console.log等同于plugin_log("INFO", ...)
| 函数 | 说明 |
|---|---|
plugin_require(moduleName) | 引入 Node.js 模块 |
plugin_executeCommand(command, fn?) | 执行 Minecraft 指令 |
plugin_startServer() | 启动 Minecraft 服务器 |
plugin_forceStopServer() | 强制停止服务器进程 |
plugin_registerCommand(expr, fn) | 注册自定义指令 |
plugin_onEvent(event, fn) | 监听事件 |
plugin_triggerEvent(event, ...args) | 触发自定义事件 |
plugin_log(type, message) | 输出日志 |
plugin_generateOfflineUUID(name) | 生成离线玩家 UUID |
plugin_registerApi(method, path, fn) | 注册 HTTP API |
plugin_push(key, value) | 存储全局数据 |
plugin_pull(key) | 获取全局数据 |
plugin_getPluginsList() | 获取插件列表 |
完整示例插件
下面是一个功能完整的示例插件,展示了大部分 API 的使用方法:
javascript
/**
* 示例插件 - 展示 MSL 插件 API 的使用方法
*/
// 插件加载时执行
plugin_log("INFO", "示例插件正在加载...");
// ========== 事件监听 ==========
// 服务器启动
plugin_onEvent("serverStart", () => {
plugin_log("INFO", "服务器进程已启动");
});
// 服务器停止
plugin_onEvent("serverStop", () => {
plugin_log("INFO", "服务器进程已停止");
});
// 服务器启动完成
plugin_onEvent("serverDone", () => {
plugin_log("INFO", "服务器启动完成!");
// 执行初始化指令
plugin_executeCommand("say 服务器已就绪");
});
// 玩家加入
plugin_onEvent("playerJoin", (time, player) => {
plugin_log("INFO", `玩家 ${player} 加入了游戏`);
// 发送欢迎消息
plugin_executeCommand(`tellraw ${player} {"text":"欢迎来到服务器!","color":"green"}`);
});
// 玩家退出
plugin_onEvent("playerQuit", (time, player) => {
plugin_log("INFO", `玩家 ${player} 离开了游戏`);
});
// 玩家聊天
plugin_onEvent("playerSendMessage", (time, player, message) => {
plugin_log("INFO", `[聊天] ${player}: ${message}`);
});
// 玩家执行指令
plugin_onEvent("playerSendCommand", (time, player, command, args) => {
plugin_log("INFO", `[指令] ${player} 执行了 /${command} ${args.join(" ")}`);
});
// ========== 自定义指令 ==========
// 注册 !ping 指令
plugin_registerCommand("!ping", (player) => {
plugin_executeCommand(`say ${player} 请求了 ping!`);
plugin_log("INFO", `${player} 使用了 !ping 指令`);
});
// 注册带参数的指令 !kick <player>
plugin_registerCommand("!kick <player>", (player, target) => {
plugin_executeCommand(`kick ${target} 被 ${player} 踢出`);
plugin_log("INFO", `${player} 踢出了 ${target}`);
});
// 注册 !online 指令(带响应捕获)
plugin_registerCommand("!online", (player) => {
plugin_executeCommand("list", (lines) => {
lines.forEach(line => {
if (line.includes("players online")) {
plugin_executeCommand(`tellraw ${player} {"text":"${line}","color":"yellow"}`);
}
});
});
});
// ========== HTTP API ==========
// 注册 GET /api/hello 接口
plugin_registerApi("GET", "/api/hello", (req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
success: true,
message: "Hello from MSL Plugin!"
}));
});
// 注册 POST /api/broadcast 接口
plugin_registerApi("POST", "/api/broadcast", (req, res) => {
let body = "";
req.on("data", chunk => body += chunk);
req.on("end", () => {
try {
const data = JSON.parse(body);
if (data.message) {
plugin_executeCommand(`say ${data.message}`);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true }));
} else {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: "Missing message" }));
}
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: false, error: "Invalid JSON" }));
}
});
});
// ========== 全局数据存储 ==========
// 存储插件数据
plugin_push("myPluginData", {
version: "1.0.0",
startTime: new Date().toISOString()
});
// 读取插件数据
const data = plugin_pull("myPluginData");
plugin_log("INFO", `插件数据: ${JSON.stringify(data)}`);
// ========== 自定义事件 ==========
// 触发自定义事件
plugin_triggerEvent("myPluginLoaded", "1.0.0");
// 监听自定义事件(可在其他插件中监听)
plugin_onEvent("myPluginLoaded", (version) => {
plugin_log("INFO", `插件已加载,版本: ${version}`);
});
// ========== 插件列表 ==========
const plugins = plugin_getPluginsList();
plugin_log("INFO", `已加载插件: ${plugins.loaded.join(", ")}`);
plugin_log("INFO", "示例插件加载完成!");最佳实践
1. 防止注入
对于支持用户输入参数的指令,请确保对用户输入的内容检查,避免用户使用转义符等黑入服务器。
2. 错误处理
始终在回调函数中添加错误处理:
javascript
plugin_onEvent("playerJoin", (time, player) => {
try {
// 你的代码
} catch (error) {
plugin_log("ERROR", `处理玩家加入事件时出错: ${error.message}`);
}
});3. 日志规范
使用适当的日志级别:
javascript
plugin_log("INFO", "普通信息"); // 常规操作日志
plugin_log("WARN", "警告信息"); // 潜在问题
plugin_log("ERROR", "错误信息"); // 错误情况4. 指令命名
使用有意义的前缀避免冲突:
javascript
// 推荐:使用插件名作为前缀
plugin_registerCommand("!myplugin help", ...);
plugin_registerCommand("!myplugin reload", ...);
// 不推荐:通用名称可能与其他插件冲突
plugin_registerCommand("!help", ...);5. 数据存储
使用唯一的键名存储数据:
javascript
// 推荐:使用插件名作为前缀
plugin_push("myPlugin_config", configData);
plugin_push("myPlugin_cache", cacheData);