msw-behaviourtree
This skill end-to-end authors MSW `.behaviourtree` files and maintains project-specific authoring specifications. It scans codeblocks to build a catalog of custom behavior nodes, then generates the complete behavior tree, including node graphs, blackboard variables, and property wiring, with self-validation. This Skill significantly simplifies the creation and management of game AI behavior trees, ensuring project specification consistency and correctness.
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-behaviourtreeBefore / After Comparison
1 组Developers manually creating and updating complex behavior trees must meticulously verify custom nodes, UUIDs, property keys, and versioning, which is error-prone and time-consuming. Project spec maintenance also relies on manual effort, leading to inconsistencies and potential runtime issues.
This skill automates behavior tree generation and spec maintenance, scanning codeblocks to automatically build node catalogs and generating complete, validated behavior trees. This significantly reduces manual effort, ensures data consistency, and accelerates development iteration.
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).
- new / renamed / removed
- 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
...
User Reviews (0)
Write a Review
No reviews yet
Statistics
User Rating
Rate this Skill