Skip to content

Adding world logic types

Since SDK 1.2, plugins can add new triggers, conditions and actions to World logic. Types registered with host.AddSceneEvent appear in the following places, exactly like built-in types.

WhereWhat appears
World logic formThe Category group in the trigger · condition · action menus, field inputs
Logic graphNodes and pins
CheckRequired fields · actor kinds · number ranges · enum values
One-line rule summarySummary template
Play previewSimulate handling · Fire button
AI connection (MCP)Marked [<plugin id>] in the scene_rules section=schema list

Registration in the editor is for the form, checks, MCP and preview. To actually run the type on the game server, you must also register the same type in the server plugin NP Scene. See “Server side” below.

Write "sdk": "^1.2" or later in plugin.json. If you use the field kind ActorList, use "^1.3".

host.AddSceneEvent(new NpSceneEvent(NpSceneEventRole.Trigger, "myquest_npc_talk", "NPC 에게 말 걸 때", "퀘스트", new[]
{
new NpSceneField("npc", NpSceneFieldKind.Actor, Required: true, ActorKinds: new[] { "display", "text", "spawn" }, Label: "NPC"),
new NpSceneField("line", NpSceneFieldKind.Text, Label: "대사 id"),
},
Help: "플레이어가 NPC 와 대화를 시작할 때", Icon: "interact",
Summary: "플레이어가 NPC {npc} 에게 말을 걸면")); // no Simulate = fired only with the Fire button
host.AddSceneEvent(new NpSceneEvent(NpSceneEventRole.Action, "myquest_start", "퀘스트 시작", "퀘스트", new[]
{
new NpSceneField("quest", NpSceneFieldKind.Text, Required: true, Label: "퀘스트 id"),
},
Icon: "star", Summary: "퀘스트 {quest} 시작",
Simulate: s => { s.Log($"퀘스트 {s.Str("quest")} 시작 → {s.Player}"); return true; }));

A rule that uses these types is saved in scene.json like this (part of a rule).

{
"trigger": { "type": "myquest_npc_talk", "npc": "npc_guard" },
"actions": [ { "type": "myquest_start", "quest": "q1" } ]
}
public sealed record NpSceneEvent(NpSceneEventRole Role, string Type, string Label, string Category, IReadOnlyList<NpSceneField> Fields,
string Help = "", string Icon = "event", string Summary = "", Func<NpSceneSim, bool>? Simulate = null);
public enum NpSceneEventRole { Trigger, Condition, Action }
FieldRequiredDescription
RoleYesRole: Trigger · Condition · Action
TypeYesSaved name (JSON "type"). Start it with your own name (see “Naming rules” below).
LabelYesName shown in the form and graph
CategoryNoMenu group name. If empty, the plugin name (name in plugin.json)
FieldsYesField list (NpSceneField). An empty array if there are no fields
HelpNoDescription (tooltip)
IconNoIcon name (trigger · event · signal · interact · logic · star …). Default event; an unknown name uses the default image
SummaryNoOne-line summary template (see “Summary templates” below). If empty, Label
SimulateNoPlay preview handling (see “Simulate” below)

Plugin types appear after the built-in types within the same group.

public sealed record NpSceneField(string Name, NpSceneFieldKind Kind, bool Required = false, string Default = "", string[]? Options = null,
string[]? ActorKinds = null, string Label = "", string Help = "", double? Min = null, double? Max = null);
FieldDescription
NameJSON key
KindField kind (table below)
RequiredIf required, an empty value is a check error
DefaultDefault value string. The format is in the “How to write Default” column of the table below
OptionsEnum choices
ActorKindsActor kinds an Actor field accepts (trigger · blocker · display · text · spawn · camera · path …). If empty, any kind
Label · HelpName and description shown in the form
Min · MaxNumber range (checked)
KindForm inputJSON valueHow to write DefaultSDK
TextTextstring"안녕"1.2
NumberNumbernumber"1.5"1.2
BoolCheckboxtrue / false"true"1.2
EnumOne of the choices (Options)string"none"1.2
ActorActor picker (ActorKinds only)actor id stringactor id1.2
AnimAnimation clipclip id stringclip id1.2
SoundEventSound event"ns:path" string"minecraft:block.note_block.bell"1.2
BlockBlock statestring (minecraft:stone)block state string1.2
ItemItemstring (minecraft:diamond_sword)item id1.2
ParticleParticlestring (minecraft:happy_villager)particle id1.2
EffectPotion effectstring (minecraft:speed)effect id1.2
Vec3x · y · z[x, y, z]"[0,1,0]"1.2
SignalSignal namestringsignal name1.2
FlagScene flag namestringflag name1.2
ActorListMultiple actorsarray of actor ids"cam_a,cam_b"1.3

There are more field kinds used only by built-in types: flag value · function (call a logic function) · block cell list (Cells). These kinds are not in the SDK yet. Field options such as visibility conditions, integers only and allowing #tags are not in the SDK yet either.

Plugin types also appear as nodes in the Logic graph. The pin rules are the same as for built-in types.

