Skip to content

创建插件

本章节将指导你如何创建一个完整的 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 插件运行在一个隔离的沙箱环境中,这意味着:

  1. 无需 require:所有 plugin_* 函数已自动注入,可直接使用
  2. 受限访问:插件无法直接访问MSL运行时数据
  3. 错误隔离:插件崩溃基本不会导致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);

下一步