MSW BehaviourTree
End-to-end authoring skill for MSW .behaviourtree files. Owns both the project-specific authoring spec (<ProjectRoot>/.behaviourDocs/bt-spec.md) and the tree generation itself. Fixed graph rules and skeletons live in this skill's references/; the per-project spec is (re)built by this skill's local scripts/build-spec.cjs.
---
🚦 Execution order (follow this sequence)
0. Build / refresh the project spec (bt-spec.md)
The spec is the source of truth for every project-specific data point: each custom action/decorator node's definitionId, btNodeType, visible propertyKey names, and the serialized Type.type strings stamped to this project's CoreVersion.
When to (re)build:
- First time working on BT in a project (no
.behaviourDocs/bt-spec.mdyet). - After any change that affects BT node surface area:
- new / renamed / removed
.codeblockwhose paired.mluaextendsActionNode/DecoratorNode - added / removed / renamed
propertylines in such a.mlua Environment/configCoreVersionbumped (the serialized type strings are version-tagged).- The user says they recently added/changed a BT codeblock or a
.mluaproperty — stale UUIDs / missing properties silently produce broken trees. - The downstream validation (Step 7) flags a
definitionId,propertyKey, or version mismatch.
How to run — invoke this skill's local script:
node "scripts/build-spec.cjs" --projectRoot "<MSW project root>"
If the current working directory is already the MSW project root, --projectRoot can be omitted. Requires Node.js on PATH (no other dependencies — pure stdlib fs/path).
Optional overrides (long flags, case-insensitive):
| Flag | Default | Notes |
|---|---|---|
--projectRoot | current working directory | MSW project root to scan |
--outputPath | <ProjectRoot>/.behaviourDocs/bt-spec.md | folder is created if missing |
--coreVersion | read from <ProjectRoot>/Environment/config (CoreVersion field) | required if the config is missing |
Example with overrides:
node "scripts/build-spec.cjs" --projectRoot "C:/path/to/project" --coreVersion 26.5.0.0
The script throws if Environment/config is absent and --coreVersion is not passed — there is no fallback default.
What the spec contains:
- Project metadata — project root,
CoreVersion, generated time, discovered node counts. - Built-in composite node names and their fixed
definitionId/btNodeType. - Custom action nodes —
Name,definitionId,btNodeType, visible property names. - Custom decorator nodes — same shape as action nodes.
- Type map — mlua type to serialized
MODNativeType.typeplus BlackboardObjectValueshape.
UUIDs come from real .codeblock files in the project — the spec never invents them. @HideFromInspector properties are filtered out automatically. Fixed authoring rules, file skeletons, and validation checklists live in this skill's references/ rather than in the generated spec.
After (re)building, read the freshly written <ProjectRoot>/.behaviourDocs/bt-spec.md and continue with the steps below. The compact spec intentionally lists only property names; when constructing nodeProperties, resolve each property's mlua type/default from the paired .mlua file, then use the type map in bt-spec.md §4 for propertyType.type.
Also read references/skeleton-minimal.json for the smallest valid tree, references/skeleton-full.json for a Composite+Decorator+Action+Blackboard example with all optional fields populated, references/node-catalog.md for fixed graph rules, and any existing .behaviourtree in the project (*/.behaviourtree) to mirror conventions. Replace {CORE_VERSION} in the skeletons with the CoreVersion from bt-spec.md — both at the top level and inside every MOD.Core.* type string in Blackboard variables and nodeProperties.
1. Collect input from the user
Confirm via context, or ask via AskUserQuestion if anything is ambiguous:
| Item | Description | Example |
|---|---|---|
name | Display name for the tree | "PatrolAndChase" |
| Save path | .behaviourtree location (relative to project root) | RootDesk/MyDesk/PatrolAndChase.behaviourtree |
| Tree shape | Intended node graph (root composite + children) | Sequence → [Chase, MoveTo] |
| Custom nodes | Action/decorator codeblocks the tree references | Chase, MoveTo, Jump |
| Blackboard variables | Variable name + type + initial value | TargetEntity: Entity, MoveSpeed: number = 10.0 |
| Node properties | For each custom node, which property maps to which Blackboard variable | Chase.TargetEntityKey = "TargetEntity" |
Custom-node existence check (mandatory): every custom action/decorator name the user mentions must appear in bt-spec.md §2 / §3. If a referenced node is not in the spec, stop and ask the user — do not invent a UUID, do not assume a node exists by name, and do not skip rerunning Step 0.
2. Mint UUIDs
You need:
- One UUID for the file → goes into
EntryKeyandContentProto.Json.id(both identical, both prefixedbehaviourtree://). - One UUID for each node in
Nodes(nodeId).
node -e "console.log(require('node:crypto').randomUUID())"
Mint up front, write into a scratch table, then assemble. Don't reuse the file UUID as a nodeId.
3. Resolve every definitionId
| Node category | definitionId value | btNodeType |
|---|---|---|
Built-in composite (SequenceNode, SelectorNode, ParallelNode) | Same string as nodeName | 1 |
| Custom action node | value from bt-spec.md §2 | 0 |
| Custom decorator node | value from bt-spec.md §3 | 2 |
Custom-node UUIDs come from bt-spec.md (which read them from real .codeblock files) — never any other source.
4. Build the Blackboard
For each variable, copy the Type.type string and ObjectValue shape verbatim from bt-spec.md §4. The version-tagged substring (Version=<CoreVersion>) must match exactly — a typo silently breaks deserialization.
Variables is an ordered array; each entry: { Name, Type: { "$type": "MODNativeType", type: "<from spec>" }, ObjectValue: <from spec> }. The ObjectValue does not include a $type discriminator (unlike Value in .model files).
For Component / ComponentRef, ComponentId is <entity-uuid>:<ComponentName> (engine component) or <entity-uuid>:<scriptCodeblockUuid>:<ScriptComponentName> (script component). Mirror an existing serialized example in the project.
Numeric ObjectValues use float literal form (3.0, not 3).
4.5 Resolve node property values
For each custom node that needs nodeProperties:
- Confirm the
propertyKeyexists inbt-spec.md§2 / §3 for that node. - Find the paired
.mluaby searching forscript <NodeName> extends ActionNodeorscript <NodeName> extends DecoratorNodeunder the project. If multiple files match, prefer the one whose sibling.codeblockhas the exactdefinitionIdUUID frombt-spec.md; if still ambiguous, ask the user. - Read the visible
propertydeclarations in that.mlua, ignoring@HideFromInspectorproperties. This gives the mlua type and default value. - Include a
nodePropertiesentry only when the user provided a value, the behavior requires a non-default value, or a*Keyproperty must point at a Blackboard variable. Omit optional properties that can safely use the.mluadefault. - For
*Keystring properties, setpropertyValueto the Blackboard variable name. Infer the variable by name and getter usage when obvious (MoveSpeedKey->MoveSpeed,TargetEntityKey->TargetEntity). If more than one Blackboard variable could match, ask. - For literal properties, use the user-provided value. If no value is provided and the
.mluadefault is meaningful, omit the property instead of serializing a guessed value. - If
OnBehavechecks a property fornil, empty string, or invalid enum and no value can be inferred, ask the user before writing the tree.
nodeProperties entry shape:
{
"propertyKey": "<property name>",
"propertyType": { "$type": "MODNativeType", "type": "<type from bt-spec.md §4>" },
"propertyValue": <value>
}
5. Assemble Nodes
Hard graph constraints (validate before writing):
- RootNode is not a parent node. It must not have
childNodes. It only storesstartNodeId, andstartNodeIdpoints to exactly one node inNodes. - If the tree needs several top-level behaviors, use either one Composite as the single
startNodeId, or one Decorator as the singlestartNodeIdwhosedecoChildNodeswraps a Composite or another Decorator chain that eventually wraps a Composite. Put the multiple behaviors under that Composite'schildNodes. - Exactly one node in
Nodesmay havenodeParentId: "": the node referenced byRootNode.startNodeId. Do not create multiple root-level Action/Composite/Decorator nodes. - Composite (
btNodeType: 1) is the only node category that can own multiple children throughchildNodes. - Decorator (
btNodeType: 2) is only a wrapper/parent for exactly one Action, Composite, or Decorator node. It can also be the child of another Decorator, so Decorator-to-Decorator chains are valid. It must use singulardecoChildNodes(a singlenodeIdstring) for that one child, notchildNodes; the wrapped child must also record the Decorator's id in itsnodeParentId. - Decorators applying to the same Action MUST be chained — never flattened as siblings. Each decorator owns exactly one downstream subtree. If two or more decorators are meant to gate/modify the same Action, build a single chain
Composite → ADeco → BDeco → CDeco → Actionwhere each decorator'sdecoChildNodespoints to the next decorator (and finally the Action). Concretely: within one chain leading to a single Action, no two decorators may share the samenodeParentId— each decorator's parent is the previous decorator, and only the topmost decorator's parent is the Composite. Sibling decorators under one Composite are still valid when each wraps a different downstream subtree. ✅Composite → ADeco → BDeco → CDeco → Action(chain — every decorator has a unique parent within the chain). ❌Composite → [ADeco→Action, BDeco→Action, CDeco→Action](Action duplicated to bypass chaining). ❌Composite → [ADeco, BDeco, CDeco, Action](decorators flattened — they don't wrap the Action and are effectively orphaned). - Action (
btNodeType: 0) is a leaf — never has children.
Node-write invariants:
- Every
nodeIdis unique within the file. nodeParentIdof every non-root node points to a realnodeIdthat is a Composite or Decorator. It must never point toRootNode, becauseRootNodeis not represented as a node inNodes.- If a node's parent is a Composite, that Composite must include the node id in
childNodes. - If a node's parent is a Decorator, that Decorator's
decoChildNodesmust equal that node'snodeId. This is valid even when both parent and child are Decorators. - Composite
childNodes↔ childnodeParentIdis bidirectionally consistent. - Action nodes omit
childNodes. Decorator nodes omitchildNodesand use exactly onedecoChildNodes(single stringnodeId) instead. - Never write
probability. The editor strips this field on round-trip, and the supported composites (SequenceNode,SelectorNode,ParallelNode) do not consume per-child weights. Older generated trees in the project may still carry"probability": 1.0on every node; treat that as legacy on read but do not write it on new nodes. - Decorator nodes (
btNodeType: 2) omitnodePosition. The editor positions a Decorator automatically relative to the child it wraps, and writes nonodePositionfield for it on save. Only Composites and Actions carrynodePosition. TheRootNodeblock also carries its ownnodePosition(separate from the start node). - Empty collection fields are omitted, not serialized as
[]. A Composite with no children yet should omitchildNodesentirely; a node with no overrides should omitnodePropertiesentirely. Empty arrays are an editor-draft artifact — do not author them. - Decorator child field is
decoChildNodes(canonical — this is what the editor preserves on save;ChildNodeIdis silently stripped on round-trip). It is a single string holding the wrapped child'snodeId(not an array). When reading legacy files you may still encounterChildNodeIdon hand-authored decorators; treat it as the same field. When writing, always emitdecoChildNodes. RootNode.startNodeIdreferences one of thenodeIds — an Action, Composite, or Decorator — and that node is the only node withnodeParentId: "".
*Key-suffix String properties carry the name of a Blackboard variable (resolved at runtime via BlackBoard:GetXxx). Non-Key properties carry the literal value.
6. nodePosition format
nodePosition is a JSON object with numeric x / y:
"nodePosition": { "x": 0.0, "y": 0.0 }
Use float literals (0.0, not 0). The legacy string form "(0.000, 0.000)" may still appear in older hand-authored trees — read it as equivalent, but always write the object form (the BT editor canonicalizes to this shape on save, so the string form re-serializes to a noisy diff the first time the file is opened).
Editor axes: the BT editor uses a math-convention canvas — +x is right, +y is up (upper-right quadrant is positive). So a child placed at a higher y than its parent appears above the parent on screen.
Layout rule — draw the tree downward: depth grows along −y (children sit below their parent), and siblings spread along ±x around the parent's x. Typical spacing: 200 units between depth levels and 200 units between siblings.
RootNode block vs the start node — do not stack them at the same position. RootNode.nodePosition is the canvas anchor and stays at { "x": 0.0, "y": 0.0 }. The start node (the node referenced by startNodeId) must sit one level below that anchor — putting it at (0, 0) makes it visually overlap the RootNode marker on the editor canvas. Treat the RootNode anchor as depth 0 and the start node as depth 1.
RootNode.nodePosition:{ "x": 0.0, "y": 0.0 }(fixed anchor — never moves)- Start node (depth 1, referenced by
startNodeId):{ "x": 0.0, "y": -200.0 } - Single child of the start node (depth 2):
{ "x": 0.0, "y": -400.0 } - Two children of the start node (depth 2):
{ "x": -100.0, "y": -400.0 }and{ "x": 100.0, "y": -400.0 } - Each additional level: parent.y − 200
Never place a child at a y greater than or equal to its parent's y — that draws upward and overlaps the parent visually. The same rule applies between RootNode and the start node: the start node must be at y ≤ -200 (strictly below the anchor).
Decorator nodes do not carry nodePosition. The editor lays them out automatically relative to the wrapped child. Omit the field on every btNodeType: 2 node; it appears only on RootNode, Composites (btNodeType: 1), and Actions (btNodeType: 0).
7. Write and validate
Write the JSON file, then run this checklist. In particular:
- [ ]
EntryKeyisbehaviourtree://{uuid}and matchesContentProto.Json.idexactly. - [ ] Top-level
Id,GameId,Contentare"".Usage,UseService,DynamicLoadingare0.UsePublishis1.CoreVersionmatches the project (Environment/config).StudioVersionis0.1.0.0.ContentTypeisx-mod/behaviourtree.ContentProto.UseisJson. - [ ]
RootNodehas nochildNodes;RootNode.startNodeIdmatches exactly onenodeIdinNodes; that start node hasnodeParentId: ""; and no other node hasnodeParentId: "". - [ ] Every
nodeParentIdis""or an existingnodeId. - [ ] All
nodeIdvalues are unique. - [ ] For every Composite, the set of
childNodesIDs equals the set of nodes whosenodeParentIdis this Composite. - [ ] Every Action has no
childNodes. Every Decorator has nochildNodes, has exactly onedecoChildNodes(singlenodeIdstring —ChildNodeIdis the legacy variant; the editor strips it on round-trip), and that id points to exactly one Action, Composite, or Decorator child whosenodeParentIdpoints back to the Decorator. Decorator-to-Decorator parent/child chains are valid and must be checked with the samedecoChildNodes↔nodeParentIdrule. - [ ] Decorator chain rule: when multiple decorators apply to the same Action, they form a single chain (
Composite → ADeco → BDeco → … → Action). Verify by walking each Action upward to its enclosing Composite: the decorators encountered along that one path must all have uniquenodeParentIdvalues (i.e. each decorator's parent is the previous decorator, never another decorator that already appeared in the chain). Two decorators in the same chain sharing anodeParentIdis invalid. (Sibling decorators under one Composite that wrap different downstream subtrees are fine — uniqueness is per-chain, not global.) - [ ] No node serializes
"probability". (Legacy1.0values may appear on read but are never authored.) - [ ] Every Composite and Action carries
nodePositionin object form{ "x": <num>, "y": <num> }with float literals — no legacy"(x.xxx, y.yyy)"strings on write. Decorator nodes carry nonodePositionat all. - [ ] Start node is not stacked on the RootNode anchor.
RootNode.nodePositionis{ "x": 0.0, "y": 0.0 }and the node referenced bystartNodeIdhasy ≤ -200.0(typically{ "x": 0.0, "y": -200.0 }). If the start node is a Decorator (nonodePosition), the first wrapped Composite/Action down the chain must satisfy this offset instead. - [ ] No node serializes empty arrays — a Composite with no children omits
childNodes; a node with no overrides omitsnodeProperties. Do not write"childNodes": []or"nodeProperties": []. - [ ] Every custom node's
definitionIdis copied frombt-spec.md(never invented). - [ ] Every
nodeProperties[].propertyKeymatches a property inbt-spec.mdfor that node. - [ ] Every
*Keyproperty'spropertyValuematches aBlackboard.Variables[].Nameof the right type. - [ ] Every type string is copied verbatim from
bt-spec.md§4 — version-tagged, typo-fragile. - [ ] Version cross-check: every
MOD.Core.type string'sVersion=X.Y.Z.Zsubstring (inBlackboard.Variables[].Type.typeandNodes[].nodeProperties[].propertyType.type) equals the file's top-levelCoreVersion. Mismatch silently breaks deserialization — common whenbt-spec.mdis stale relative to the project's currentCoreVersion. If they differ, re-run Step 0 before writing. (System.types use the immutableVersion=4.0.0.0and are exempt.) - [ ] JSON parses:
node -e "JSON.parse(require('node:fs').readFileSync(process.argv[1],'utf8'))" "<path>"
If any check fails, fix it before reporting done.
---
📂 Files in / consumed by this skill
scripts/build-spec.cjs— Node.js script that scans the project and emits<ProjectRoot>/.behaviourDocs/bt-spec.md. Invoked in Step 0.<ProjectRoot>/.behaviourDocs/bt-spec.md— compact generated catalog. Source of truth for node names,definitionId,btNodeType, property names, and type strings. Written by the script above; consumed by Steps 1–7.references/skeleton-minimal.json— smallest valid tree (empty Blackboard, single Composite root with no children).references/skeleton-full.json— Composite → Decorator → Action withnodeProperties(literal +Key-suffix) and a populatedBlackboard. Use this as the shape reference whenever the tree is non-trivial.references/node-catalog.md— narrative explanation ofbtNodeTypevalues, valid graph shapes, theKey-suffix convention, and how the spec builder discovers nodes (kept for reference; the runtime catalog itself lives inbt-spec.md).
---
🔁 Edit workflow (existing file)
- Read the entire file — never Edit blind. UUIDs and the parent/child graph must stay consistent.
- Never change the file's wrapper UUID (
EntryKey/ContentProto.Json.id) — external references break. - Adding a node: mint a fresh
nodeId, append toNodes, update the parent Composite'schildNodes, set the new node'snodeParentId. - Removing a node: remove from
Nodes, remove its ID from any Composite'schildNodes. If it was a Composite, decide whether to re-parent or remove its children — never leave danglingnodeParentIdreferences. - If the edit involves a custom node name, property, or type that may have changed in the project since the spec was last built, re-run Step 0 first.
- Re-run the Step 7 validation checklist after every edit.

