behaviortrees 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/LICENSE-MIT +28 -0
- package/README.md +81 -2
- package/dist/chunks/chunk-7LANS4ZA.js +130 -0
- package/dist/chunks/chunk-QW4NPMSB.js +6 -0
- package/dist/chunks/chunk-VTXL3OVS.js +1902 -0
- package/dist/chunks/mcp-CHN7W7L5.js +8 -0
- package/dist/cli.js +169 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +24 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +43 -0
- package/package.json +63 -3
|
@@ -0,0 +1,1902 @@
|
|
|
1
|
+
// src/core/site.ts
|
|
2
|
+
var SITE = (process.env.BEHAVIORTREES_SITE ?? "https://www.behaviortrees.com").replace(/\/$/, "");
|
|
3
|
+
|
|
4
|
+
// ../examples/trees/cooldown-specials.json
|
|
5
|
+
var cooldown_specials_default = {
|
|
6
|
+
id: "cooldown-specials",
|
|
7
|
+
title: "Cooldown-Gated Special Attacks",
|
|
8
|
+
description: "Attack selection with a priority selector: a dramatic rocket attack gated by a cooldown decorator, falling back to shooting while ammo lasts, then melee. Note the cooldown wraps the whole rocket sequence, not just the range check.",
|
|
9
|
+
root: "n1",
|
|
10
|
+
properties: {},
|
|
11
|
+
custom_nodes: [
|
|
12
|
+
{ name: "Cooldown", category: "decorator", title: "Cooldown <milliseconds>ms", description: "Fails without running its child until the cooldown has elapsed since the last completed run.", properties: { milliseconds: 1e4 } },
|
|
13
|
+
{ name: "IsPlayerInRange", category: "condition", title: "Is Player In Range?", description: "Succeeds if the player is within the given range.", properties: { range: 8 } },
|
|
14
|
+
{ name: "FireRocket", category: "action", title: "Fire Rocket", description: "Fires the rocket launcher at the player.", properties: {} },
|
|
15
|
+
{ name: "HasAmmo", category: "condition", title: "Has Ammo?", description: "Succeeds if the primary weapon has ammunition left.", properties: {} },
|
|
16
|
+
{ name: "ShootPlayer", category: "action", title: "Shoot Player", description: "Fires the primary weapon at the player.", properties: {} },
|
|
17
|
+
{ name: "MeleeAttack", category: "action", title: "Melee Attack", description: "Unconditional fallback: swings at the player up close.", properties: {} }
|
|
18
|
+
],
|
|
19
|
+
nodes: {
|
|
20
|
+
n1: { id: "n1", name: "Priority", title: "Choose Attack", description: "Prefers the special whenever it is legal, degrading gracefully to basic attacks.", properties: {}, display: { x: 0, y: 0 }, children: ["n2", "n6", "n9"] },
|
|
21
|
+
n2: { id: "n2", name: "Cooldown", title: "Cooldown", description: "", properties: { milliseconds: 1e4 }, display: { x: 0, y: 0 }, child: "n3" },
|
|
22
|
+
n3: { id: "n3", name: "Sequence", title: "Rocket Attack", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n4", "n5"] },
|
|
23
|
+
n4: { id: "n4", name: "IsPlayerInRange", title: "Is Player In Range?", description: "", properties: { range: 8 }, display: { x: 0, y: 0 } },
|
|
24
|
+
n5: { id: "n5", name: "FireRocket", title: "Fire Rocket", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
25
|
+
n6: { id: "n6", name: "Sequence", title: "Shoot", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n7", "n8"] },
|
|
26
|
+
n7: { id: "n7", name: "HasAmmo", title: "Has Ammo?", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
27
|
+
n8: { id: "n8", name: "ShootPlayer", title: "Shoot Player", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
28
|
+
n9: { id: "n9", name: "MeleeAttack", title: "Melee Attack", description: "", properties: {}, display: { x: 0, y: 0 } }
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ../examples/trees/enemy-patrol.json
|
|
33
|
+
var enemy_patrol_default = {
|
|
34
|
+
id: "enemy-patrol",
|
|
35
|
+
title: "Enemy Patrol AI",
|
|
36
|
+
description: "A classic enemy NPC: attack the player when in range, chase when visible, otherwise patrol between waypoints.",
|
|
37
|
+
root: "n1",
|
|
38
|
+
properties: {},
|
|
39
|
+
custom_nodes: [
|
|
40
|
+
{ name: "IsPlayerInRange", category: "condition", title: "Is Player In Range?", description: "Succeeds if the player is within attack range.", properties: { range: 2 } },
|
|
41
|
+
{ name: "AttackPlayer", category: "action", title: "Attack Player", description: "Performs an attack against the player.", properties: {} },
|
|
42
|
+
{ name: "IsPlayerVisible", category: "condition", title: "Is Player Visible?", description: "Succeeds if the player is within sight distance and line of sight.", properties: { distance: 10 } },
|
|
43
|
+
{ name: "MoveToPlayer", category: "action", title: "Move To Player", description: "Moves the agent toward the player's position.", properties: { speed: 4 } },
|
|
44
|
+
{ name: "MoveToWaypoint", category: "action", title: "Move To Waypoint", description: "Moves the agent toward the current patrol waypoint.", properties: { speed: 2 } },
|
|
45
|
+
{ name: "NextWaypoint", category: "action", title: "Next Waypoint", description: "Advances the patrol route to the next waypoint.", properties: {} }
|
|
46
|
+
],
|
|
47
|
+
nodes: {
|
|
48
|
+
n1: { id: "n1", name: "Priority", title: "Enemy AI", description: "Tries each behavior in order until one succeeds.", properties: {}, display: { x: 0, y: 0 }, children: ["n2", "n5", "n8"] },
|
|
49
|
+
n2: { id: "n2", name: "Sequence", title: "Attack", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n3", "n4"] },
|
|
50
|
+
n3: { id: "n3", name: "IsPlayerInRange", title: "Is Player In Range?", description: "", properties: { range: 2 }, display: { x: 0, y: 0 } },
|
|
51
|
+
n4: { id: "n4", name: "AttackPlayer", title: "Attack Player", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
52
|
+
n5: { id: "n5", name: "Sequence", title: "Chase", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n6", "n7"] },
|
|
53
|
+
n6: { id: "n6", name: "IsPlayerVisible", title: "Is Player Visible?", description: "", properties: { distance: 10 }, display: { x: 0, y: 0 } },
|
|
54
|
+
n7: { id: "n7", name: "MoveToPlayer", title: "Move To Player", description: "", properties: { speed: 4 }, display: { x: 0, y: 0 } },
|
|
55
|
+
n8: { id: "n8", name: "Sequence", title: "Patrol", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n9", "n10", "n11"] },
|
|
56
|
+
n9: { id: "n9", name: "MoveToWaypoint", title: "Move To Waypoint", description: "", properties: { speed: 2 }, display: { x: 0, y: 0 } },
|
|
57
|
+
n10: { id: "n10", name: "Wait", title: "Wait", description: "", properties: { milliseconds: 2e3 }, display: { x: 0, y: 0 } },
|
|
58
|
+
n11: { id: "n11", name: "NextWaypoint", title: "Next Waypoint", description: "", properties: {}, display: { x: 0, y: 0 } }
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ../examples/trees/open-the-door.json
|
|
63
|
+
var open_the_door_default = {
|
|
64
|
+
id: "open-the-door",
|
|
65
|
+
title: "Open The Door (Selector vs Sequence)",
|
|
66
|
+
description: "The classic teaching example: a selector tries strategies in priority order, and each strategy is a sequence of steps that must all succeed.",
|
|
67
|
+
root: "n1",
|
|
68
|
+
properties: {},
|
|
69
|
+
custom_nodes: [
|
|
70
|
+
{ name: "IsDoorOpen", category: "condition", title: "Is Door Open?", description: "Succeeds if the door is already open.", properties: {} },
|
|
71
|
+
{ name: "WalkThrough", category: "action", title: "Walk Through Door", description: "Walks the agent through the doorway.", properties: {} },
|
|
72
|
+
{ name: "HasKey", category: "condition", title: "Has Key?", description: "Succeeds if the agent carries the door key.", properties: {} },
|
|
73
|
+
{ name: "UnlockDoor", category: "action", title: "Unlock Door", description: "Unlocks the door with the key.", properties: {} },
|
|
74
|
+
{ name: "OpenDoor", category: "action", title: "Open Door", description: "Opens the unlocked door.", properties: {} },
|
|
75
|
+
{ name: "SmashDoor", category: "action", title: "Smash Door", description: "Breaks the door down as a last resort.", properties: { strengthRequired: 8 } }
|
|
76
|
+
],
|
|
77
|
+
nodes: {
|
|
78
|
+
n1: { id: "n1", name: "Priority", title: "Enter The Room", description: "Tries the cheapest strategy first, falling back to more drastic ones.", properties: {}, display: { x: 0, y: 0 }, children: ["n2", "n5", "n10"] },
|
|
79
|
+
n2: { id: "n2", name: "Sequence", title: "Just Walk In", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n3", "n4"] },
|
|
80
|
+
n3: { id: "n3", name: "IsDoorOpen", title: "Is Door Open?", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
81
|
+
n4: { id: "n4", name: "WalkThrough", title: "Walk Through Door", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
82
|
+
n5: { id: "n5", name: "Sequence", title: "Use The Key", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n6", "n7", "n8", "n9"] },
|
|
83
|
+
n6: { id: "n6", name: "HasKey", title: "Has Key?", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
84
|
+
n7: { id: "n7", name: "UnlockDoor", title: "Unlock Door", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
85
|
+
n8: { id: "n8", name: "OpenDoor", title: "Open Door", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
86
|
+
n9: { id: "n9", name: "WalkThrough", title: "Walk Through Door", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
87
|
+
n10: { id: "n10", name: "Sequence", title: "Brute Force", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n11", "n12"] },
|
|
88
|
+
n11: { id: "n11", name: "SmashDoor", title: "Smash Door", description: "", properties: { strengthRequired: 8 }, display: { x: 0, y: 0 } },
|
|
89
|
+
n12: { id: "n12", name: "WalkThrough", title: "Walk Through Door", description: "", properties: {}, display: { x: 0, y: 0 } }
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ../examples/trees/robot-navigate-recovery.json
|
|
94
|
+
var robot_navigate_recovery_default = {
|
|
95
|
+
id: "robot-navigate-recovery",
|
|
96
|
+
title: "Robot Navigation With Recovery",
|
|
97
|
+
description: "A Nav2-style navigate-to-pose tree: plan a path (with retries) and follow it; when navigation fails, run recovery behaviors in escalating fallback order \u2014 clear costmaps, spin, back up, wait \u2014 then retry navigation.",
|
|
98
|
+
root: "n1",
|
|
99
|
+
properties: {},
|
|
100
|
+
custom_nodes: [
|
|
101
|
+
{ name: "ComputePathToPose", category: "action", title: "Compute Path To Pose", description: "Asks the planner for a path from the robot to the goal pose.", properties: { goal: "goal_pose" } },
|
|
102
|
+
{ name: "FollowPath", category: "action", title: "Follow Path", description: "Drives the robot along the planned path; fails if the robot gets stuck or the path becomes blocked.", properties: { controller: "FollowPath" } },
|
|
103
|
+
{ name: "ClearCostmaps", category: "action", title: "Clear Costmaps", description: "Cheapest recovery: wipes stale obstacle data so planning can see freed space.", properties: {} },
|
|
104
|
+
{ name: "Spin", category: "action", title: "Spin In Place", description: "Rotates in place to refresh sensor coverage around the robot.", properties: { degrees: 90 } },
|
|
105
|
+
{ name: "BackUp", category: "action", title: "Back Up", description: "Reverses a short distance to escape a tight spot.", properties: { distance: 0.3 } }
|
|
106
|
+
],
|
|
107
|
+
nodes: {
|
|
108
|
+
n1: { id: "n1", name: "RepeatUntilSuccess", title: "Retry Navigation", description: "The whole attempt loops until navigation succeeds or the retry budget runs out.", properties: { maxLoop: 6 }, display: { x: 0, y: 0 }, child: "n2" },
|
|
109
|
+
n2: { id: "n2", name: "Priority", title: "Navigate Or Recover", description: "Try to navigate; only when that fails does a recovery run.", properties: {}, display: { x: 0, y: 0 }, children: ["n3", "n7"] },
|
|
110
|
+
n3: { id: "n3", name: "MemSequence", title: "Navigate", description: "A procedural checklist, so a long FollowPath is not restarted every tick.", properties: {}, display: { x: 0, y: 0 }, children: ["n4", "n6"] },
|
|
111
|
+
n4: { id: "n4", name: "RepeatUntilSuccess", title: "Retry Planning", description: "", properties: { maxLoop: 3 }, display: { x: 0, y: 0 }, child: "n5" },
|
|
112
|
+
n5: { id: "n5", name: "ComputePathToPose", title: "Compute Path To Pose", description: "", properties: { goal: "goal_pose" }, display: { x: 0, y: 0 } },
|
|
113
|
+
n6: { id: "n6", name: "FollowPath", title: "Follow Path", description: "", properties: { controller: "FollowPath" }, display: { x: 0, y: 0 } },
|
|
114
|
+
n7: { id: "n7", name: "Sequence", title: "Recover", description: "Runs one recovery, then deliberately fails so the retry loop navigates again.", properties: {}, display: { x: 0, y: 0 }, children: ["n8", "n13"] },
|
|
115
|
+
n8: { id: "n8", name: "Priority", title: "Recovery Behaviors", description: "Escalating fallbacks, cheapest first.", properties: {}, display: { x: 0, y: 0 }, children: ["n9", "n10", "n11", "n12"] },
|
|
116
|
+
n9: { id: "n9", name: "ClearCostmaps", title: "Clear Costmaps", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
117
|
+
n10: { id: "n10", name: "Spin", title: "Spin In Place", description: "", properties: { degrees: 90 }, display: { x: 0, y: 0 } },
|
|
118
|
+
n11: { id: "n11", name: "BackUp", title: "Back Up", description: "", properties: { distance: 0.3 }, display: { x: 0, y: 0 } },
|
|
119
|
+
n12: { id: "n12", name: "Wait", title: "Wait", description: "Last resort: wait for the world to change (a door to open, a person to pass).", properties: { milliseconds: 5e3 }, display: { x: 0, y: 0 } },
|
|
120
|
+
n13: { id: "n13", name: "Failer", title: "Recovery Done \u2014 Go Retry", description: "Forces the branch to fail so RepeatUntilSuccess ticks navigation again.", properties: {}, display: { x: 0, y: 0 } }
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ../examples/trees/robot-pick-and-place.json
|
|
125
|
+
var robot_pick_and_place_default = {
|
|
126
|
+
id: "robot-pick-and-place",
|
|
127
|
+
title: "Robot Pick And Place",
|
|
128
|
+
description: "A robotics-style task: acquire an object if not already holding one, carry it to the target, and place it. Grasping retries until it succeeds.",
|
|
129
|
+
root: "n1",
|
|
130
|
+
properties: {},
|
|
131
|
+
custom_nodes: [
|
|
132
|
+
{ name: "IsHoldingObject", category: "condition", title: "Is Holding Object?", description: "Succeeds if the gripper currently holds the object.", properties: {} },
|
|
133
|
+
{ name: "MoveToObject", category: "action", title: "Move To Object", description: "Plans and executes a motion to the object's pose.", properties: { tolerance: 0.01 } },
|
|
134
|
+
{ name: "GraspObject", category: "action", title: "Grasp Object", description: "Closes the gripper on the object.", properties: { force: 20 } },
|
|
135
|
+
{ name: "MoveToTarget", category: "action", title: "Move To Target", description: "Carries the object to the target location.", properties: { tolerance: 0.01 } },
|
|
136
|
+
{ name: "PlaceObject", category: "action", title: "Place Object", description: "Releases the object at the target pose.", properties: {} }
|
|
137
|
+
],
|
|
138
|
+
nodes: {
|
|
139
|
+
n1: { id: "n1", name: "Sequence", title: "Pick And Place", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n2", "n7", "n8"] },
|
|
140
|
+
n2: { id: "n2", name: "Priority", title: "Ensure Holding Object", description: "Skip acquisition if the object is already held.", properties: {}, display: { x: 0, y: 0 }, children: ["n3", "n4"] },
|
|
141
|
+
n3: { id: "n3", name: "IsHoldingObject", title: "Is Holding Object?", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
142
|
+
n4: { id: "n4", name: "Sequence", title: "Acquire Object", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n5", "n6"] },
|
|
143
|
+
n5: { id: "n5", name: "MoveToObject", title: "Move To Object", description: "", properties: { tolerance: 0.01 }, display: { x: 0, y: 0 } },
|
|
144
|
+
n6: { id: "n6", name: "RepeatUntilSuccess", title: "Retry Grasp", description: "", properties: { maxLoop: 3 }, display: { x: 0, y: 0 }, child: "n6a" },
|
|
145
|
+
n6a: { id: "n6a", name: "GraspObject", title: "Grasp Object", description: "", properties: { force: 20 }, display: { x: 0, y: 0 } },
|
|
146
|
+
n7: { id: "n7", name: "MoveToTarget", title: "Move To Target", description: "", properties: { tolerance: 0.01 }, display: { x: 0, y: 0 } },
|
|
147
|
+
n8: { id: "n8", name: "PlaceObject", title: "Place Object", description: "", properties: {}, display: { x: 0, y: 0 } }
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ../examples/trees/survival-override.json
|
|
152
|
+
var survival_override_default = {
|
|
153
|
+
id: "survival-override",
|
|
154
|
+
title: "Survival Override Enemy",
|
|
155
|
+
description: "A full enemy brain with a top-priority survival branch: when health runs low the agent abandons whatever it was doing to flee, heal, or fight cornered \u2014 no transitions required.",
|
|
156
|
+
root: "n1",
|
|
157
|
+
properties: {},
|
|
158
|
+
custom_nodes: [
|
|
159
|
+
{ name: "IsHealthLow", category: "condition", title: "Is Health Low?", description: "Succeeds if health is below the threshold.", properties: { threshold: 30 } },
|
|
160
|
+
{ name: "FleeToCover", category: "action", title: "Flee To Cover", description: "Runs to the nearest cover point away from the threat.", properties: { speed: 5 } },
|
|
161
|
+
{ name: "UseHealthItem", category: "action", title: "Use Health Item", description: "Consumes a health item if one is carried; fails otherwise.", properties: {} },
|
|
162
|
+
{ name: "FightCornered", category: "action", title: "Fight Cornered", description: "Last resort: attacks desperately when there is no escape.", properties: {} },
|
|
163
|
+
{ name: "IsPlayerInRange", category: "condition", title: "Is Player In Range?", description: "Succeeds if the player is within attack range.", properties: { range: 2 } },
|
|
164
|
+
{ name: "AttackPlayer", category: "action", title: "Attack Player", description: "Performs an attack against the player.", properties: {} },
|
|
165
|
+
{ name: "IsPlayerVisible", category: "condition", title: "Is Player Visible?", description: "Succeeds if the player is within sight distance and line of sight.", properties: { distance: 10 } },
|
|
166
|
+
{ name: "MoveToPlayer", category: "action", title: "Move To Player", description: "Moves the agent toward the player's position.", properties: { speed: 4 } },
|
|
167
|
+
{ name: "MoveToWaypoint", category: "action", title: "Move To Waypoint", description: "Moves the agent toward the current patrol waypoint.", properties: { speed: 2 } },
|
|
168
|
+
{ name: "NextWaypoint", category: "action", title: "Next Waypoint", description: "Advances the patrol route to the next waypoint.", properties: {} }
|
|
169
|
+
],
|
|
170
|
+
nodes: {
|
|
171
|
+
n1: { id: "n1", name: "Priority", title: "Enemy AI", description: "The survival branch is evaluated first on every tick, so it preempts everything below it.", properties: {}, display: { x: 0, y: 0 }, children: ["n2", "n7", "n10", "n13"] },
|
|
172
|
+
n2: { id: "n2", name: "Sequence", title: "Survive", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n3", "n4"] },
|
|
173
|
+
n3: { id: "n3", name: "IsHealthLow", title: "Is Health Low?", description: "", properties: { threshold: 30 }, display: { x: 0, y: 0 } },
|
|
174
|
+
n4: { id: "n4", name: "Priority", title: "Stay Alive", description: "Escalating survival strategies, cheapest first.", properties: {}, display: { x: 0, y: 0 }, children: ["n5", "n6", "n6b"] },
|
|
175
|
+
n5: { id: "n5", name: "FleeToCover", title: "Flee To Cover", description: "", properties: { speed: 5 }, display: { x: 0, y: 0 } },
|
|
176
|
+
n6: { id: "n6", name: "UseHealthItem", title: "Use Health Item", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
177
|
+
n6b: { id: "n6b", name: "FightCornered", title: "Fight Cornered", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
178
|
+
n7: { id: "n7", name: "Sequence", title: "Attack", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n8", "n9"] },
|
|
179
|
+
n8: { id: "n8", name: "IsPlayerInRange", title: "Is Player In Range?", description: "", properties: { range: 2 }, display: { x: 0, y: 0 } },
|
|
180
|
+
n9: { id: "n9", name: "AttackPlayer", title: "Attack Player", description: "", properties: {}, display: { x: 0, y: 0 } },
|
|
181
|
+
n10: { id: "n10", name: "Sequence", title: "Chase", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n11", "n12"] },
|
|
182
|
+
n11: { id: "n11", name: "IsPlayerVisible", title: "Is Player Visible?", description: "", properties: { distance: 10 }, display: { x: 0, y: 0 } },
|
|
183
|
+
n12: { id: "n12", name: "MoveToPlayer", title: "Move To Player", description: "", properties: { speed: 4 }, display: { x: 0, y: 0 } },
|
|
184
|
+
n13: { id: "n13", name: "Sequence", title: "Patrol", description: "", properties: {}, display: { x: 0, y: 0 }, children: ["n14", "n15", "n16"] },
|
|
185
|
+
n14: { id: "n14", name: "MoveToWaypoint", title: "Move To Waypoint", description: "", properties: { speed: 2 }, display: { x: 0, y: 0 } },
|
|
186
|
+
n15: { id: "n15", name: "Wait", title: "Wait", description: "", properties: { milliseconds: 2e3 }, display: { x: 0, y: 0 } },
|
|
187
|
+
n16: { id: "n16", name: "NextWaypoint", title: "Next Waypoint", description: "", properties: {}, display: { x: 0, y: 0 } }
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ../../apps/editor/src/lib/behavior/defaults.ts
|
|
192
|
+
var DEFAULT_NODES = {
|
|
193
|
+
Root: {
|
|
194
|
+
name: "Root",
|
|
195
|
+
title: "Root",
|
|
196
|
+
category: "root",
|
|
197
|
+
description: "The entry point of the behavior tree",
|
|
198
|
+
properties: {},
|
|
199
|
+
isDefault: true
|
|
200
|
+
},
|
|
201
|
+
// Composites
|
|
202
|
+
Sequence: {
|
|
203
|
+
name: "Sequence",
|
|
204
|
+
title: "Sequence",
|
|
205
|
+
category: "composite",
|
|
206
|
+
description: "Runs children in order; fails when one fails (AND)",
|
|
207
|
+
properties: {},
|
|
208
|
+
isDefault: true
|
|
209
|
+
},
|
|
210
|
+
Priority: {
|
|
211
|
+
name: "Priority",
|
|
212
|
+
title: "Priority",
|
|
213
|
+
category: "composite",
|
|
214
|
+
description: "Runs children in order; succeeds when one succeeds (OR / selector)",
|
|
215
|
+
properties: {},
|
|
216
|
+
isDefault: true
|
|
217
|
+
},
|
|
218
|
+
MemSequence: {
|
|
219
|
+
name: "MemSequence",
|
|
220
|
+
title: "MemSequence",
|
|
221
|
+
category: "composite",
|
|
222
|
+
description: "Sequence with memory: resumes from the running child",
|
|
223
|
+
properties: {},
|
|
224
|
+
isDefault: true
|
|
225
|
+
},
|
|
226
|
+
MemPriority: {
|
|
227
|
+
name: "MemPriority",
|
|
228
|
+
title: "MemPriority",
|
|
229
|
+
category: "composite",
|
|
230
|
+
description: "Priority with memory: resumes from the running child",
|
|
231
|
+
properties: {},
|
|
232
|
+
isDefault: true
|
|
233
|
+
},
|
|
234
|
+
// Decorators
|
|
235
|
+
Repeater: {
|
|
236
|
+
name: "Repeater",
|
|
237
|
+
title: "Repeat <maxLoop>x",
|
|
238
|
+
category: "decorator",
|
|
239
|
+
description: "Repeats the child up to maxLoop times (-1 = forever)",
|
|
240
|
+
properties: { maxLoop: -1 },
|
|
241
|
+
isDefault: true
|
|
242
|
+
},
|
|
243
|
+
RepeatUntilFailure: {
|
|
244
|
+
name: "RepeatUntilFailure",
|
|
245
|
+
title: "Repeat Until Failure",
|
|
246
|
+
category: "decorator",
|
|
247
|
+
description: "Repeats the child until it fails",
|
|
248
|
+
properties: { maxLoop: -1 },
|
|
249
|
+
isDefault: true
|
|
250
|
+
},
|
|
251
|
+
RepeatUntilSuccess: {
|
|
252
|
+
name: "RepeatUntilSuccess",
|
|
253
|
+
title: "Repeat Until Success",
|
|
254
|
+
category: "decorator",
|
|
255
|
+
description: "Repeats the child until it succeeds",
|
|
256
|
+
properties: { maxLoop: -1 },
|
|
257
|
+
isDefault: true
|
|
258
|
+
},
|
|
259
|
+
MaxTime: {
|
|
260
|
+
name: "MaxTime",
|
|
261
|
+
title: "Max <maxTime>ms",
|
|
262
|
+
category: "decorator",
|
|
263
|
+
description: "Fails the child if it runs longer than maxTime milliseconds",
|
|
264
|
+
properties: { maxTime: 0 },
|
|
265
|
+
isDefault: true
|
|
266
|
+
},
|
|
267
|
+
Inverter: {
|
|
268
|
+
name: "Inverter",
|
|
269
|
+
title: "Inverter",
|
|
270
|
+
category: "decorator",
|
|
271
|
+
description: "Inverts the result of the child",
|
|
272
|
+
properties: {},
|
|
273
|
+
isDefault: true
|
|
274
|
+
},
|
|
275
|
+
Limiter: {
|
|
276
|
+
name: "Limiter",
|
|
277
|
+
title: "Limit <maxLoop> Activations",
|
|
278
|
+
category: "decorator",
|
|
279
|
+
description: "Limits how many times the child can be executed",
|
|
280
|
+
properties: { maxLoop: 1 },
|
|
281
|
+
isDefault: true
|
|
282
|
+
},
|
|
283
|
+
// Actions
|
|
284
|
+
Failer: {
|
|
285
|
+
name: "Failer",
|
|
286
|
+
title: "Failer",
|
|
287
|
+
category: "action",
|
|
288
|
+
description: "Always returns failure",
|
|
289
|
+
properties: {},
|
|
290
|
+
isDefault: true
|
|
291
|
+
},
|
|
292
|
+
Succeeder: {
|
|
293
|
+
name: "Succeeder",
|
|
294
|
+
title: "Succeeder",
|
|
295
|
+
category: "action",
|
|
296
|
+
description: "Always returns success",
|
|
297
|
+
properties: {},
|
|
298
|
+
isDefault: true
|
|
299
|
+
},
|
|
300
|
+
Runner: {
|
|
301
|
+
name: "Runner",
|
|
302
|
+
title: "Runner",
|
|
303
|
+
category: "action",
|
|
304
|
+
description: "Always returns running",
|
|
305
|
+
properties: {},
|
|
306
|
+
isDefault: true
|
|
307
|
+
},
|
|
308
|
+
Error: {
|
|
309
|
+
name: "Error",
|
|
310
|
+
title: "Error",
|
|
311
|
+
category: "action",
|
|
312
|
+
description: "Always returns an error status",
|
|
313
|
+
properties: {},
|
|
314
|
+
isDefault: true
|
|
315
|
+
},
|
|
316
|
+
Wait: {
|
|
317
|
+
name: "Wait",
|
|
318
|
+
title: "Wait <milliseconds>ms",
|
|
319
|
+
category: "action",
|
|
320
|
+
description: "Waits for the given number of milliseconds",
|
|
321
|
+
properties: { milliseconds: 0 },
|
|
322
|
+
isDefault: true
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/stringify.js
|
|
327
|
+
var byteToHex = [];
|
|
328
|
+
for (let i = 0; i < 256; ++i) {
|
|
329
|
+
byteToHex.push((i + 256).toString(16).slice(1));
|
|
330
|
+
}
|
|
331
|
+
function unsafeStringify(arr, offset = 0) {
|
|
332
|
+
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/rng.js
|
|
336
|
+
import { randomFillSync } from "crypto";
|
|
337
|
+
var rnds8Pool = new Uint8Array(256);
|
|
338
|
+
var poolPtr = rnds8Pool.length;
|
|
339
|
+
function rng() {
|
|
340
|
+
if (poolPtr > rnds8Pool.length - 16) {
|
|
341
|
+
randomFillSync(rnds8Pool);
|
|
342
|
+
poolPtr = 0;
|
|
343
|
+
}
|
|
344
|
+
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/native.js
|
|
348
|
+
import { randomUUID } from "crypto";
|
|
349
|
+
var native_default = { randomUUID };
|
|
350
|
+
|
|
351
|
+
// ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/v4.js
|
|
352
|
+
function v4(options, buf, offset) {
|
|
353
|
+
if (native_default.randomUUID && !buf && !options) {
|
|
354
|
+
return native_default.randomUUID();
|
|
355
|
+
}
|
|
356
|
+
options = options || {};
|
|
357
|
+
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
358
|
+
if (rnds.length < 16) {
|
|
359
|
+
throw new Error("Random bytes length must be >= 16");
|
|
360
|
+
}
|
|
361
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
362
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
363
|
+
if (buf) {
|
|
364
|
+
offset = offset || 0;
|
|
365
|
+
if (offset < 0 || offset + 16 > buf.length) {
|
|
366
|
+
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
367
|
+
}
|
|
368
|
+
for (let i = 0; i < 16; ++i) {
|
|
369
|
+
buf[offset + i] = rnds[i];
|
|
370
|
+
}
|
|
371
|
+
return buf;
|
|
372
|
+
}
|
|
373
|
+
return unsafeStringify(rnds);
|
|
374
|
+
}
|
|
375
|
+
var v4_default = v4;
|
|
376
|
+
|
|
377
|
+
// ../../apps/editor/src/lib/behavior/import-error.ts
|
|
378
|
+
var InvalidFileError = class extends Error {
|
|
379
|
+
reason;
|
|
380
|
+
// Short parser message, safe to send to analytics
|
|
381
|
+
detail;
|
|
382
|
+
constructor(message, reason, detail) {
|
|
383
|
+
super(message);
|
|
384
|
+
this.name = "InvalidFileError";
|
|
385
|
+
this.reason = reason;
|
|
386
|
+
this.detail = detail;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// ../../apps/editor/src/lib/behavior/organize.ts
|
|
391
|
+
var LAYOUT_STORAGE_KEY = "bt-layout";
|
|
392
|
+
function readTreeLayout() {
|
|
393
|
+
try {
|
|
394
|
+
return localStorage.getItem(LAYOUT_STORAGE_KEY) === "vertical" ? "vertical" : "horizontal";
|
|
395
|
+
} catch {
|
|
396
|
+
return "horizontal";
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function compareSiblings(layout) {
|
|
400
|
+
return (a, b) => layout === "horizontal" ? a.y - b.y || a.x - b.x : a.x - b.x || a.y - b.y;
|
|
401
|
+
}
|
|
402
|
+
function childIdsInOrder(tree, blockId, layout) {
|
|
403
|
+
const byPosition = compareSiblings(layout);
|
|
404
|
+
return Object.values(tree.connections).filter((c) => c.source === blockId).map((c) => tree.blocks[c.target]).filter((b) => !!b).sort((a, b) => byPosition(a.position, b.position)).map((b) => b.id);
|
|
405
|
+
}
|
|
406
|
+
var H_SPACING = 208;
|
|
407
|
+
var V_SPACING = 88;
|
|
408
|
+
var V_COMPENSATION = 42;
|
|
409
|
+
function organizeTree(tree, layout = "horizontal", byIndex = false, orderLayout = layout) {
|
|
410
|
+
if (!tree.rootId || !tree.blocks[tree.rootId]) return;
|
|
411
|
+
const root = tree.blocks[tree.rootId];
|
|
412
|
+
const offsetX = root.position.x;
|
|
413
|
+
const offsetY = root.position.y;
|
|
414
|
+
const childIdsOf = (blockId) => byIndex ? Object.values(tree.connections).filter((c) => c.source === blockId && !!tree.blocks[c.target]).map((c) => c.target) : childIdsInOrder(tree, blockId, orderLayout);
|
|
415
|
+
let leafCount = 0;
|
|
416
|
+
const visited = /* @__PURE__ */ new Set();
|
|
417
|
+
const placed = [];
|
|
418
|
+
const step = (blockId, depth) => {
|
|
419
|
+
if (visited.has(blockId)) return 0;
|
|
420
|
+
visited.add(blockId);
|
|
421
|
+
placed.push(blockId);
|
|
422
|
+
const block = tree.blocks[blockId];
|
|
423
|
+
const children = childIdsOf(blockId);
|
|
424
|
+
let cross;
|
|
425
|
+
if (children.length === 0) {
|
|
426
|
+
leafCount += 1;
|
|
427
|
+
cross = leafCount * (layout === "horizontal" ? V_SPACING : H_SPACING);
|
|
428
|
+
} else {
|
|
429
|
+
let sum = 0;
|
|
430
|
+
children.forEach((childId) => {
|
|
431
|
+
sum += step(childId, depth + 1);
|
|
432
|
+
});
|
|
433
|
+
cross = sum / children.length;
|
|
434
|
+
}
|
|
435
|
+
if (layout === "horizontal") {
|
|
436
|
+
block.position = { x: depth * H_SPACING, y: cross };
|
|
437
|
+
} else {
|
|
438
|
+
block.position = { x: cross, y: depth * (V_SPACING + V_COMPENSATION) };
|
|
439
|
+
}
|
|
440
|
+
return cross;
|
|
441
|
+
};
|
|
442
|
+
step(tree.rootId, 0);
|
|
443
|
+
const dx = offsetX - root.position.x;
|
|
444
|
+
const dy = offsetY - root.position.y;
|
|
445
|
+
placed.forEach((id) => {
|
|
446
|
+
const block = tree.blocks[id];
|
|
447
|
+
block.position = { x: block.position.x + dx, y: block.position.y + dy };
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ../../apps/editor/src/lib/behavior/serializer.ts
|
|
452
|
+
function deserializeTree(serializedTree) {
|
|
453
|
+
const tree = {
|
|
454
|
+
id: serializedTree.id,
|
|
455
|
+
title: serializedTree.title,
|
|
456
|
+
description: serializedTree.description,
|
|
457
|
+
rootId: serializedTree.rootId,
|
|
458
|
+
blocks: {},
|
|
459
|
+
connections: {},
|
|
460
|
+
viewport: serializedTree.viewport
|
|
461
|
+
};
|
|
462
|
+
Object.values(serializedTree.blocks).forEach((block) => {
|
|
463
|
+
const newBlock = {
|
|
464
|
+
id: block.id,
|
|
465
|
+
name: block.name,
|
|
466
|
+
title: block.title,
|
|
467
|
+
category: block.category || "action",
|
|
468
|
+
// Default to action if not specified
|
|
469
|
+
description: block.description,
|
|
470
|
+
properties: { ...block.properties },
|
|
471
|
+
position: block.position
|
|
472
|
+
};
|
|
473
|
+
tree.blocks[block.id] = newBlock;
|
|
474
|
+
});
|
|
475
|
+
Object.values(serializedTree.blocks).forEach((block) => {
|
|
476
|
+
if (block.children && Array.isArray(block.children)) {
|
|
477
|
+
block.children.forEach((childId) => {
|
|
478
|
+
const connectionId = `${block.id}-${childId}`;
|
|
479
|
+
const connection = {
|
|
480
|
+
id: connectionId,
|
|
481
|
+
source: block.id,
|
|
482
|
+
target: childId
|
|
483
|
+
};
|
|
484
|
+
tree.connections[connectionId] = connection;
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
return tree;
|
|
489
|
+
}
|
|
490
|
+
function deserializeProject(serializedProject) {
|
|
491
|
+
const project = {
|
|
492
|
+
id: serializedProject.id,
|
|
493
|
+
name: serializedProject.name,
|
|
494
|
+
description: serializedProject.description,
|
|
495
|
+
trees: {},
|
|
496
|
+
nodes: {},
|
|
497
|
+
// Will be populated with default + custom nodes
|
|
498
|
+
selectedTreeId: serializedProject.selectedTreeId,
|
|
499
|
+
createdAt: serializedProject.createdAt,
|
|
500
|
+
updatedAt: serializedProject.updatedAt
|
|
501
|
+
};
|
|
502
|
+
Object.values(serializedProject.trees).forEach((serializedTree) => {
|
|
503
|
+
const tree = deserializeTree(serializedTree);
|
|
504
|
+
project.trees[tree.id] = tree;
|
|
505
|
+
});
|
|
506
|
+
if (serializedProject.customNodes) {
|
|
507
|
+
Object.entries(serializedProject.customNodes).forEach(([key, node]) => {
|
|
508
|
+
project.nodes[key] = {
|
|
509
|
+
name: node.name,
|
|
510
|
+
category: node.category,
|
|
511
|
+
title: node.title,
|
|
512
|
+
description: node.description,
|
|
513
|
+
properties: node.properties,
|
|
514
|
+
isDefault: false
|
|
515
|
+
};
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return project;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ../../apps/editor/src/lib/behavior/b3.ts
|
|
522
|
+
var B3_VERSION = "0.3.0";
|
|
523
|
+
function treeToB3(tree, project, includeCustomNodes, layout = readTreeLayout()) {
|
|
524
|
+
const root = tree.rootId ? tree.blocks[tree.rootId] : void 0;
|
|
525
|
+
const rootChildren = root ? childIdsInOrder(tree, root.id, layout) : [];
|
|
526
|
+
const data = {
|
|
527
|
+
version: B3_VERSION,
|
|
528
|
+
scope: "tree",
|
|
529
|
+
id: tree.id,
|
|
530
|
+
title: tree.title,
|
|
531
|
+
description: tree.description ?? "",
|
|
532
|
+
root: rootChildren[0] ?? null,
|
|
533
|
+
properties: { ...tree.properties ?? {} },
|
|
534
|
+
nodes: {},
|
|
535
|
+
display: {
|
|
536
|
+
camera_x: tree.viewport.x,
|
|
537
|
+
camera_y: tree.viewport.y,
|
|
538
|
+
camera_z: tree.viewport.zoom,
|
|
539
|
+
x: root?.position.x ?? 0,
|
|
540
|
+
y: root?.position.y ?? 0
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
Object.values(tree.blocks).forEach((block) => {
|
|
544
|
+
if (block.category === "root") return;
|
|
545
|
+
const spec = {
|
|
546
|
+
id: block.id,
|
|
547
|
+
name: block.name,
|
|
548
|
+
title: block.title ?? block.name,
|
|
549
|
+
description: block.description ?? "",
|
|
550
|
+
properties: { ...block.properties },
|
|
551
|
+
display: { x: block.position.x, y: block.position.y }
|
|
552
|
+
};
|
|
553
|
+
const children = childIdsInOrder(tree, block.id, layout);
|
|
554
|
+
if (block.category === "composite") {
|
|
555
|
+
spec.children = children;
|
|
556
|
+
} else if (block.category === "decorator") {
|
|
557
|
+
spec.child = children[0];
|
|
558
|
+
}
|
|
559
|
+
data.nodes[block.id] = spec;
|
|
560
|
+
});
|
|
561
|
+
if (includeCustomNodes && project) {
|
|
562
|
+
data.custom_nodes = customNodesToB3(project);
|
|
563
|
+
}
|
|
564
|
+
return data;
|
|
565
|
+
}
|
|
566
|
+
function customNodesToB3(project) {
|
|
567
|
+
return Object.values(project.nodes).filter((node) => !node.isDefault).map((node) => ({
|
|
568
|
+
version: B3_VERSION,
|
|
569
|
+
scope: "node",
|
|
570
|
+
name: node.name,
|
|
571
|
+
category: node.category,
|
|
572
|
+
title: node.title,
|
|
573
|
+
description: node.description,
|
|
574
|
+
properties: { ...node.properties },
|
|
575
|
+
// Omitted when absent so behavior3-only files stay byte-identical
|
|
576
|
+
...node.ports ? { ports: { ...node.ports } } : {}
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
function projectToB3(project) {
|
|
580
|
+
return {
|
|
581
|
+
version: B3_VERSION,
|
|
582
|
+
scope: "project",
|
|
583
|
+
selectedTree: project.selectedTreeId ?? null,
|
|
584
|
+
trees: Object.values(project.trees).map((tree) => treeToB3(tree, null, false)),
|
|
585
|
+
custom_nodes: customNodesToB3(project),
|
|
586
|
+
id: project.id,
|
|
587
|
+
name: project.name,
|
|
588
|
+
description: project.description,
|
|
589
|
+
createdAt: project.createdAt,
|
|
590
|
+
updatedAt: project.updatedAt
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function templateToNode(t) {
|
|
594
|
+
return {
|
|
595
|
+
name: t.name,
|
|
596
|
+
category: t.category ?? "action",
|
|
597
|
+
title: t.title,
|
|
598
|
+
description: t.description,
|
|
599
|
+
properties: { ...t.properties ?? {} },
|
|
600
|
+
...t.ports ? { ports: { ...t.ports } } : {},
|
|
601
|
+
isDefault: false
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function inferCategory(spec) {
|
|
605
|
+
if (spec.children) return "composite";
|
|
606
|
+
if (spec.child !== void 0) return "decorator";
|
|
607
|
+
return "action";
|
|
608
|
+
}
|
|
609
|
+
function collectNodeTemplates(data, known) {
|
|
610
|
+
const nodes = {};
|
|
611
|
+
(data.custom_nodes ?? []).forEach((t) => {
|
|
612
|
+
if (!known[t.name] && !nodes[t.name]) {
|
|
613
|
+
nodes[t.name] = templateToNode(t);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
Object.values(data.nodes ?? {}).forEach((spec) => {
|
|
617
|
+
if (!known[spec.name] && !nodes[spec.name]) {
|
|
618
|
+
nodes[spec.name] = {
|
|
619
|
+
name: spec.name,
|
|
620
|
+
category: inferCategory(spec),
|
|
621
|
+
title: spec.title,
|
|
622
|
+
description: "",
|
|
623
|
+
properties: {},
|
|
624
|
+
isDefault: false
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
return nodes;
|
|
629
|
+
}
|
|
630
|
+
function b3ToTree(data, knownNodes) {
|
|
631
|
+
const treeId = data.id ?? v4_default();
|
|
632
|
+
const rootId = v4_default();
|
|
633
|
+
const rootBlock = {
|
|
634
|
+
id: rootId,
|
|
635
|
+
name: "Root",
|
|
636
|
+
title: data.title ?? "A behavior tree",
|
|
637
|
+
description: data.description ?? "",
|
|
638
|
+
category: "root",
|
|
639
|
+
properties: { ...data.properties ?? {} },
|
|
640
|
+
position: { x: data.display?.x ?? 0, y: data.display?.y ?? 0 }
|
|
641
|
+
};
|
|
642
|
+
const tree = {
|
|
643
|
+
id: treeId,
|
|
644
|
+
title: data.title ?? "A behavior tree",
|
|
645
|
+
description: data.description ?? "",
|
|
646
|
+
blocks: { [rootId]: rootBlock },
|
|
647
|
+
connections: {},
|
|
648
|
+
rootId,
|
|
649
|
+
properties: { ...data.properties ?? {} },
|
|
650
|
+
viewport: {
|
|
651
|
+
x: data.display?.camera_x ?? 0,
|
|
652
|
+
y: data.display?.camera_y ?? 0,
|
|
653
|
+
zoom: data.display?.camera_z ?? 1
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
const nodes = collectNodeTemplates(data, knownNodes);
|
|
657
|
+
const templateOf = (name) => knownNodes[name] ?? nodes[name];
|
|
658
|
+
const specs = Object.entries(data.nodes ?? {}).map(([key, spec]) => ({ ...spec, id: spec.id ?? key }));
|
|
659
|
+
specs.forEach((spec) => {
|
|
660
|
+
const template = templateOf(spec.name);
|
|
661
|
+
const block = {
|
|
662
|
+
id: spec.id,
|
|
663
|
+
name: spec.name,
|
|
664
|
+
title: spec.title ?? template?.title ?? spec.name,
|
|
665
|
+
description: spec.description ?? "",
|
|
666
|
+
category: template?.category ?? inferCategory(spec),
|
|
667
|
+
properties: { ...template?.properties ?? {}, ...spec.properties ?? {} },
|
|
668
|
+
position: spec.display ? { x: spec.display.x, y: spec.display.y } : { x: 0, y: 0 }
|
|
669
|
+
};
|
|
670
|
+
tree.blocks[block.id] = block;
|
|
671
|
+
});
|
|
672
|
+
const connect = (source, target) => {
|
|
673
|
+
if (!tree.blocks[source] || !tree.blocks[target]) return;
|
|
674
|
+
const connection = { id: v4_default(), source, target };
|
|
675
|
+
tree.connections[connection.id] = connection;
|
|
676
|
+
};
|
|
677
|
+
if (data.root && tree.blocks[data.root]) {
|
|
678
|
+
connect(rootId, data.root);
|
|
679
|
+
}
|
|
680
|
+
specs.forEach((spec) => {
|
|
681
|
+
const children = spec.children ?? (spec.child ? [spec.child] : []);
|
|
682
|
+
children.forEach((childId) => connect(spec.id, childId));
|
|
683
|
+
});
|
|
684
|
+
if (!data.display) {
|
|
685
|
+
organizeTree(tree, "horizontal", true);
|
|
686
|
+
}
|
|
687
|
+
return { tree, nodes };
|
|
688
|
+
}
|
|
689
|
+
function b3ToProject(data) {
|
|
690
|
+
const project = {
|
|
691
|
+
id: data.id ?? v4_default(),
|
|
692
|
+
name: data.name ?? "Imported Project",
|
|
693
|
+
description: data.description ?? "",
|
|
694
|
+
trees: {},
|
|
695
|
+
nodes: { ...DEFAULT_NODES },
|
|
696
|
+
selectedTreeId: void 0,
|
|
697
|
+
createdAt: data.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
698
|
+
updatedAt: data.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
699
|
+
};
|
|
700
|
+
(data.custom_nodes ?? []).forEach((t) => {
|
|
701
|
+
if (!project.nodes[t.name]) {
|
|
702
|
+
project.nodes[t.name] = templateToNode(t);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
(data.trees ?? []).forEach((treeData) => {
|
|
706
|
+
const { tree, nodes } = b3ToTree(treeData, project.nodes);
|
|
707
|
+
Object.assign(project.nodes, nodes);
|
|
708
|
+
project.trees[tree.id] = tree;
|
|
709
|
+
});
|
|
710
|
+
const treeIds = Object.keys(project.trees);
|
|
711
|
+
project.selectedTreeId = data.selectedTree && project.trees[data.selectedTree] ? data.selectedTree : treeIds[0];
|
|
712
|
+
return project;
|
|
713
|
+
}
|
|
714
|
+
function b3ToNodes(data) {
|
|
715
|
+
const nodes = {};
|
|
716
|
+
data.forEach((t) => {
|
|
717
|
+
if (t.name) {
|
|
718
|
+
nodes[t.name] = templateToNode(t);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
return nodes;
|
|
722
|
+
}
|
|
723
|
+
function isLegacyReactProject(data) {
|
|
724
|
+
const d = data;
|
|
725
|
+
if (!d || typeof d !== "object" || Array.isArray(d.trees)) return false;
|
|
726
|
+
if (!d.trees || typeof d.trees !== "object") return false;
|
|
727
|
+
const trees = Object.values(d.trees);
|
|
728
|
+
return trees.length > 0 && !!trees[0].blocks;
|
|
729
|
+
}
|
|
730
|
+
var LEGACY_NAME_MAP = {
|
|
731
|
+
root: "Root",
|
|
732
|
+
sequence: "Sequence",
|
|
733
|
+
selector: "Priority",
|
|
734
|
+
priority: "Priority",
|
|
735
|
+
inverter: "Inverter",
|
|
736
|
+
repeater: "Repeater",
|
|
737
|
+
wait: "Wait",
|
|
738
|
+
succeeder: "Succeeder",
|
|
739
|
+
failer: "Failer"
|
|
740
|
+
};
|
|
741
|
+
function convertLegacyProject(data) {
|
|
742
|
+
const project = deserializeProject(data);
|
|
743
|
+
project.nodes = { ...DEFAULT_NODES, ...project.nodes };
|
|
744
|
+
Object.values(project.trees).forEach((tree) => {
|
|
745
|
+
Object.values(tree.blocks).forEach((block) => {
|
|
746
|
+
const mapped = LEGACY_NAME_MAP[block.name];
|
|
747
|
+
if (mapped) block.name = mapped;
|
|
748
|
+
if (!project.nodes[block.name]) {
|
|
749
|
+
project.nodes[block.name] = {
|
|
750
|
+
name: block.name,
|
|
751
|
+
category: block.category,
|
|
752
|
+
title: block.title,
|
|
753
|
+
description: block.description,
|
|
754
|
+
properties: {},
|
|
755
|
+
isDefault: false
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
return project;
|
|
761
|
+
}
|
|
762
|
+
function parseImportedJson(data) {
|
|
763
|
+
if (Array.isArray(data)) {
|
|
764
|
+
return { kind: "nodes", nodes: b3ToNodes(data) };
|
|
765
|
+
}
|
|
766
|
+
if (!data || typeof data !== "object") {
|
|
767
|
+
throw new InvalidFileError("Not a behavior tree file", "not_behavior_tree");
|
|
768
|
+
}
|
|
769
|
+
const d = data;
|
|
770
|
+
if (isLegacyReactProject(d)) {
|
|
771
|
+
return { kind: "project", project: convertLegacyProject(d) };
|
|
772
|
+
}
|
|
773
|
+
if (Array.isArray(d.trees)) {
|
|
774
|
+
return { kind: "project", project: b3ToProject(d) };
|
|
775
|
+
}
|
|
776
|
+
if (d.nodes && "root" in d) {
|
|
777
|
+
return { kind: "tree", tree: d };
|
|
778
|
+
}
|
|
779
|
+
if (d.scope === "node" && d.name) {
|
|
780
|
+
return { kind: "nodes", nodes: b3ToNodes([d]) };
|
|
781
|
+
}
|
|
782
|
+
throw new InvalidFileError("Unrecognized behavior tree file format", "not_behavior_tree");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ../../apps/editor/src/lib/behavior/btcpp-catalog.ts
|
|
786
|
+
var port = (direction, type, defaultValue) => ({
|
|
787
|
+
direction,
|
|
788
|
+
...type ? { type } : {},
|
|
789
|
+
...defaultValue !== void 0 ? { default: defaultValue } : {}
|
|
790
|
+
});
|
|
791
|
+
var input = (type, defaultValue) => port("input", type, defaultValue);
|
|
792
|
+
var BOTH = [3, 4];
|
|
793
|
+
var V4 = [4];
|
|
794
|
+
var V3 = [3];
|
|
795
|
+
var model = (category, versions, ports = {}) => ({
|
|
796
|
+
category,
|
|
797
|
+
versions,
|
|
798
|
+
ports
|
|
799
|
+
});
|
|
800
|
+
var switchPorts = (cases) => {
|
|
801
|
+
const ports = { variable: input("std::string") };
|
|
802
|
+
for (let i = 1; i <= cases; i++) ports[`case_${i}`] = input("std::string");
|
|
803
|
+
return ports;
|
|
804
|
+
};
|
|
805
|
+
var loopPorts = (type) => ({
|
|
806
|
+
queue: port("inout"),
|
|
807
|
+
if_empty: input("NodeStatus", "SUCCESS"),
|
|
808
|
+
value: port("output", type)
|
|
809
|
+
});
|
|
810
|
+
var blackboardCheckPorts = {
|
|
811
|
+
value_A: input(),
|
|
812
|
+
value_B: input(),
|
|
813
|
+
return_on_mismatch: input("NodeStatus")
|
|
814
|
+
};
|
|
815
|
+
var BTCPP_BUILTINS = {
|
|
816
|
+
// Controls
|
|
817
|
+
Fallback: model("composite", BOTH),
|
|
818
|
+
AsyncFallback: model("composite", V4),
|
|
819
|
+
Sequence: model("composite", BOTH),
|
|
820
|
+
AsyncSequence: model("composite", V4),
|
|
821
|
+
SequenceWithMemory: model("composite", BOTH),
|
|
822
|
+
Parallel: model("composite", BOTH, {
|
|
823
|
+
success_count: input("int", "-1"),
|
|
824
|
+
failure_count: input("int", "1")
|
|
825
|
+
}),
|
|
826
|
+
ParallelAll: model("composite", V4, { max_failures: input("int", "1") }),
|
|
827
|
+
ReactiveSequence: model("composite", BOTH),
|
|
828
|
+
ReactiveFallback: model("composite", BOTH),
|
|
829
|
+
IfThenElse: model("composite", BOTH),
|
|
830
|
+
WhileDoElse: model("composite", BOTH),
|
|
831
|
+
TryCatch: model("composite", V4, { catch_on_halt: input("bool", "false") }),
|
|
832
|
+
Switch2: model("composite", BOTH, switchPorts(2)),
|
|
833
|
+
Switch3: model("composite", BOTH, switchPorts(3)),
|
|
834
|
+
Switch4: model("composite", BOTH, switchPorts(4)),
|
|
835
|
+
Switch5: model("composite", BOTH, switchPorts(5)),
|
|
836
|
+
Switch6: model("composite", BOTH, switchPorts(6)),
|
|
837
|
+
ManualSelector: model("composite", V3, { repeat_last_selection: input("bool", "false") }),
|
|
838
|
+
// Decorators
|
|
839
|
+
Inverter: model("decorator", BOTH),
|
|
840
|
+
RetryUntilSuccessful: model("decorator", BOTH, { num_attempts: input("int") }),
|
|
841
|
+
KeepRunningUntilFailure: model("decorator", BOTH),
|
|
842
|
+
Repeat: model("decorator", BOTH, { num_cycles: input("int") }),
|
|
843
|
+
Timeout: model("decorator", BOTH, { msec: input("unsigned") }),
|
|
844
|
+
Delay: model("decorator", BOTH, { delay_msec: input("unsigned") }),
|
|
845
|
+
RunOnce: model("decorator", V4, { then_skip: input("bool", "true") }),
|
|
846
|
+
ForceSuccess: model("decorator", BOTH),
|
|
847
|
+
ForceFailure: model("decorator", BOTH),
|
|
848
|
+
Precondition: model("decorator", V4, { if: input("std::string"), else: input("NodeStatus", "FAILURE") }),
|
|
849
|
+
SkipUnlessUpdated: model("decorator", V4, { entry: input() }),
|
|
850
|
+
WaitValueUpdate: model("decorator", V4, { entry: input() }),
|
|
851
|
+
LoopInt: model("decorator", V4, loopPorts("int")),
|
|
852
|
+
LoopBool: model("decorator", V4, loopPorts("bool")),
|
|
853
|
+
LoopDouble: model("decorator", V4, loopPorts("double")),
|
|
854
|
+
LoopString: model("decorator", V4, loopPorts("std::string")),
|
|
855
|
+
BlackboardCheckInt: model("decorator", V3, blackboardCheckPorts),
|
|
856
|
+
BlackboardCheckDouble: model("decorator", V3, blackboardCheckPorts),
|
|
857
|
+
BlackboardCheckString: model("decorator", V3, blackboardCheckPorts),
|
|
858
|
+
BlackboardCheckBool: model("decorator", V3, blackboardCheckPorts),
|
|
859
|
+
// Leaves
|
|
860
|
+
AlwaysSuccess: model("action", BOTH),
|
|
861
|
+
AlwaysFailure: model("action", BOTH),
|
|
862
|
+
Script: model("action", V4, { code: input("std::string") }),
|
|
863
|
+
ScriptCondition: model("condition", V4, { code: input("std::string") }),
|
|
864
|
+
SetBlackboard: model("action", BOTH, { value: input(), output_key: port("inout") }),
|
|
865
|
+
Sleep: model("action", V4, { msec: input("unsigned") }),
|
|
866
|
+
UnsetBlackboard: model("action", V4, { key: input("std::string") }),
|
|
867
|
+
WasEntryUpdated: model("action", V4, { entry: input() }),
|
|
868
|
+
// A reference to another BehaviorTree in the file, by its ID. Kept as a
|
|
869
|
+
// leaf here: the referenced tree is a separate tree in the project.
|
|
870
|
+
SubTree: model("action", BOTH)
|
|
871
|
+
};
|
|
872
|
+
var V3_TO_V4_NAMES = {
|
|
873
|
+
SequenceStar: "SequenceWithMemory",
|
|
874
|
+
RetryUntilSuccesful: "RetryUntilSuccessful",
|
|
875
|
+
SubTreePlus: "SubTree"
|
|
876
|
+
};
|
|
877
|
+
var V4_TO_V3_NAMES = {
|
|
878
|
+
SequenceWithMemory: "SequenceStar"
|
|
879
|
+
};
|
|
880
|
+
var V3_TO_V4_PORTS = {
|
|
881
|
+
Parallel: { success_threshold: "success_count", failure_threshold: "failure_count" }
|
|
882
|
+
};
|
|
883
|
+
var NAV2_BY_CATEGORY = {
|
|
884
|
+
composite: ["PipelineSequence", "RecoveryNode", "RoundRobin"],
|
|
885
|
+
decorator: [
|
|
886
|
+
"DistanceController",
|
|
887
|
+
"GoalUpdatedController",
|
|
888
|
+
"GoalUpdater",
|
|
889
|
+
"PathLongerOnApproach",
|
|
890
|
+
"RateController",
|
|
891
|
+
"SingleTrigger",
|
|
892
|
+
"SpeedController"
|
|
893
|
+
],
|
|
894
|
+
condition: [
|
|
895
|
+
"AreErrorCodesPresent",
|
|
896
|
+
"ArePosesNear",
|
|
897
|
+
"DistanceTraveled",
|
|
898
|
+
"GlobalUpdatedGoal",
|
|
899
|
+
"GoalReached",
|
|
900
|
+
"GoalUpdated",
|
|
901
|
+
"InitialPoseReceived",
|
|
902
|
+
"IsBatteryCharging",
|
|
903
|
+
"IsBatteryLow",
|
|
904
|
+
"IsPathValid",
|
|
905
|
+
"IsStuck",
|
|
906
|
+
"PathExpiringTimer",
|
|
907
|
+
"TimeExpired",
|
|
908
|
+
"TransformAvailable",
|
|
909
|
+
"WouldAControllerRecoveryHelp",
|
|
910
|
+
"WouldAPlannerRecoveryHelp",
|
|
911
|
+
"WouldASmootherRecoveryHelp"
|
|
912
|
+
],
|
|
913
|
+
action: [
|
|
914
|
+
"AssistedTeleop",
|
|
915
|
+
"BackUp",
|
|
916
|
+
"CancelAssistedTeleop",
|
|
917
|
+
"CancelBackUp",
|
|
918
|
+
"CancelComputeAndTrackRoute",
|
|
919
|
+
"CancelControl",
|
|
920
|
+
"CancelDriveOnHeading",
|
|
921
|
+
"CancelSpin",
|
|
922
|
+
"CancelWait",
|
|
923
|
+
"ClearCostmapAroundPose",
|
|
924
|
+
"ClearCostmapAroundRobot",
|
|
925
|
+
"ClearCostmapExceptRegion",
|
|
926
|
+
"ClearEntireCostmap",
|
|
927
|
+
"ComputeAndTrackRoute",
|
|
928
|
+
"ComputePathThroughPoses",
|
|
929
|
+
"ComputePathToPose",
|
|
930
|
+
"ComputeRoute",
|
|
931
|
+
"ConcatenatePaths",
|
|
932
|
+
"ControllerSelector",
|
|
933
|
+
"DockRobot",
|
|
934
|
+
"DriveOnHeading",
|
|
935
|
+
"FollowPath",
|
|
936
|
+
"GetCurrentPose",
|
|
937
|
+
"GetPoseFromPath",
|
|
938
|
+
"GoalCheckerSelector",
|
|
939
|
+
"NavigateThroughPoses",
|
|
940
|
+
"NavigateToPose",
|
|
941
|
+
"PlannerSelector",
|
|
942
|
+
"ProgressCheckerSelector",
|
|
943
|
+
"ReinitializeGlobalLocalization",
|
|
944
|
+
"RemovePassedGoals",
|
|
945
|
+
"SmoothPath",
|
|
946
|
+
"SmootherSelector",
|
|
947
|
+
"Spin",
|
|
948
|
+
"TruncatePath",
|
|
949
|
+
"TruncatePathLocal",
|
|
950
|
+
"UndockRobot",
|
|
951
|
+
"Wait"
|
|
952
|
+
]
|
|
953
|
+
};
|
|
954
|
+
var NAV2_CATEGORIES = Object.fromEntries(
|
|
955
|
+
Object.entries(NAV2_BY_CATEGORY).flatMap(
|
|
956
|
+
([category, names]) => names.map((name) => [name, category])
|
|
957
|
+
)
|
|
958
|
+
);
|
|
959
|
+
var B3_TO_BTCPP = {
|
|
960
|
+
Priority: { name: "Fallback" },
|
|
961
|
+
MemSequence: { name: "SequenceWithMemory", v3Name: "SequenceStar" },
|
|
962
|
+
MaxTime: { name: "Timeout", props: { maxTime: "msec" } },
|
|
963
|
+
Repeater: { name: "Repeat", props: { maxLoop: "num_cycles" } },
|
|
964
|
+
RepeatUntilSuccess: { name: "RetryUntilSuccessful", props: { maxLoop: "num_attempts" } },
|
|
965
|
+
Succeeder: { name: "AlwaysSuccess" },
|
|
966
|
+
Failer: { name: "AlwaysFailure" },
|
|
967
|
+
// Only behavior3's Wait (properties ⊆ {milliseconds}); Nav2 has its own Wait
|
|
968
|
+
Wait: { name: "Sleep", props: { milliseconds: "msec" }, v4Only: true }
|
|
969
|
+
};
|
|
970
|
+
function catalogNode(name) {
|
|
971
|
+
const builtin = BTCPP_BUILTINS[name];
|
|
972
|
+
if (!builtin) return void 0;
|
|
973
|
+
const properties = name === "SubTree" ? { ID: "" } : Object.fromEntries(
|
|
974
|
+
Object.entries(builtin.ports).filter(([, p]) => p.direction !== "output").map(([key, p]) => [key, p.default ?? ""])
|
|
975
|
+
);
|
|
976
|
+
return {
|
|
977
|
+
name,
|
|
978
|
+
title: name,
|
|
979
|
+
category: builtin.category,
|
|
980
|
+
description: "",
|
|
981
|
+
properties,
|
|
982
|
+
ports: { ...builtin.ports },
|
|
983
|
+
isDefault: false
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// ../../apps/editor/src/lib/behavior/btcpp.ts
|
|
988
|
+
var WRAPPER_TAGS = {
|
|
989
|
+
Action: "action",
|
|
990
|
+
Condition: "condition",
|
|
991
|
+
Control: "composite",
|
|
992
|
+
Decorator: "decorator"
|
|
993
|
+
};
|
|
994
|
+
var MODEL_TAGS = {
|
|
995
|
+
action: "Action",
|
|
996
|
+
condition: "Condition",
|
|
997
|
+
composite: "Control",
|
|
998
|
+
decorator: "Decorator"
|
|
999
|
+
};
|
|
1000
|
+
var PORT_TAGS = {
|
|
1001
|
+
input_port: "input",
|
|
1002
|
+
output_port: "output",
|
|
1003
|
+
inout_port: "inout"
|
|
1004
|
+
};
|
|
1005
|
+
var XML_NAME = /^[A-Za-z_][\w.:-]*$/;
|
|
1006
|
+
var isLeaf = (category) => category === "action" || category === "condition";
|
|
1007
|
+
function isLikelyXml(text) {
|
|
1008
|
+
return /^\s*</.test(text.replace(/^\uFEFF/, ""));
|
|
1009
|
+
}
|
|
1010
|
+
var CONTROL_NAME = /Sequence|Fallback|Selector|Parallel|Switch|Pipeline|RoundRobin|Recovery|Priority/;
|
|
1011
|
+
var CONDITION_NAME = /^(Is|Has|Are|Can|Would|Should|Check)[A-Z]|(Reached|Updated|Received|Available|Valid|Present|Near)$/;
|
|
1012
|
+
function guessCategory(name, maxChildren) {
|
|
1013
|
+
if (maxChildren >= 2) return "composite";
|
|
1014
|
+
if (maxChildren === 1) return CONTROL_NAME.test(name) ? "composite" : "decorator";
|
|
1015
|
+
return CONDITION_NAME.test(name) ? "condition" : "action";
|
|
1016
|
+
}
|
|
1017
|
+
function parseDocument(text) {
|
|
1018
|
+
if (/<!DOCTYPE/i.test(text)) {
|
|
1019
|
+
throw new InvalidFileError("XML files with a DOCTYPE are not supported", "xml_doctype");
|
|
1020
|
+
}
|
|
1021
|
+
const doc = new DOMParser().parseFromString(text.replace(/^\uFEFF/, ""), "application/xml");
|
|
1022
|
+
const error = doc.getElementsByTagName("parsererror")[0];
|
|
1023
|
+
if (error) {
|
|
1024
|
+
const detail = (error.textContent ?? "").replace(/^\s*This page contains the following errors:\s*/i, "").trim().split("\n")[0].slice(0, 160);
|
|
1025
|
+
throw new InvalidFileError(`Not valid XML${detail ? `: ${detail}` : ""}`, "invalid_xml", detail || void 0);
|
|
1026
|
+
}
|
|
1027
|
+
if (doc.documentElement.tagName !== "root") {
|
|
1028
|
+
throw new InvalidFileError("Not a BehaviorTree.CPP file: expected a <root> element", "not_behavior_tree");
|
|
1029
|
+
}
|
|
1030
|
+
return doc.documentElement;
|
|
1031
|
+
}
|
|
1032
|
+
function parseModel(el, name, category) {
|
|
1033
|
+
const ports = {};
|
|
1034
|
+
Array.from(el.children).forEach((portEl) => {
|
|
1035
|
+
const direction = PORT_TAGS[portEl.tagName];
|
|
1036
|
+
const portName = portEl.getAttribute("name");
|
|
1037
|
+
if (!direction || !portName) return;
|
|
1038
|
+
const type = portEl.getAttribute("type");
|
|
1039
|
+
const defaultValue = portEl.getAttribute("default");
|
|
1040
|
+
const description = (portEl.textContent ?? "").trim();
|
|
1041
|
+
ports[portName] = {
|
|
1042
|
+
direction,
|
|
1043
|
+
...type ? { type } : {},
|
|
1044
|
+
...defaultValue !== null ? { default: defaultValue } : {},
|
|
1045
|
+
...description ? { description } : {}
|
|
1046
|
+
};
|
|
1047
|
+
});
|
|
1048
|
+
return {
|
|
1049
|
+
name,
|
|
1050
|
+
title: name,
|
|
1051
|
+
category,
|
|
1052
|
+
description: "",
|
|
1053
|
+
properties: Object.fromEntries(
|
|
1054
|
+
Object.entries(ports).filter(([, p]) => p.direction !== "output").map(([key, p]) => [key, p.default ?? ""])
|
|
1055
|
+
),
|
|
1056
|
+
ports,
|
|
1057
|
+
isDefault: false
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
var normalizeName = (name) => V3_TO_V4_NAMES[name] ?? name;
|
|
1061
|
+
function parseBtcppXml(text, knownNodes, layout = readTreeLayout()) {
|
|
1062
|
+
const root = parseDocument(text);
|
|
1063
|
+
const version = root.getAttribute("BTCPP_format") === "4" ? 4 : 3;
|
|
1064
|
+
const issues = [];
|
|
1065
|
+
const models = {};
|
|
1066
|
+
const treeElements = [];
|
|
1067
|
+
Array.from(root.children).forEach((el) => {
|
|
1068
|
+
if (el.tagName === "TreeNodesModel") {
|
|
1069
|
+
Array.from(el.children).forEach((modelEl) => {
|
|
1070
|
+
const category = WRAPPER_TAGS[modelEl.tagName];
|
|
1071
|
+
const id = modelEl.getAttribute("ID");
|
|
1072
|
+
if (!category || !id) return;
|
|
1073
|
+
const name = normalizeName(id);
|
|
1074
|
+
models[name] = parseModel(modelEl, name, category);
|
|
1075
|
+
});
|
|
1076
|
+
} else if (el.tagName === "BehaviorTree") {
|
|
1077
|
+
treeElements.push(el);
|
|
1078
|
+
} else if (el.tagName === "include") {
|
|
1079
|
+
issues.push({
|
|
1080
|
+
level: "warning",
|
|
1081
|
+
message: `Included file "${el.getAttribute("path") ?? ""}" wasn't loaded. Import it separately.`
|
|
1082
|
+
});
|
|
1083
|
+
} else {
|
|
1084
|
+
issues.push({ level: "warning", message: `Skipped unknown <${el.tagName}> element` });
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
if (treeElements.length === 0) {
|
|
1088
|
+
if (Object.keys(models).length === 0) {
|
|
1089
|
+
throw new InvalidFileError("This file has no <BehaviorTree> or <TreeNodesModel>", "not_behavior_tree");
|
|
1090
|
+
}
|
|
1091
|
+
return { kind: "nodes", version, models, issues };
|
|
1092
|
+
}
|
|
1093
|
+
const nameOf = (el) => normalizeName(WRAPPER_TAGS[el.tagName] ? el.getAttribute("ID") ?? el.tagName : el.tagName);
|
|
1094
|
+
const usage = {};
|
|
1095
|
+
const survey = (el) => {
|
|
1096
|
+
const name = nameOf(el);
|
|
1097
|
+
const entry = usage[name] ??= { maxChildren: 0, keys: /* @__PURE__ */ new Set() };
|
|
1098
|
+
entry.maxChildren = Math.max(entry.maxChildren, el.children.length);
|
|
1099
|
+
if (WRAPPER_TAGS[el.tagName]) entry.explicit = WRAPPER_TAGS[el.tagName];
|
|
1100
|
+
Array.from(el.attributes).forEach((attr) => entry.keys.add(attr.name));
|
|
1101
|
+
Array.from(el.children).forEach(survey);
|
|
1102
|
+
};
|
|
1103
|
+
treeElements.forEach((treeEl) => Array.from(treeEl.children).forEach(survey));
|
|
1104
|
+
const guessed = [];
|
|
1105
|
+
const categories = {};
|
|
1106
|
+
Object.entries(usage).forEach(([name, use]) => {
|
|
1107
|
+
const known = knownNodes[name]?.category;
|
|
1108
|
+
const category = (name === "SubTree" ? "action" : void 0) ?? use.explicit ?? models[name]?.category ?? (known && known !== "root" ? known : void 0) ?? BTCPP_BUILTINS[name]?.category ?? NAV2_CATEGORIES[name];
|
|
1109
|
+
if (category) {
|
|
1110
|
+
categories[name] = category;
|
|
1111
|
+
} else {
|
|
1112
|
+
categories[name] = guessCategory(name, use.maxChildren);
|
|
1113
|
+
guessed.push(name);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
if (guessed.length > 0) {
|
|
1117
|
+
issues.push({
|
|
1118
|
+
level: "info",
|
|
1119
|
+
message: `Guessed the node type of ${guessed.join(", ")}. Import a TreeNodesModel file to set them exactly.`
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
const propertiesOf = (el, name) => {
|
|
1123
|
+
const properties = {};
|
|
1124
|
+
const renames = version === 3 ? V3_TO_V4_PORTS[name] ?? {} : {};
|
|
1125
|
+
const classicSubTree = version === 3 && el.tagName === "SubTree";
|
|
1126
|
+
Array.from(el.attributes).forEach(({ name: key, value }) => {
|
|
1127
|
+
if (key === "name") return;
|
|
1128
|
+
if (key === "ID" && WRAPPER_TAGS[el.tagName]) return;
|
|
1129
|
+
if (key === "__autoremap") {
|
|
1130
|
+
properties._autoremap = value;
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (key === "__shared_blackboard") {
|
|
1134
|
+
if (value === "true" || value === "1") {
|
|
1135
|
+
properties._autoremap = "true";
|
|
1136
|
+
issues.push({
|
|
1137
|
+
level: "warning",
|
|
1138
|
+
message: `SubTree "${el.getAttribute("ID") ?? ""}" shared its parent's blackboard (v3). It now uses _autoremap, which only shares the subtree's own ports.`
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (classicSubTree && key !== "ID" && !/^\{.*\}$/.test(value)) {
|
|
1144
|
+
properties[key] = `{${value}}`;
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
properties[renames[key] ?? key] = value;
|
|
1148
|
+
});
|
|
1149
|
+
return properties;
|
|
1150
|
+
};
|
|
1151
|
+
const trees = [];
|
|
1152
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1153
|
+
treeElements.forEach((treeEl, index) => {
|
|
1154
|
+
const title = treeEl.getAttribute("ID") || `Tree ${index + 1}`;
|
|
1155
|
+
if (!treeEl.getAttribute("ID")) {
|
|
1156
|
+
issues.push({ level: "warning", message: `A <BehaviorTree> had no ID; named it "${title}"` });
|
|
1157
|
+
}
|
|
1158
|
+
const rootId = v4_default();
|
|
1159
|
+
const tree = {
|
|
1160
|
+
id: v4_default(),
|
|
1161
|
+
title,
|
|
1162
|
+
description: "",
|
|
1163
|
+
blocks: {
|
|
1164
|
+
[rootId]: {
|
|
1165
|
+
id: rootId,
|
|
1166
|
+
name: "Root",
|
|
1167
|
+
title,
|
|
1168
|
+
description: "",
|
|
1169
|
+
category: "root",
|
|
1170
|
+
properties: {},
|
|
1171
|
+
position: { x: 0, y: 0 }
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
connections: {},
|
|
1175
|
+
rootId,
|
|
1176
|
+
properties: {},
|
|
1177
|
+
viewport: { x: 0, y: 0, zoom: 1 }
|
|
1178
|
+
};
|
|
1179
|
+
const connect = (source, target) => {
|
|
1180
|
+
const connection = { id: v4_default(), source, target };
|
|
1181
|
+
tree.connections[connection.id] = connection;
|
|
1182
|
+
};
|
|
1183
|
+
const attachChildren = (parent, childIds) => {
|
|
1184
|
+
const capacity = parent.category === "composite" ? Infinity : isLeaf(parent.category) ? 0 : 1;
|
|
1185
|
+
childIds.forEach((childId, i) => {
|
|
1186
|
+
if (i < capacity) connect(parent.id, childId);
|
|
1187
|
+
});
|
|
1188
|
+
const key = `${tree.title}/${parent.name}`;
|
|
1189
|
+
if (childIds.length > capacity && !reported.has(key)) {
|
|
1190
|
+
reported.add(key);
|
|
1191
|
+
const label = parent.category === "root" ? `Tree "${tree.title}"` : `${parent.name} in "${tree.title}"`;
|
|
1192
|
+
issues.push({
|
|
1193
|
+
level: "error",
|
|
1194
|
+
message: `${label} has ${childIds.length} children but can only have ${capacity}. The extra ones are left unconnected.`
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const addBlock = (el) => {
|
|
1199
|
+
const name = nameOf(el);
|
|
1200
|
+
const block = {
|
|
1201
|
+
id: v4_default(),
|
|
1202
|
+
name,
|
|
1203
|
+
title: el.getAttribute("name") || name,
|
|
1204
|
+
description: "",
|
|
1205
|
+
category: categories[name],
|
|
1206
|
+
properties: propertiesOf(el, name),
|
|
1207
|
+
position: { x: 0, y: 0 }
|
|
1208
|
+
};
|
|
1209
|
+
tree.blocks[block.id] = block;
|
|
1210
|
+
attachChildren(block, Array.from(el.children).map(addBlock));
|
|
1211
|
+
return block.id;
|
|
1212
|
+
};
|
|
1213
|
+
attachChildren(tree.blocks[rootId], Array.from(treeEl.children).map(addBlock));
|
|
1214
|
+
organizeTree(tree, layout, true);
|
|
1215
|
+
trees.push(tree);
|
|
1216
|
+
});
|
|
1217
|
+
const mainName = root.getAttribute("main_tree_to_execute");
|
|
1218
|
+
let mainTree = mainName ? trees.find((t) => t.title === mainName) : void 0;
|
|
1219
|
+
if (!mainTree) {
|
|
1220
|
+
mainTree = trees[0];
|
|
1221
|
+
if (mainName) {
|
|
1222
|
+
issues.push({ level: "warning", message: `main_tree_to_execute names "${mainName}", which isn't in the file` });
|
|
1223
|
+
} else if (trees.length > 1) {
|
|
1224
|
+
issues.push({ level: "info", message: `No main_tree_to_execute; opened "${mainTree.title}"` });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const nodes = {};
|
|
1228
|
+
Object.entries(usage).forEach(([name, use]) => {
|
|
1229
|
+
if (knownNodes[name]) return;
|
|
1230
|
+
nodes[name] = models[name] ?? catalogNode(name) ?? {
|
|
1231
|
+
name,
|
|
1232
|
+
title: name,
|
|
1233
|
+
category: categories[name],
|
|
1234
|
+
description: "",
|
|
1235
|
+
properties: Object.fromEntries(
|
|
1236
|
+
[...use.keys].filter((key) => key !== "name" && key !== "ID" && !key.startsWith("_")).map((key) => [key, ""])
|
|
1237
|
+
),
|
|
1238
|
+
isDefault: false
|
|
1239
|
+
};
|
|
1240
|
+
});
|
|
1241
|
+
return { kind: "trees", version, trees, mainTreeId: mainTree.id, nodes, models, issues };
|
|
1242
|
+
}
|
|
1243
|
+
function btcppToProject(imported, name) {
|
|
1244
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1245
|
+
return {
|
|
1246
|
+
id: v4_default(),
|
|
1247
|
+
name,
|
|
1248
|
+
description: "",
|
|
1249
|
+
trees: Object.fromEntries(imported.trees.map((tree) => [tree.id, tree])),
|
|
1250
|
+
nodes: { ...DEFAULT_NODES, ...imported.nodes, ...imported.models },
|
|
1251
|
+
selectedTreeId: imported.mainTreeId,
|
|
1252
|
+
createdAt: now,
|
|
1253
|
+
updatedAt: now
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/\n/g, " ").replace(/\r/g, " ").replace(/\t/g, "	");
|
|
1257
|
+
var escapeText = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1258
|
+
var sanitizeId = (title) => title.trim().replace(/\s+/g, "_") || "Tree";
|
|
1259
|
+
var UNMAPPED_B3 = /* @__PURE__ */ new Set(["MemPriority", "Limiter", "RepeatUntilFailure", "Runner", "Error"]);
|
|
1260
|
+
function exportTrees(project, mainTreeId, treeIds, { version, layout = readTreeLayout(), includeNodesModel = true }) {
|
|
1261
|
+
const issues = [];
|
|
1262
|
+
const once = /* @__PURE__ */ new Set();
|
|
1263
|
+
const issue = (level, message) => {
|
|
1264
|
+
if (once.has(message)) return;
|
|
1265
|
+
once.add(message);
|
|
1266
|
+
issues.push({ level, message });
|
|
1267
|
+
};
|
|
1268
|
+
const allTrees = Object.values(project.trees);
|
|
1269
|
+
const xmlIds = {};
|
|
1270
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1271
|
+
allTrees.forEach((tree) => {
|
|
1272
|
+
const base = sanitizeId(tree.title);
|
|
1273
|
+
let id = base;
|
|
1274
|
+
for (let n = 2; taken.has(id); n++) id = `${base}_${n}`;
|
|
1275
|
+
taken.add(id);
|
|
1276
|
+
xmlIds[tree.id] = id;
|
|
1277
|
+
});
|
|
1278
|
+
const resolveSubTree = (ref) => allTrees.find((t) => t.title === ref) ?? allTrees.find((t) => xmlIds[t.id] === ref);
|
|
1279
|
+
const subTreeRefs = (tree) => Object.values(tree.blocks).filter((b) => b.name === "SubTree" && typeof b.properties.ID === "string" && b.properties.ID).map((b) => b.properties.ID);
|
|
1280
|
+
let ordered;
|
|
1281
|
+
if (treeIds === "referenced") {
|
|
1282
|
+
ordered = [];
|
|
1283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1284
|
+
const queue = [project.trees[mainTreeId]];
|
|
1285
|
+
while (queue.length > 0) {
|
|
1286
|
+
const tree = queue.shift();
|
|
1287
|
+
if (seen.has(tree.id)) continue;
|
|
1288
|
+
seen.add(tree.id);
|
|
1289
|
+
ordered.push(tree);
|
|
1290
|
+
subTreeRefs(tree).forEach((ref) => {
|
|
1291
|
+
const target = resolveSubTree(ref);
|
|
1292
|
+
if (target) queue.push(target);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
} else {
|
|
1296
|
+
const main = project.trees[mainTreeId];
|
|
1297
|
+
ordered = [main, ...treeIds.map((id) => project.trees[id]).filter((t) => t && t.id !== main.id)];
|
|
1298
|
+
}
|
|
1299
|
+
const visiting = [];
|
|
1300
|
+
const done = /* @__PURE__ */ new Set();
|
|
1301
|
+
const checkCycles = (tree) => {
|
|
1302
|
+
if (done.has(tree.id)) return;
|
|
1303
|
+
if (visiting.includes(tree.id)) {
|
|
1304
|
+
const path = [...visiting.slice(visiting.indexOf(tree.id)), tree.id].map((id) => project.trees[id].title);
|
|
1305
|
+
issue("error", `SubTrees form a loop: ${path.join(" \u2192 ")}. BT.CPP can't load recursive trees.`);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
visiting.push(tree.id);
|
|
1309
|
+
subTreeRefs(tree).forEach((ref) => {
|
|
1310
|
+
const target = resolveSubTree(ref);
|
|
1311
|
+
if (target) checkCycles(target);
|
|
1312
|
+
else issue("warning", `SubTree references "${ref}", which isn't in this project. Your runtime must register it.`);
|
|
1313
|
+
});
|
|
1314
|
+
visiting.pop();
|
|
1315
|
+
done.add(tree.id);
|
|
1316
|
+
};
|
|
1317
|
+
ordered.forEach(checkCycles);
|
|
1318
|
+
ordered.forEach((tree) => {
|
|
1319
|
+
if (xmlIds[tree.id] !== tree.title) {
|
|
1320
|
+
issue("info", `Tree "${tree.title}" is written with ID "${xmlIds[tree.id]}"`);
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
const modelUses = {};
|
|
1324
|
+
const lines = [];
|
|
1325
|
+
let droppedDescriptions = false;
|
|
1326
|
+
const writeTree = (tree) => {
|
|
1327
|
+
const id = xmlIds[tree.id];
|
|
1328
|
+
if (tree.description || Object.keys(tree.properties ?? {}).length > 0) droppedDescriptions = true;
|
|
1329
|
+
const root = tree.rootId ? tree.blocks[tree.rootId] : void 0;
|
|
1330
|
+
if (!root) {
|
|
1331
|
+
issue("error", `Tree "${tree.title}" has no root node`);
|
|
1332
|
+
lines.push(` <BehaviorTree ID="${escapeAttr(id)}"/>`);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const visited = /* @__PURE__ */ new Set([root.id]);
|
|
1336
|
+
const rootChildren = childIdsInOrder(tree, root.id, layout);
|
|
1337
|
+
if (rootChildren.length !== 1) {
|
|
1338
|
+
issue(
|
|
1339
|
+
"error",
|
|
1340
|
+
rootChildren.length === 0 ? `Tree "${tree.title}" is empty. BT.CPP needs one node under the root.` : `The root of "${tree.title}" has ${rootChildren.length} children. BT.CPP allows one.`
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const writeBlock = (blockId, depth) => {
|
|
1344
|
+
if (visited.has(blockId)) return;
|
|
1345
|
+
visited.add(blockId);
|
|
1346
|
+
const block = tree.blocks[blockId];
|
|
1347
|
+
const template = project.nodes[block.name];
|
|
1348
|
+
const where = `"${block.title || block.name}" in "${tree.title}"`;
|
|
1349
|
+
if (block.description) droppedDescriptions = true;
|
|
1350
|
+
let name = block.name;
|
|
1351
|
+
let renames = {};
|
|
1352
|
+
const translation = template?.isDefault ? B3_TO_BTCPP[name] : void 0;
|
|
1353
|
+
const keys = Object.keys(block.properties);
|
|
1354
|
+
const isB3Wait = name !== "Wait" || keys.every((k) => k === "milliseconds");
|
|
1355
|
+
if (translation && isB3Wait) {
|
|
1356
|
+
if (translation.v4Only && version === 3) {
|
|
1357
|
+
issue("warning", `${name} has no BT.CPP v3 equivalent. Your runtime must register a "${name}" node.`);
|
|
1358
|
+
} else {
|
|
1359
|
+
name = translation.name;
|
|
1360
|
+
renames = translation.props ?? {};
|
|
1361
|
+
}
|
|
1362
|
+
} else if (template?.isDefault && UNMAPPED_B3.has(name)) {
|
|
1363
|
+
issue("warning", `${name} has no BT.CPP equivalent. Your runtime must register a "${name}" node.`);
|
|
1364
|
+
}
|
|
1365
|
+
const builtin = BTCPP_BUILTINS[name];
|
|
1366
|
+
if (builtin && !builtin.versions.includes(version)) {
|
|
1367
|
+
issue("warning", `${name} isn't a builtin in BT.CPP v${version}`);
|
|
1368
|
+
}
|
|
1369
|
+
const attrs = [];
|
|
1370
|
+
Object.entries(block.properties).forEach(([rawKey, value]) => {
|
|
1371
|
+
const key = renames[rawKey] ?? rawKey;
|
|
1372
|
+
if (value === "" || value === null || value === void 0) return;
|
|
1373
|
+
if (key === "name" || key === "ID" && name !== "SubTree") {
|
|
1374
|
+
issue("warning", `Property "${key}" on ${where} is reserved in BT.CPP and was left out`);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
if (!XML_NAME.test(key)) {
|
|
1378
|
+
issue("warning", `Property "${key}" on ${where} isn't a valid XML attribute name and was left out`);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (builtin && name !== "SubTree" && !builtin.ports[key] && !key.startsWith("_")) {
|
|
1382
|
+
issue("warning", `${name} has no "${key}" port; it was left out of ${where}`);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (key.startsWith("_") && version === 3 && !(name === "SubTree" && key === "_autoremap")) {
|
|
1386
|
+
issue("warning", `"${key}" on ${where} needs BT.CPP v4 and was left out`);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
let text;
|
|
1390
|
+
if (typeof value === "object") {
|
|
1391
|
+
text = JSON.stringify(value);
|
|
1392
|
+
issue("warning", `Property "${key}" on ${where} is an object; it was written as JSON text`);
|
|
1393
|
+
} else {
|
|
1394
|
+
text = String(value);
|
|
1395
|
+
}
|
|
1396
|
+
attrs.push([key, text]);
|
|
1397
|
+
});
|
|
1398
|
+
if (builtin) {
|
|
1399
|
+
Object.entries(builtin.ports).forEach(([key, port2]) => {
|
|
1400
|
+
if (port2.direction === "input" && port2.default === void 0 && !attrs.some(([k]) => k === key)) {
|
|
1401
|
+
issue("warning", `${where} is missing the "${key}" port that ${name} requires`);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
let tag = name;
|
|
1406
|
+
if (version === 3) {
|
|
1407
|
+
tag = translation?.v3Name && name === translation.name ? translation.v3Name : V4_TO_V3_NAMES[name] ?? name;
|
|
1408
|
+
const portRenames = Object.fromEntries(
|
|
1409
|
+
Object.entries(V3_TO_V4_PORTS[name] ?? {}).map(([v3, v42]) => [v42, v3])
|
|
1410
|
+
);
|
|
1411
|
+
attrs.forEach((attr) => {
|
|
1412
|
+
attr[0] = portRenames[attr[0]] ?? attr[0];
|
|
1413
|
+
});
|
|
1414
|
+
if (name === "SubTree") {
|
|
1415
|
+
tag = "SubTreePlus";
|
|
1416
|
+
attrs.forEach((attr) => {
|
|
1417
|
+
if (attr[0] === "_autoremap") attr[0] = "__autoremap";
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const title = block.title && block.title !== block.name && block.title !== template?.title ? block.title : "";
|
|
1422
|
+
const ordered2 = name === "SubTree" ? [...attrs.filter(([k]) => k === "ID"), ...title ? [["name", title]] : [], ...attrs.filter(([k]) => k !== "ID")] : [...title ? [["name", title]] : [], ...attrs];
|
|
1423
|
+
const attrText = ordered2.map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
|
|
1424
|
+
if (!builtin && name !== "SubTree") {
|
|
1425
|
+
const use = modelUses[name] ??= { category: block.category, template, keys: /* @__PURE__ */ new Set(), references: /* @__PURE__ */ new Set() };
|
|
1426
|
+
attrs.forEach(([k, v]) => {
|
|
1427
|
+
use.keys.add(k);
|
|
1428
|
+
if (/^\{.+\}$/.test(v)) use.references.add(k);
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
const children = childIdsInOrder(tree, block.id, layout);
|
|
1432
|
+
if (block.category === "decorator" && children.length !== 1) {
|
|
1433
|
+
issue("error", `${where} is a decorator with ${children.length} children. BT.CPP needs exactly one.`);
|
|
1434
|
+
} else if (block.category === "composite" && children.length === 0) {
|
|
1435
|
+
issue("error", `${where} has no children. BT.CPP needs at least one.`);
|
|
1436
|
+
} else if (isLeaf(block.category) && children.length > 0) {
|
|
1437
|
+
issue("error", `${where} is a leaf node but has children. BT.CPP doesn't allow that.`);
|
|
1438
|
+
}
|
|
1439
|
+
const indent = " ".repeat(depth);
|
|
1440
|
+
if (children.length === 0) {
|
|
1441
|
+
lines.push(`${indent}<${tag}${attrText}/>`);
|
|
1442
|
+
} else {
|
|
1443
|
+
lines.push(`${indent}<${tag}${attrText}>`);
|
|
1444
|
+
children.forEach((childId) => writeBlock(childId, depth + 1));
|
|
1445
|
+
lines.push(`${indent}</${tag}>`);
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
lines.push(` <BehaviorTree ID="${escapeAttr(id)}">`);
|
|
1449
|
+
rootChildren.forEach((childId) => writeBlock(childId, 2));
|
|
1450
|
+
lines.push(" </BehaviorTree>");
|
|
1451
|
+
const unreachable = Object.keys(tree.blocks).filter((blockId) => !visited.has(blockId)).length;
|
|
1452
|
+
if (unreachable > 0) {
|
|
1453
|
+
issue(
|
|
1454
|
+
"warning",
|
|
1455
|
+
`${unreachable} node${unreachable === 1 ? "" : "s"} in "${tree.title}" ${unreachable === 1 ? "isn't" : "aren't"} connected to the root and ${unreachable === 1 ? "was" : "were"} left out`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
ordered.forEach(writeTree);
|
|
1460
|
+
if (droppedDescriptions) {
|
|
1461
|
+
issue("info", "Descriptions and tree properties aren't part of BT.CPP XML and were left out");
|
|
1462
|
+
}
|
|
1463
|
+
const model2 = includeNodesModel ? nodesModelLines(modelUses) : { lines: [], inferred: [] };
|
|
1464
|
+
if (model2.inferred.length > 0) {
|
|
1465
|
+
issue(
|
|
1466
|
+
"info",
|
|
1467
|
+
`Port directions of ${model2.inferred.join(", ")} aren't known, so they were written as inputs. Import their TreeNodesModel (for Nav2, nav2_tree_nodes.xml) to get outputs right.`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
const head = version === 4 ? `<root BTCPP_format="4" main_tree_to_execute="${escapeAttr(xmlIds[mainTreeId])}">` : `<root main_tree_to_execute="${escapeAttr(xmlIds[mainTreeId])}">`;
|
|
1471
|
+
return {
|
|
1472
|
+
xml: ['<?xml version="1.0" encoding="UTF-8"?>', head, ...lines, ...model2.lines, "</root>", ""].join("\n"),
|
|
1473
|
+
issues
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function nodesModelLines(uses) {
|
|
1477
|
+
const names = Object.keys(uses).sort();
|
|
1478
|
+
const inferred = [];
|
|
1479
|
+
if (names.length === 0) return { lines: [], inferred };
|
|
1480
|
+
const lines = [" <TreeNodesModel>"];
|
|
1481
|
+
names.forEach((name) => {
|
|
1482
|
+
const { category, template, keys, references } = uses[name];
|
|
1483
|
+
if (category === "root") return;
|
|
1484
|
+
const ports = { ...template?.ports ?? {} };
|
|
1485
|
+
const extra = template?.ports ? [] : Object.keys(template && !template.isDefault ? template.properties : {});
|
|
1486
|
+
[...extra, ...keys].forEach((key) => {
|
|
1487
|
+
if (!ports[key] && !key.startsWith("_")) {
|
|
1488
|
+
if (references.has(key) && !inferred.includes(name)) inferred.push(name);
|
|
1489
|
+
const fallback = template && !template.isDefault ? template.properties[key] : void 0;
|
|
1490
|
+
ports[key] = {
|
|
1491
|
+
direction: "input",
|
|
1492
|
+
...fallback !== void 0 && fallback !== "" && typeof fallback !== "object" ? { default: String(fallback) } : {}
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
const tag = MODEL_TAGS[category];
|
|
1497
|
+
const portLines = Object.entries(ports).map(([key, port2]) => {
|
|
1498
|
+
const portTag = `${port2.direction}_port`;
|
|
1499
|
+
const attrs = ` name="${escapeAttr(key)}"` + (port2.type ? ` type="${escapeAttr(port2.type)}"` : "") + (port2.default !== void 0 ? ` default="${escapeAttr(port2.default)}"` : "");
|
|
1500
|
+
return port2.description ? ` <${portTag}${attrs}>${escapeText(port2.description)}</${portTag}>` : ` <${portTag}${attrs}/>`;
|
|
1501
|
+
});
|
|
1502
|
+
if (portLines.length === 0) {
|
|
1503
|
+
lines.push(` <${tag} ID="${escapeAttr(name)}"/>`);
|
|
1504
|
+
} else {
|
|
1505
|
+
lines.push(` <${tag} ID="${escapeAttr(name)}">`, ...portLines, ` </${tag}>`);
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
lines.push(" </TreeNodesModel>");
|
|
1509
|
+
return { lines, inferred };
|
|
1510
|
+
}
|
|
1511
|
+
function treeToBtcpp(project, treeId, options) {
|
|
1512
|
+
return exportTrees(project, treeId, "referenced", options);
|
|
1513
|
+
}
|
|
1514
|
+
function projectToBtcpp(project, options) {
|
|
1515
|
+
const treeIds = Object.keys(project.trees);
|
|
1516
|
+
if (treeIds.length === 0) throw new Error("This project has no trees");
|
|
1517
|
+
const main = project.selectedTreeId && project.trees[project.selectedTreeId] ? project.selectedTreeId : treeIds[0];
|
|
1518
|
+
return exportTrees(project, main, treeIds, options);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// ../../apps/editor/src/lib/share-link.ts
|
|
1522
|
+
var MAX_BYTES = 5e6;
|
|
1523
|
+
var toBase64Url = (bytes) => {
|
|
1524
|
+
let binary = "";
|
|
1525
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
1526
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
1527
|
+
}
|
|
1528
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1529
|
+
};
|
|
1530
|
+
var readCapped = async (stream) => {
|
|
1531
|
+
const reader = stream.getReader();
|
|
1532
|
+
const chunks = [];
|
|
1533
|
+
let size = 0;
|
|
1534
|
+
for (; ; ) {
|
|
1535
|
+
const { done, value } = await reader.read();
|
|
1536
|
+
if (done) break;
|
|
1537
|
+
size += value.length;
|
|
1538
|
+
if (size > MAX_BYTES) {
|
|
1539
|
+
await reader.cancel();
|
|
1540
|
+
throw new Error("Shared tree is too large");
|
|
1541
|
+
}
|
|
1542
|
+
chunks.push(value);
|
|
1543
|
+
}
|
|
1544
|
+
const out = new Uint8Array(size);
|
|
1545
|
+
let offset = 0;
|
|
1546
|
+
for (const chunk of chunks) {
|
|
1547
|
+
out.set(chunk, offset);
|
|
1548
|
+
offset += chunk.length;
|
|
1549
|
+
}
|
|
1550
|
+
return out;
|
|
1551
|
+
};
|
|
1552
|
+
var transform = (bytes, stream) => readCapped(new Blob([bytes]).stream().pipeThrough(stream));
|
|
1553
|
+
async function encodeSharedTree(data) {
|
|
1554
|
+
const json = new TextEncoder().encode(JSON.stringify(data));
|
|
1555
|
+
return toBase64Url(await transform(json, new CompressionStream("deflate")));
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/core/catalog.ts
|
|
1559
|
+
var listNodeTypes = () => Object.values(DEFAULT_NODES).filter((node) => node.category !== "root").map((node) => ({
|
|
1560
|
+
name: node.name,
|
|
1561
|
+
category: node.category,
|
|
1562
|
+
title: node.title,
|
|
1563
|
+
description: node.description,
|
|
1564
|
+
properties: node.properties ?? {}
|
|
1565
|
+
}));
|
|
1566
|
+
var EXAMPLE_TREES = [
|
|
1567
|
+
cooldown_specials_default,
|
|
1568
|
+
enemy_patrol_default,
|
|
1569
|
+
open_the_door_default,
|
|
1570
|
+
robot_navigate_recovery_default,
|
|
1571
|
+
robot_pick_and_place_default,
|
|
1572
|
+
survival_override_default
|
|
1573
|
+
];
|
|
1574
|
+
var listExamples = () => EXAMPLE_TREES.map(({ id, title, description }) => ({
|
|
1575
|
+
id,
|
|
1576
|
+
title,
|
|
1577
|
+
description,
|
|
1578
|
+
editorUrl: `${SITE}/?example=${id}`
|
|
1579
|
+
}));
|
|
1580
|
+
var getExample = (id) => EXAMPLE_TREES.find((tree) => tree.id === id);
|
|
1581
|
+
|
|
1582
|
+
// src/core/xml-dom.ts
|
|
1583
|
+
import { DOMParser as XmlDomParser } from "@xmldom/xmldom";
|
|
1584
|
+
var escapeText2 = (text) => text.replace(/&/g, "&").replace(/</g, "<");
|
|
1585
|
+
var NodeDOMParser = class {
|
|
1586
|
+
parseFromString(text, type) {
|
|
1587
|
+
try {
|
|
1588
|
+
return new XmlDomParser({
|
|
1589
|
+
onError: (level, message) => {
|
|
1590
|
+
if (level !== "warning") throw new Error(message);
|
|
1591
|
+
}
|
|
1592
|
+
}).parseFromString(text, type);
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
const raw = String(error.message);
|
|
1595
|
+
const [message] = (/caused Error: (.*)/.exec(raw)?.[1] ?? raw).split("\n");
|
|
1596
|
+
const at = /line:(\d+),col:(\d+)/.exec(raw);
|
|
1597
|
+
const detail = `${at ? `${at[1]}:${at[2]}: ` : ""}${message.trim()}`;
|
|
1598
|
+
return new XmlDomParser().parseFromString(`<parsererror>${escapeText2(detail)}</parsererror>`, "application/xml");
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
var ensureDomParser = async () => {
|
|
1603
|
+
if (typeof globalThis.DOMParser === "undefined") {
|
|
1604
|
+
globalThis.DOMParser = NodeDOMParser;
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
// src/core/load.ts
|
|
1609
|
+
async function loadInput(input2) {
|
|
1610
|
+
if (typeof input2 === "string" && isLikelyXml(input2.trim())) {
|
|
1611
|
+
await ensureDomParser();
|
|
1612
|
+
const parsed = parseBtcppXml(input2.trim(), { ...DEFAULT_NODES }, "horizontal");
|
|
1613
|
+
if (parsed.kind === "nodes") {
|
|
1614
|
+
throw new InvalidFileError("This BehaviorTree.CPP file only defines node models, with no trees", "not_behavior_tree");
|
|
1615
|
+
}
|
|
1616
|
+
const project = btcppToProject(parsed, "Imported project");
|
|
1617
|
+
return {
|
|
1618
|
+
format: "btcpp",
|
|
1619
|
+
kind: parsed.trees.length === 1 ? "tree" : "project",
|
|
1620
|
+
project,
|
|
1621
|
+
b3Trees: [],
|
|
1622
|
+
btcppVersion: parsed.version,
|
|
1623
|
+
issues: parsed.issues
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
let json = input2;
|
|
1627
|
+
if (typeof input2 === "string") {
|
|
1628
|
+
try {
|
|
1629
|
+
json = JSON.parse(input2.trim());
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
throw new InvalidFileError(`Not valid JSON or XML: ${error.message}`, "not_behavior_tree");
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
const imported = parseImportedJson(json);
|
|
1635
|
+
if (imported.kind === "nodes") {
|
|
1636
|
+
throw new InvalidFileError("This file only defines custom nodes, with no trees", "not_behavior_tree");
|
|
1637
|
+
}
|
|
1638
|
+
if (imported.kind === "tree") {
|
|
1639
|
+
const tree = imported.tree;
|
|
1640
|
+
const project = b3ToProject({ name: tree.title, trees: [tree], custom_nodes: tree.custom_nodes });
|
|
1641
|
+
return { format: "behavior3", kind: "tree", project, b3Trees: [tree], json, issues: [] };
|
|
1642
|
+
}
|
|
1643
|
+
const raw = json;
|
|
1644
|
+
const b3Trees = Array.isArray(raw.trees) ? raw.trees : [];
|
|
1645
|
+
return { format: "behavior3", kind: "project", project: imported.project, b3Trees, json, issues: [] };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// src/core/convert.ts
|
|
1649
|
+
var CONVERT_TARGETS = ["behavior3", "btcpp-v4", "btcpp-v3"];
|
|
1650
|
+
var toB3 = (loaded) => {
|
|
1651
|
+
const { project } = loaded;
|
|
1652
|
+
const trees = Object.values(project.trees);
|
|
1653
|
+
return loaded.kind === "tree" && trees.length === 1 ? treeToB3(trees[0], project, true) : projectToB3(project);
|
|
1654
|
+
};
|
|
1655
|
+
async function convertTree(input2, to) {
|
|
1656
|
+
const loaded = await loadInput(input2);
|
|
1657
|
+
const issues = loaded.issues.map((issue) => ({ level: issue.level, message: issue.message }));
|
|
1658
|
+
if (to === "behavior3") {
|
|
1659
|
+
return { output: `${JSON.stringify(toB3(loaded), null, 2)}
|
|
1660
|
+
`, format: to, issues };
|
|
1661
|
+
}
|
|
1662
|
+
const options = { version: to === "btcpp-v3" ? 3 : 4, layout: "horizontal" };
|
|
1663
|
+
const { project } = loaded;
|
|
1664
|
+
const trees = Object.values(project.trees);
|
|
1665
|
+
const result = loaded.kind === "tree" && trees.length === 1 ? treeToBtcpp(project, trees[0].id, options) : projectToBtcpp(project, options);
|
|
1666
|
+
issues.push(...result.issues.map((issue) => ({ level: issue.level, message: issue.message })));
|
|
1667
|
+
return { output: result.xml.endsWith("\n") ? result.xml : `${result.xml}
|
|
1668
|
+
`, format: to, issues };
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// src/core/guides.ts
|
|
1672
|
+
var get = async (url) => {
|
|
1673
|
+
const response = await fetch(url, { headers: { "User-Agent": "behaviortrees-cli" } });
|
|
1674
|
+
if (!response.ok) throw new Error(`${url} returned HTTP ${response.status}`);
|
|
1675
|
+
return response;
|
|
1676
|
+
};
|
|
1677
|
+
var listGuides = async () => (await get(`${SITE}/learn/guides.json`)).json();
|
|
1678
|
+
var readGuide = async (slug) => {
|
|
1679
|
+
if (!/^[a-z0-9-]+$/.test(slug)) throw new Error(`Not a guide slug: ${slug}`);
|
|
1680
|
+
const response = await fetch(`${SITE}/learn/${slug}.md`, { headers: { "User-Agent": "behaviortrees-cli" } });
|
|
1681
|
+
if (response.status === 404) {
|
|
1682
|
+
const slugs = (await listGuides()).map((guide) => guide.slug);
|
|
1683
|
+
throw new Error(`No guide "${slug}". Guides: ${slugs.join(", ")}`);
|
|
1684
|
+
}
|
|
1685
|
+
if (!response.ok) throw new Error(`Guide request returned HTTP ${response.status}`);
|
|
1686
|
+
return response.text();
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
// src/core/validate.ts
|
|
1690
|
+
var CATEGORIES = /* @__PURE__ */ new Set(["composite", "decorator", "action", "condition"]);
|
|
1691
|
+
var ALIASES = {
|
|
1692
|
+
Selector: "Priority",
|
|
1693
|
+
Fallback: "Priority",
|
|
1694
|
+
ReactiveSelector: "Priority",
|
|
1695
|
+
ReactiveSequence: "Sequence",
|
|
1696
|
+
Repeat: "Repeater",
|
|
1697
|
+
Timeout: "MaxTime",
|
|
1698
|
+
Invert: "Inverter",
|
|
1699
|
+
Not: "Inverter",
|
|
1700
|
+
AlwaysSuccess: "Succeeder",
|
|
1701
|
+
AlwaysFailure: "Failer"
|
|
1702
|
+
};
|
|
1703
|
+
var article = (word) => /^[aeiou]/.test(word) ? `an ${word}` : `a ${word}`;
|
|
1704
|
+
function checkCustomNodes(templates, out, tree) {
|
|
1705
|
+
for (const template of templates ?? []) {
|
|
1706
|
+
if (!template?.name) {
|
|
1707
|
+
out.push({ level: "error", message: 'A custom node has no "name"', tree });
|
|
1708
|
+
} else if (template.category && !CATEGORIES.has(template.category)) {
|
|
1709
|
+
out.push({
|
|
1710
|
+
level: "error",
|
|
1711
|
+
message: `Custom node "${template.name}" has category "${template.category}"; use action, condition, composite or decorator`,
|
|
1712
|
+
tree
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function checkTree(spec, categories, out) {
|
|
1718
|
+
const tree = spec.title || spec.id || "Untitled tree";
|
|
1719
|
+
const push = (level, message) => out.push({ level, message, tree });
|
|
1720
|
+
if (!spec.nodes || typeof spec.nodes !== "object" || Array.isArray(spec.nodes)) {
|
|
1721
|
+
push("error", 'The tree has no "nodes" object (node id \u2192 node)');
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
checkCustomNodes(spec.custom_nodes, out, tree);
|
|
1725
|
+
const treeCategories = new Map(categories);
|
|
1726
|
+
for (const template of spec.custom_nodes ?? []) {
|
|
1727
|
+
if (template?.name) treeCategories.set(template.name, template.category ?? "action");
|
|
1728
|
+
}
|
|
1729
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1730
|
+
for (const [key, node] of Object.entries(spec.nodes)) {
|
|
1731
|
+
if (!node || typeof node !== "object") {
|
|
1732
|
+
push("error", `Node "${key}" is not an object`);
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
const id = node.id ?? key;
|
|
1736
|
+
if (node.id && node.id !== key) {
|
|
1737
|
+
push("warning", `Node keyed "${key}" has id "${node.id}"; children and root must refer to "${node.id}"`);
|
|
1738
|
+
}
|
|
1739
|
+
byId.set(id, node);
|
|
1740
|
+
}
|
|
1741
|
+
const childrenOf = (node) => Array.isArray(node.children) ? node.children : node.child !== void 0 && node.child !== null ? [node.child] : [];
|
|
1742
|
+
if (byId.size > 0) {
|
|
1743
|
+
if (spec.root === void 0 || spec.root === null) {
|
|
1744
|
+
push("error", 'The tree has nodes but no "root" (the id of the first node under the root)');
|
|
1745
|
+
} else if (!byId.has(spec.root)) {
|
|
1746
|
+
push("error", `"root" is "${spec.root}", but there is no node with that id`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const parents = /* @__PURE__ */ new Map();
|
|
1750
|
+
const undeclared = /* @__PURE__ */ new Map();
|
|
1751
|
+
const aliased = /* @__PURE__ */ new Set();
|
|
1752
|
+
for (const [id, node] of byId) {
|
|
1753
|
+
if (!node.name) {
|
|
1754
|
+
push("error", `Node "${id}" has no "name"`);
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
const known = treeCategories.get(node.name);
|
|
1758
|
+
const category = known ?? (Array.isArray(node.children) ? "composite" : node.child !== void 0 ? "decorator" : "action");
|
|
1759
|
+
if (!known && ALIASES[node.name] && !aliased.has(node.name)) {
|
|
1760
|
+
aliased.add(node.name);
|
|
1761
|
+
push(
|
|
1762
|
+
"warning",
|
|
1763
|
+
`"${node.name}" isn't a behavior3 node, so the editor adds it as a custom ${category}. Did you mean "${ALIASES[node.name]}"?`
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
if (!known && !ALIASES[node.name]) undeclared.set(node.name, category);
|
|
1767
|
+
if (node.children !== void 0 && !Array.isArray(node.children)) {
|
|
1768
|
+
push("error", `Node "${id}" has "children" that isn't an array`);
|
|
1769
|
+
}
|
|
1770
|
+
const children = childrenOf(node);
|
|
1771
|
+
if (category === "composite" && node.child !== void 0 && !Array.isArray(node.children)) {
|
|
1772
|
+
push("warning", `"${id}" is a composite (${node.name}); composites list "children", not a single "child"`);
|
|
1773
|
+
}
|
|
1774
|
+
if (category === "composite" && children.length === 0) {
|
|
1775
|
+
push("warning", `Composite "${id}" (${node.name}) has no children, so it does nothing`);
|
|
1776
|
+
}
|
|
1777
|
+
if (category === "decorator") {
|
|
1778
|
+
if (Array.isArray(node.children) && node.children.length > 1) {
|
|
1779
|
+
push("error", `Decorator "${id}" (${node.name}) has ${node.children.length} children; decorators wrap exactly one "child"`);
|
|
1780
|
+
} else if (Array.isArray(node.children)) {
|
|
1781
|
+
push("warning", `Decorator "${id}" (${node.name}) uses "children"; decorators take a single "child"`);
|
|
1782
|
+
} else if (children.length === 0) {
|
|
1783
|
+
push("warning", `Decorator "${id}" (${node.name}) has no child`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if ((category === "action" || category === "condition") && children.length > 0) {
|
|
1787
|
+
push("error", `"${id}" is ${article(category)} (${node.name}), so it can't have children`);
|
|
1788
|
+
}
|
|
1789
|
+
for (const child of children) {
|
|
1790
|
+
if (!byId.has(child)) {
|
|
1791
|
+
push("error", `Node "${id}" lists child "${child}", which doesn't exist`);
|
|
1792
|
+
} else if (child === spec.root) {
|
|
1793
|
+
push("error", `The root node "${child}" is also a child of "${id}"`);
|
|
1794
|
+
} else if (parents.has(child)) {
|
|
1795
|
+
push("error", `Node "${child}" has two parents ("${parents.get(child)}" and "${id}"); each node can appear once`);
|
|
1796
|
+
} else {
|
|
1797
|
+
parents.set(child, id);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (undeclared.size > 0) {
|
|
1802
|
+
const names = [...undeclared].map(([name, category]) => `${name} (${category})`).join(", ");
|
|
1803
|
+
push(
|
|
1804
|
+
"info",
|
|
1805
|
+
`Not built in and not in custom_nodes, so the editor infers their category: ${names}. Declare them in custom_nodes to set it, e.g. {"name": "IsEnemyVisible", "category": "condition"}.`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
const reached = /* @__PURE__ */ new Set();
|
|
1809
|
+
const walk = (id, path) => {
|
|
1810
|
+
if (path.has(id)) {
|
|
1811
|
+
push("error", `Cycle: "${id}" is its own descendant`);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (reached.has(id) || !byId.has(id)) return;
|
|
1815
|
+
reached.add(id);
|
|
1816
|
+
path.add(id);
|
|
1817
|
+
for (const child of childrenOf(byId.get(id))) walk(child, path);
|
|
1818
|
+
path.delete(id);
|
|
1819
|
+
};
|
|
1820
|
+
if (spec.root && byId.has(spec.root)) walk(spec.root, /* @__PURE__ */ new Set());
|
|
1821
|
+
const orphans = [...byId.keys()].filter((id) => !reached.has(id));
|
|
1822
|
+
if (orphans.length > 0 && spec.root && byId.has(spec.root)) {
|
|
1823
|
+
push(
|
|
1824
|
+
"warning",
|
|
1825
|
+
`${orphans.length === 1 ? "Node" : "Nodes"} ${orphans.map((id) => `"${id}"`).join(", ")} ${orphans.length === 1 ? "isn't" : "aren't"} connected to the root, so ${orphans.length === 1 ? "it" : "they"} will never run`
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
async function validateTree(input2) {
|
|
1830
|
+
let loaded;
|
|
1831
|
+
try {
|
|
1832
|
+
loaded = await loadInput(input2);
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
return { valid: false, trees: [], findings: [{ level: "error", message: error.message }] };
|
|
1835
|
+
}
|
|
1836
|
+
const findings = loaded.issues.map((issue) => ({ level: issue.level, message: issue.message }));
|
|
1837
|
+
if (loaded.format === "behavior3") {
|
|
1838
|
+
const categories = new Map(
|
|
1839
|
+
Object.values(DEFAULT_NODES).map((node) => [node.name, node.category])
|
|
1840
|
+
);
|
|
1841
|
+
const projectNodes = loaded.json.custom_nodes;
|
|
1842
|
+
if (loaded.kind === "project") checkCustomNodes(projectNodes, findings);
|
|
1843
|
+
for (const template of projectNodes ?? []) {
|
|
1844
|
+
if (template?.name) categories.set(template.name, template.category ?? "action");
|
|
1845
|
+
}
|
|
1846
|
+
for (const tree of loaded.b3Trees) checkTree(tree, categories, findings);
|
|
1847
|
+
}
|
|
1848
|
+
const trees = Object.values(loaded.project.trees).map((tree) => ({
|
|
1849
|
+
title: tree.title,
|
|
1850
|
+
nodes: Object.keys(tree.blocks).length - 1
|
|
1851
|
+
}));
|
|
1852
|
+
if (trees.length === 0) findings.push({ level: "error", message: "The project has no trees" });
|
|
1853
|
+
return {
|
|
1854
|
+
valid: !findings.some((finding) => finding.level === "error"),
|
|
1855
|
+
format: loaded.format,
|
|
1856
|
+
kind: loaded.kind,
|
|
1857
|
+
trees,
|
|
1858
|
+
findings
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// src/core/link.ts
|
|
1863
|
+
var LONG_LINK = 3e4;
|
|
1864
|
+
var InvalidTreeError = class extends Error {
|
|
1865
|
+
constructor(findings) {
|
|
1866
|
+
super(
|
|
1867
|
+
`The tree has errors:
|
|
1868
|
+
${findings.filter((finding) => finding.level === "error").map((finding) => `- ${finding.tree ? `${finding.tree}: ` : ""}${finding.message}`).join("\n")}`
|
|
1869
|
+
);
|
|
1870
|
+
this.findings = findings;
|
|
1871
|
+
}
|
|
1872
|
+
findings;
|
|
1873
|
+
};
|
|
1874
|
+
async function createEditorLink(input2) {
|
|
1875
|
+
const validation = await validateTree(input2);
|
|
1876
|
+
if (!validation.valid) throw new InvalidTreeError(validation.findings);
|
|
1877
|
+
const loaded = await loadInput(input2);
|
|
1878
|
+
const data = loaded.format === "behavior3" ? loaded.json : toB3(loaded);
|
|
1879
|
+
const url = `${SITE}/editor#tree=${await encodeSharedTree(data)}`;
|
|
1880
|
+
const findings = [...validation.findings];
|
|
1881
|
+
if (url.length > LONG_LINK) {
|
|
1882
|
+
findings.push({
|
|
1883
|
+
level: "warning",
|
|
1884
|
+
message: `The link is ${url.length.toLocaleString("en-US")} characters; some apps cut off long links, so also share the file`
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
return { url, title: validation.trees.map((tree) => tree.title).join(", "), findings };
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
export {
|
|
1891
|
+
SITE,
|
|
1892
|
+
listNodeTypes,
|
|
1893
|
+
listExamples,
|
|
1894
|
+
getExample,
|
|
1895
|
+
CONVERT_TARGETS,
|
|
1896
|
+
convertTree,
|
|
1897
|
+
listGuides,
|
|
1898
|
+
readGuide,
|
|
1899
|
+
validateTree,
|
|
1900
|
+
InvalidTreeError,
|
|
1901
|
+
createEditorLink
|
|
1902
|
+
};
|