跳转到内容

加载与生命周期

编辑器扫描插件文件夹以查找 plugin.json,只加载通过检查且处于启用状态的插件。插件代码中发生异常时,只禁用该插件,编辑器继续运行。这称为隔离。

  1. 扫描文件夹。 对每个插件文件夹,按名称顺序查看其下一级子文件夹。只有包含 plugin.json 的文件夹才是插件。
  2. 检查 plugin.json。 字段有误时为「错误 (文件)」。
  3. 比较 SDK 版本。 编辑器的 SDK 版本不在 sdk 范围内时为「版本不匹配」。
  4. 确认启用。 用户禁用的插件为「关闭」。
  5. 首次运行警告。 首次发现插件时会弹出一次警告窗口。点击「可信 — 加载」后会记住并加载。点击「本次不加载」则本次不加载任何内容(保持「等待」)。
  6. 创建加载上下文。 为每个插件单独创建。dll 以字节读取后加载,因此不会锁定文件。同一文件夹中有 .pdb 时一并加载。
  7. 创建入口类。 用无参数构造函数创建 entry(没有时为第一个 INpEditorPlugin 实现)。找不到或无法创建该类时为「错误 (已隔离)」。
  8. 调用 Register(host)。 发生异常时不注册任何内容,状态为「错误 (已隔离)」。
  9. 挂接到编辑器。 将菜单 · 模式 · 面板 · 命令等挂接到编辑器。挂接过程中发生异常时为「错误 (已隔离)」。名称已存在的命令 · Actor 类型只有该项不会挂接。
  10. 已启用。 输出日志中记录 [插件 <id>] <版本> 已启用 — <注册摘要>。

启动编辑器时执行一次 110。插件管理器的「重新扫描」或控制台 plugins reload 会重新执行 110。已启用的插件不会重新加载。

显示在插件管理器的「状态」列和控制台 plugins 的结果中。

状态含义处理
等待已找到但尚未加载(确认警告前等)确认警告,或点击「重新扫描」
已启用正常运行中—
关闭用户取消了「启用」勾选重新勾选
错误 (文件)plugin.json 有误 · 没有 dll · id 重复根据下方详细行中的原因修改
版本不匹配sdk 范围不包含编辑器的 SDK;或使用了编辑器 SDK 中没有的成员而加载失败修改 sdk 或更新编辑器
错误 (已隔离)无法创建入口类 · Register 或回调中发生异常 → 只卸载该插件修正输出日志中的异常后点击「重新加载 (所选)」

通过「工具 › 插件管理器…」打开。

列 · 按钮作用
启用勾选则启用(加载),取消则卸载。状态保存在 plugins_state.json 中。
名称 · 版本 · 状态plugin.json 的 name · version 与当前状态。
注册已启用时为注册摘要(例 메뉴 1 · 빠른 추가 1),否则为错误原因。
重新扫描重新查看文件夹。查找新插件,并卸载已消失的文件夹。
重新加载 (所选)卸载所选插件,重新读取 plugin.json · dll 并启用。在构建之后使用。
打开文件夹打开用户插件文件夹。

以下情况会卸载插件:取消「启用」勾选 · 「重新加载 (所选)」 · 删除文件夹后「重新扫描」 · 错误隔离。关闭编辑器时不经过卸载过程,进程直接结束。因此不要把保存之类必须执行的操作放在 Dispose 中。

  1. 移除编辑器中注册的所有内容(菜单 · 模式 · 面板 · 命令 · 快捷键 · Actor 类型 · 格守卫 · 对称)。若正在使用该插件的模式,则返回方块笔刷模式。
  2. 若入口类为 IDisposable,则调用 Dispose()。
  3. 从注册表中移除世界逻辑 type。使用该 type 的规则不会被删除,而是保留为「未知 type」警告。
  4. 若持有运行相机,则释放(恢复原视角)。
  5. 清除通过 host.Draw 绘制的内容。
  6. 卸载加载上下文(collectible 卸载)。

host.Settings 的值保留在文件中,重新启用时原样读取。

应做的事原因
在 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 使用最先找到的那个。

注册的内容编辑器内名称重名时
菜单 · 快速添加 · 导入/导出 · 面板 · 模式 · 快捷键ext.<插件 id>.<我的 id>不会与其他插件 · 内置功能重名
快捷键 Mode直接写自己的模式 id,编辑器会加上 ext.<插件 id>.—
控制台命令名原样(不含空格)已存在时只有该命令不会挂接(先存在者优先)
世界逻辑 type原样(在同一角色内)已存在时(含内置)注册异常 → 该插件被隔离
Actor 类型 Kind原样已存在时只有该类型不会挂接(先存在者优先)

在命令名 · 世界逻辑 type · Actor kind 前加上自己的名称(例 road_build、myquest_start、myroad.zone)。

  • 无限循环 · 栈溢出 · 进程终止(Environment.Exit)
  • 文件 · 网络访问

插件是以与编辑器相同权限运行的代码。隔离是防止失误的机制,而不是安全边界。