PinHow many connections
Exec pin (white arrow)One chain. One node after each node
Function pin (yellow diamond)One wire per pin. Connecting an already-connected pin replaces the wire.
Data pin (purple circle, actor reference)One actor can be connected to multiple fields.
  • Actor fields accept data pins. When you connect an actor in the graph, ActorKinds is checked.
  • Function pins exist only on built-in Call function types. Plugin types cannot create function pins.

Since 0.9.11: the yellow pins that call functions take one wire per pin.

The template used for the one-line summary in the rule list. Each {field name} placeholder is replaced with that field’s value.

TemplateField valueDisplayed text
퀘스트 {quest} 시작quest = q1퀘스트 q1 시작
플레이어가 NPC {npc} 에게 말을 걸면npc = npc_guard (name “경비병”)플레이어가 NPC 「경비병」 에게 말을 걸면
퀘스트 {quest} 가 {state} 일 때state empty, Default = none퀘스트 q1 가 none 일 때
  • Actor fields are replaced with the actor name instead of the actor id.
  • Fields without a value are filled with Default.

Simulate is a function that runs rules ahead of time during Play and Simulate. It is not used on the game server.

RoleWhat Simulate doesReturn valueIf omitted
TriggerCalled once per rule of that type on every preview tick. To fire, call s.Fire(player)IgnoredFired only with the Fire button in the World logic and Play panels
ConditionTrue/false decisiontrue = trueAlways treated as true
ActionRuns the action (logging · changing your own state)IgnoredOnly a log entry is left

If Simulate throws, the plugin is isolated. All of that plugin’s types are removed, and the preview keeps running.

The value Simulate receives.

MemberTypeDescription
Role · TypeNpSceneEventRole · stringThe role and type being handled
RuleIdstringRule id
Playerstring?Who triggered it. null when handling a trigger
PlayersIReadOnlyList<string>Current players
NowdoublePreview time (seconds)
ArgsIReadOnlyDictionary<string, string>Node field values (all strings)
Str(string key, string fallback = "")stringField value as a string
Num(string key, double fallback = 0)doubleField value as a number
Bool(string key, bool fallback = false)boolField value "true" / "false"
PositionOf(string player)NpVec3?The player’s foot position. null if none
Log(string text)voidOne line in the preview log ([<id>] …)
Fire(string? player)void(Trigger handling only) Fires this rule. Does nothing in other roles

String forms of Args values:

Field valueArgs string
StringAs-is
Number"1.5" (dot decimal, culture-independent)
True/false"true" / "false"
Array · object (Vec3 · ActorList)JSON string ("[0,1,0]"). For ActorList, values saved as comma-separated strings also arrive as-is.
host.AddSceneEvent(new NpSceneEvent(NpSceneEventRole.Trigger, "myquest_near_spawn", "스폰 가까이 올 때", "퀘스트", new[]
{
new NpSceneField("r", NpSceneFieldKind.Number, Default: "5", Label: "반경", Min: 1, Max: 64),
},
Summary: "스폰 {r}칸 안에 들어오면",
Simulate: s =>
{
foreach (var p in s.Players)
if (s.PositionOf(p) is { } pos && Math.Abs(pos.X) + Math.Abs(pos.Z) < s.Num("r", 5)) s.Fire(p);
return true;
}));
  • This function is called on every preview tick. Keep it light.
  • Filters (once only · cooldown and so on) and conditions are checked by the editor after Fire.

Example: a condition and action that remember state

Section titled “Example: a condition and action that remember state”
private readonly Dictionary<string, string> _quests = new(); // player:quest → state
// Condition: quest state
Simulate: s => Get(s.Player, s.Str("quest")) == s.Str("state", "none")
// Action: start quest
Simulate: s => { _quests[(s.Player ?? "") + ":" + s.Str("quest")] = "active"; s.Log("시작"); return true; }
// clear in a new world
host.WorldChanged += _ => _quests.Clear();

The full code is in Sample: event_example.

RuleWhy
Prefix Type with your own name (myquest_start).If the same type already exists in the same role (including built-ins), registration throws → the plugin is isolated
Use lowercase letters, digits and _.Easier to match with the server plugin and JSON.
Never change a Type once used.Saved rules use that name.
Do not use built-in type names (cinematic · enter · sound …).If it collides with a built-in, the plugin does not enable. The built-in list is in World logic type list.
  • The plugin’s types are removed from the menus and the graph palette.
  • Rules that use those types are not deleted. They get an Unknown type warning, and their field values are kept.
  • In the preview, a rule with an unknown trigger is skipped, an unknown condition is false, and an unknown action is skipped on its own.

The server plugin NP Scene (Paper) runs the rules in game. Editor plugins do not run on the server.

  1. Write a handler for the same type as a Paper plugin.
  2. Get NP Scene’s SceneEventsApi from Bukkit’s ServicesManager and register the type.
  3. Match the field names and value formats (JSON) exactly with the editor-side NpSceneField.

The server skips unknown types with a single warning (trigger = rule skipped, condition = false, action = only that action skipped). So the map still opens without a server handler, but that rule does not run.