Skip to content

Testing and debugging

What you want to doHow
View logsOutput Log panel — [your id] … (host.Log); exception details if isolated
View statePlugin Manager · console plugins
Reload a fixed dllPlugin Manager Reload (selected)
Test logic quicklyUnit tests with a fake INpWorld
Set breakpointsAttach a debugger to the running editor
Automatically check down to the screenAddShotScenario (development builds only; see the limitation below)

The Output Log is a panel in the bottom area. If you closed it, open it from the Window menu. Plugin-related lines look like this:

LineMeaning
[<id>] …Text written with host.Log
[Plugin <id>] <version> enabled — Menu 1 · Quick add 1Load succeeded, with the registration summary
[Plugin] <name> attached — …Attached to the editor
[Plugin <id>] Load failed: <exception type>: <message>Exception in Register or the constructor
[Plugin <id>] Error → isolated — <where>: <exception type>: <message>Exception in a callback. <where> appears as Menu <name> · Command <name> · Committed and so on.
[Plugin <id>] Unloaded — <reason>Unloaded by disabling · reloading · an error

When a plugin is isolated, a notification Plugin "" disabled due to an error — … also appears at the bottom of the screen.

CommandWhat it does
pluginsPlugin list: id · version · state · error
plugins reloadRescans the folders and loads enabled plugins (plugins already on are not reloaded)
  1. Rebuild. The editor does not lock the dll, so you can build while it is running.
  2. Overwrite the dll (and pdb) in the plugin folder with the new one.
  3. In the Plugin Manager, select that row and click Reload (selected).

If you use the build output folder directly as a plugin folder, step 2 is unnecessary. Put the parent folder of the build output in the NP_EDITOR_PLUGINS environment variable. Each subfolder under it is one plugin.

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

Environment variables are read only when the editor restarts.

The SDK is interfaces only. So you can test plugin logic without the editor by using a small class that imitates INpWorld. Move the code that creates blocks into a function that takes an INpWorld.

using NP.Editor.Sdk;
/// <summary>Fake world for tests: cell → block state.</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");
}
}
// Code under test (inside the plugin)
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");
});
}

An xUnit test example:

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));
}
}
Easy to testHow
Block placement calculationsEdit on a fake INpWorld, then check GetBlock
Command result textCheck the return value of NpCommand.Run(new[] { "6" })
Exports such as CSVCall NpFileFormat.Run(path) with a temporary path and check the file
World logic previewConstruct new NpSceneSim(...) directly and call Simulate

A fake world cannot verify locking and undo behavior identical to the editor’s. Check by hand in the editor at the end.

  1. Build the plugin in Debug.
  2. Put the .dll and .pdb together in the plugin folder. The editor also loads the .pdb from the same folder.
  3. Start the editor and enable the plugin.
  4. In your IDE, use Attach to Process and choose NPEditor.exe (.NET / CoreCLR debugger).
  5. Set breakpoints in your code. Execution stops when you click the menu item or run the command.
  • If you stay stopped at a breakpoint for long, the editor screen freezes too, because every callback runs on the main thread.
  • To break in Register, attach first and then click Reload (selected).

Screen verification scenarios (AddShotScenario)

Section titled “Screen verification scenarios (AddShotScenario)”

AddShotScenario registers a scenario for the editor’s screen verification runner. A scenario changes the world, places the camera, and records verdicts and screenshots.

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); // eye (6,6,-6) → looks at (0,1,0)
await api.Frames(5); // wait 5 frames
api.Check(api.World.GetBlock(0, 1, 0) == "minecraft:stone", "돌 놓임");
await api.Shot("smoke"); // save PNG
}));

The runner command line:

NPEditor.exe -- --np-shot=myroad_smoke --np-out=<result folder> --np-quit
ArgumentMeaning
--np-shot=<id>[,<id>…]Scenarios to run
--np-out=<folder>Folder for PNGs and the result file (shot.json — pass/fail per scenario)
--np-quitQuit the editor when done
  • If a scenario throws, that verdict fails and the plugin is isolated.
  • When a scenario starts, WorldChanged (Reason = ShotScenario) fires.

Limitation: the screen verification runner is included only in editor development builds. It is not in the installed version. Also, in this mode the user plugin folder is not loaded automatically. So with the current installed version you cannot run external plugin scenarios. For external plugins, use unit tests and manual checks.

CheckWhy
Enable → disable → enableMenus and panels are not created twice or left behind
New world · open another worldState is cleared in WorldChanged
Use a feature once → Ctrl+Z onceIt cleanly reverts in a single undo step
No selection · empty worldNo exceptions when Selection · Bounds are null
Large worldCommitted and live text do not stall the editor