跳转到内容

测试与调试

想做的事方法
查看日志输出日志面板 — [我的 id] …(host.Log),被隔离时显示异常内容
查看状态插件管理器 · 控制台 plugins
重新加载修改后的 dll插件管理器「重新加载 (所选)」
快速测试逻辑用伪 INpWorld 做单元测试
设置断点将调试器附加到正在运行的编辑器
连画面一起自动检查AddShotScenario(仅限开发用构建,参见下方限制)

输出日志是下方区域的面板。若已关闭,请从「窗口」菜单打开。与插件相关的行如下。

行含义
[<id>] …通过 host.Log 记录的文字
[插件 <id>] <版本> 已启用 — 메뉴 1 · 빠른 추가 1加载成功及注册摘要
[插件] <名称> 已挂载 — …已挂接到编辑器
[插件 <id>] 加载失败: <异常类型>: <内容>Register · 构造函数中发生异常
[插件 <id>] 错误 → 已隔离 — <位置>: <异常类型>: <内容>回调中发生异常。<位置> 显示为 菜单 <名称> · 命令 <名称> · Committed 等。
[插件 <id>] 已卸载 — <原因>因禁用 · 重新加载 · 错误而卸载

被隔离时,画面下方也会出现「插件“<名称>”因错误已禁用 — …」通知。

命令作用
plugins插件列表:id · 版本 · 状态 · 错误
plugins reload重新扫描文件夹并加载已启用的插件(已启用的不会重新加载)
  1. 重新构建。编辑器不锁定 dll,因此可以在编辑器运行时构建。
  2. 用新的 dll(和 pdb)覆盖插件文件夹中的文件。
  3. 在插件管理器中选中该行,点击「重新加载 (所选)」。

若直接把构建输出文件夹用作插件文件夹,就不需要第 2 步。在环境变量 NP_EDITOR_PLUGINS 中填入构建输出的父文件夹。其下每个子文件夹就是一个插件。

NP_EDITOR_PLUGINS=D:\dev\np_plugins_out
D:\dev\np_plugins_out\my_first_plugin\ ← plugin.json · MyFirstPlugin.dll · .pdb

环境变量需要重新启动编辑器才会读取。

SDK 只有接口,因此可以用一个模拟 INpWorld 的小类,在没有编辑器的情况下测试插件逻辑。把生成方块的代码提取为接收 INpWorld 的函数。

using NP.Editor.Sdk;
/// <summary>测试用伪世界:格 → 方块状态。</summary>
public sealed class FakeWorld : INpWorld
{
private readonly Dictionary<NpCell, string> _b = new();
public string? FilePath => null;
public NpBox? Selection { get; set; }
public NpBox? Bounds => null;
public string GetBlock(int x, int y, int z) => _b.TryGetValue(new NpCell(x, y, z), out var s) ? s : "minecraft:air";
public bool IsAir(int x, int y, int z) => !_b.ContainsKey(new NpCell(x, y, z));
public IEnumerable<NpBlock> Blocks(NpBox? box = null) =>
_b.Where(kv => box is not { } bb || bb.Contains(kv.Key.X, kv.Key.Y, kv.Key.Z))
.Select(kv => new NpBlock(kv.Key.X, kv.Key.Y, kv.Key.Z, kv.Value));
public long CountBlocks() => _b.Count;
public int Edit(string label, Action<INpEdit> build) { var e = new Ed(_b); build(e); return e.Changed; }
private sealed class Ed : INpEdit
{
private readonly Dictionary<NpCell, string> _b; public int Changed;
public Ed(Dictionary<NpCell, string> b) => _b = b;
public void Set(int x, int y, int z, string state)
{
var c = new NpCell(x, y, z);
if (state == "minecraft:air") { if (_b.Remove(c)) Changed++; return; }
if (!_b.TryGetValue(c, out var old) || old != state) { _b[c] = state; Changed++; }
}
public void Clear(int x, int y, int z) => Set(x, y, z, "minecraft:air");
}
}
// 要测试的代码(插件内)
public static class PillarBuilder
{
public static int Build(INpWorld w, NpCell at) => w.Edit("3×3 기둥", e =>
{
for (int y = 0; y < 6; y++)
for (int x = 0; x < 3; x++)
for (int z = 0; z < 3; z++)
e.Set(at.X + x, at.Y + y, at.Z + z, "minecraft:stone_bricks");
});
}

xUnit 测试示例如下。

public class PillarTests
{
[Fact]
public void Pillar_is_3x3x6()
{
var w = new FakeWorld();
int n = PillarBuilder.Build(w, new NpCell(0, 1, 0));
Assert.Equal(54, n);
Assert.Equal("minecraft:stone_bricks", w.GetBlock(2, 6, 2));
Assert.True(w.IsAir(0, 7, 0));
}
}
容易测试的内容方法
放置方块的计算对伪 INpWorld 执行 Edit 后检查 GetBlock
命令结果字符串检查 NpCommand.Run(new[] { "6" }) 的返回值
CSV 之类的导出用临时路径调用 NpFileFormat.Run(path) 并检查文件
世界逻辑预览直接创建 new NpSceneSim(...) 并调用 Simulate

与编辑器完全相同的锁定 · 撤销行为无法用伪世界确认。最后请在编辑器中手动确认。

  1. 以 Debug 构建插件。
  2. 将 .dll 和 .pdb 一起放入插件文件夹。编辑器会一并读取同一文件夹中的 .pdb。
  3. 启动编辑器并启用插件。
  4. 在 IDE 中用「附加到进程」选择 NPEditor.exe(.NET / CoreCLR 调试器)。
  5. 在代码中设置断点。点击菜单或执行命令时即会停下。
  • 在断点处停留过久,编辑器画面也会停住。因为所有回调都在主线程上运行。
  • 若要在 Register 中设置断点,请在附加后点击「重新加载 (所选)」。

AddShotScenario 为编辑器画面验证运行器注册场景。场景会更改世界、放置相机,并留下判定结果和截图。

host.AddShotScenario(new NpShot("myroad_smoke", async api =>
{
api.World.Edit("시험", e => e.Set(0, 1, 0, "minecraft:stone"));
api.LookAt(6, 6, -6, 0, 1, 0); // 从眼睛 (6,6,-6) 看向 (0,1,0)
await api.Frames(5); // 等待 5 帧
api.Check(api.World.GetBlock(0, 1, 0) == "minecraft:stone", "돌 놓임");
await api.Shot("smoke"); // 保存 PNG
}));

运行器命令行如下。

NPEditor.exe -- --np-shot=myroad_smoke --np-out=<结果文件夹> --np-quit
参数含义
--np-shot=<id>[,<id>…]要运行的场景
--np-out=<文件夹>写入 PNG 和结果文件(shot.json — 各场景的通过/失败)的文件夹
--np-quit结束后关闭编辑器
  • 场景中发生异常时,该判定为失败,插件被隔离。
  • 场景开始时会收到 WorldChanged(Reason = ShotScenario)。

限制:画面验证运行器只包含在编辑器开发用构建中,安装版中没有。此外,该模式下不会自动读取用户插件文件夹。因此当前版本的安装版无法运行外部插件的场景。外部插件请使用单元测试和手动确认。

确认原因
启用 → 禁用 → 启用菜单 · 面板是否重复生成或残留
新建世界 · 打开其他世界是否在 WorldChanged 中清空状态
执行一次功能 → 按一次 Ctrl+Z是否能以一步撤销干净地恢复
无选择 · 空世界Selection · Bounds 为 null 时是否没有异常
大型世界Committed · 动态文字是否不会卡住编辑器