加载与生命周期
编辑器扫描插件文件夹以查找 plugin.json,只加载通过检查且处于启用状态的插件。插件代码中发生异常时,只禁用该插件,编辑器继续运行。这称为隔离。
- 扫描文件夹。 对每个插件文件夹,按名称顺序查看其下一级子文件夹。只有包含
plugin.json的文件夹才是插件。 - 检查 plugin.json。 字段有误时为「错误 (文件)」。
- 比较 SDK 版本。 编辑器的 SDK 版本不在
sdk范围内时为「版本不匹配」。 - 确认启用。 用户禁用的插件为「关闭」。
- 首次运行警告。 首次发现插件时会弹出一次警告窗口。点击「可信 — 加载」后会记住并加载。点击「本次不加载」则本次不加载任何内容(保持「等待」)。
- 创建加载上下文。 为每个插件单独创建。dll 以字节读取后加载,因此不会锁定文件。同一文件夹中有
.pdb时一并加载。 - 创建入口类。 用无参数构造函数创建
entry(没有时为第一个INpEditorPlugin实现)。找不到或无法创建该类时为「错误 (已隔离)」。 - 调用
Register(host)。 发生异常时不注册任何内容,状态为「错误 (已隔离)」。 - 挂接到编辑器。 将菜单 · 模式 · 面板 · 命令等挂接到编辑器。挂接过程中发生异常时为「错误 (已隔离)」。名称已存在的命令 · Actor 类型只有该项不会挂接。
- 已启用。 输出日志中记录
[插件 <id>] <版本> 已启用 — <注册摘要>。
启动编辑器时执行一次 110。插件管理器的「重新扫描」或控制台 10。已启用的插件不会重新加载。plugins reload 会重新执行 1
显示在插件管理器的「状态」列和控制台 plugins 的结果中。
| 状态 | 含义 | 处理 |
|---|---|---|
| 等待 | 已找到但尚未加载(确认警告前等) | 确认警告,或点击「重新扫描」 |
| 已启用 | 正常运行中 | — |
| 关闭 | 用户取消了「启用」勾选 | 重新勾选 |
| 错误 (文件) | plugin.json 有误 · 没有 dll · id 重复 | 根据下方详细行中的原因修改 |
| 版本不匹配 | sdk 范围不包含编辑器的 SDK;或使用了编辑器 SDK 中没有的成员而加载失败 | 修改 sdk 或更新编辑器 |
| 错误 (已隔离) | 无法创建入口类 · Register 或回调中发生异常 → 只卸载该插件 | 修正输出日志中的异常后点击「重新加载 (所选)」 |
通过「工具 › 插件管理器…」打开。
| 列 · 按钮 | 作用 |
|---|---|
| 启用 | 勾选则启用(加载),取消则卸载。状态保存在 plugins_state.json 中。 |
| 名称 · 版本 · 状态 | plugin.json 的 name · version 与当前状态。 |
| 注册 | 已启用时为注册摘要(例 메뉴 1 · 빠른 추가 1),否则为错误原因。 |
| 重新扫描 | 重新查看文件夹。查找新插件,并卸载已消失的文件夹。 |
| 重新加载 (所选) | 卸载所选插件,重新读取 plugin.json · dll 并启用。在构建之后使用。 |
| 打开文件夹 | 打开用户插件文件夹。 |
禁用 · 重新加载 · 卸载
Section titled “禁用 · 重新加载 · 卸载”以下情况会卸载插件:取消「启用」勾选 · 「重新加载 (所选)」 · 删除文件夹后「重新扫描」 · 错误隔离。关闭编辑器时不经过卸载过程,进程直接结束。因此不要把保存之类必须执行的操作放在 Dispose 中。
- 移除编辑器中注册的所有内容(菜单 · 模式 · 面板 · 命令 · 快捷键 · Actor 类型 · 格守卫 · 对称)。若正在使用该插件的模式,则返回方块笔刷模式。
- 若入口类为
IDisposable,则调用Dispose()。 - 从注册表中移除世界逻辑 type。使用该 type 的规则不会被删除,而是保留为「未知 type」警告。
- 若持有运行相机,则释放(恢复原视角)。
- 清除通过
host.Draw绘制的内容。 - 卸载加载上下文(collectible 卸载)。
host.Settings 的值保留在文件中,重新启用时原样读取。
Dispose 规则
Section titled “Dispose 规则”| 应做的事 | 原因 |
|---|---|
在 Dispose 中解除挂在静态事件 · 计时器 · 线程上的内容。 | 若残留,加载上下文将无法从内存中卸载。 |
在 Dispose 中停止通过 Task.Run 运行的任务(CancellationToken)。 | 禁用后不得再操作世界。 |
host.WorldChanged · Committed · View.PlayTicked 等宿主事件无需解除。 | 卸载时宿主会全部丢弃。 |
不要在 Dispose 中抛出异常。 | 会记录到输出日志,卸载继续进行。 |
public sealed class Plugin : INpEditorPlugin, IDisposable{ private readonly CancellationTokenSource _stop = new();
public void Register(INpEditorHost host) { // … 注册 … }
public void Dispose() => _stop.Cancel(); // 停止后台任务}- 所有回调(菜单 · 命令 · 事件 ·
NpUi·Simulate·PlayTicked)都在编辑器主线程上调用。 - 耗时的计算用
Task.Run移出。 World.Edit和Draw只在主线程(回调内)调用。
| 内容 | 位置 |
|---|---|
| 用户插件文件夹 | %APPDATA%\NPEditor\plugins\<文件夹>\(工具 › 打开插件文件夹) |
| 安装文件夹插件 | <安装文件夹>\plugins\<文件夹>\ |
| 额外扫描的文件夹 | 环境变量 NP_EDITOR_PLUGINS(多个用 ; 分隔) |
| 启用 · 禁用 · 警告确认 | %APPDATA%\NPEditor\plugins_state.json |
插件设置(host.Settings) | %APPDATA%\NPEditor\plugin_settings\<id>.json |
| 用户更改的快捷键 | %APPDATA%\NPEditor\shortcuts.json |
| 较大的自有数据 | host.PluginDir(= plugin.json 所在文件夹)下的自有文件 |
文件夹按上表顺序扫描。相同 id 使用最先找到的那个。
id 命名空间
Section titled “id 命名空间”| 注册的内容 | 编辑器内名称 | 重名时 |
|---|---|---|
| 菜单 · 快速添加 · 导入/导出 · 面板 · 模式 · 快捷键 | ext.<插件 id>.<我的 id> | 不会与其他插件 · 内置功能重名 |
快捷键 Mode | 直接写自己的模式 id,编辑器会加上 ext.<插件 id>. | — |
| 控制台命令名 | 原样(不含空格) | 已存在时只有该命令不会挂接(先存在者优先) |
| 世界逻辑 type | 原样(在同一角色内) | 已存在时(含内置)注册异常 → 该插件被隔离 |
Actor 类型 Kind | 原样 | 已存在时只有该类型不会挂接(先存在者优先) |
在命令名 · 世界逻辑 type · Actor kind 前加上自己的名称(例 road_build、myquest_start、myroad.zone)。
隔离无法阻止的情况
Section titled “隔离无法阻止的情况”- 无限循环 · 栈溢出 · 进程终止(
Environment.Exit) - 文件 · 网络访问
插件是以与编辑器相同权限运行的代码。隔离是防止失误的机制,而不是安全边界。