Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
BTtoVSMigrator.cpp
Go to the documentation of this file.
1/**
2 * @file BTtoVSMigrator.cpp
3 * @brief Implementation of the BT v2 -> VS v4 migrator (Phase 6).
4 * @author Olympe Engine
5 * @date 2026-03-09
6 *
7 * C++14 compliant — no std::optional, structured bindings, std::filesystem.
8 */
9
10#include "BTtoVSMigrator.h"
11
12#include <sstream>
13#include <algorithm>
14
15namespace Olympe {
16
17// ============================================================================
18// IsBTv2
19// ============================================================================
20
22{
23 if (!j.is_object()) return false;
24
25 // Must be blueprintType == "BehaviorTree"
26 if (!j.contains("blueprintType")) return false;
27 if (!j["blueprintType"].is_string()) return false;
28 if (j["blueprintType"].get<std::string>() != "BehaviorTree") return false;
29
30 // schema_version must be absent, 0, 1, or 2
31 if (j.contains("schema_version") && j["schema_version"].is_number())
32 {
33 int ver = j["schema_version"].get<int>();
34 if (ver > 2) return false;
35 }
36 return true;
37}
38
39// ============================================================================
40// IsDecoratorName
41// ============================================================================
42
43bool BTtoVSMigrator::IsDecoratorName(const std::string& nodeName)
44{
45 // Common BT v2 Decorator-style node name prefixes
46 if (nodeName == "Repeater" || nodeName.substr(0, 6) == "Repeat") return true;
47 if (nodeName == "Inverter" || nodeName.substr(0, 6) == "Invert") return true;
48 if (nodeName == "Cooldown" || nodeName.substr(0, 8) == "Cooldown") return true;
49 if (nodeName == "Timeout" || nodeName.substr(0, 7) == "Timeout") return true;
50 return false;
51}
52
53// ============================================================================
54// MapNodeType
55// ============================================================================
56
58{
59 if (btType == "Selector") return TaskNodeType::Branch;
60 if (btType == "Sequence") return TaskNodeType::Sequence;
61 if (btType == "Action") return TaskNodeType::AtomicTask;
62 if (btType == "Condition") return TaskNodeType::Branch;
63 if (btType == "Decorator") return TaskNodeType::Decorator;
64 if (btType == "Root") return TaskNodeType::EntryPoint;
65 if (btType == "Start") return TaskNodeType::EntryPoint;
66 // Default: treat unknown types as AtomicTask
68}
69
70// ============================================================================
71// ConvertNode
72// ============================================================================
73
75 std::vector<std::string>& outErrors)
76{
78
79 // id
80 if (btNode.contains("id") && btNode["id"].is_number())
81 def.NodeID = btNode["id"].get<int32_t>();
82 else
83 {
84 outErrors.push_back("BTtoVSMigrator: node missing 'id' field");
86 }
87
88 // name
89 if (btNode.contains("name") && btNode["name"].is_string())
90 def.NodeName = btNode["name"].get<std::string>();
91 else
92 def.NodeName = "UnnamedNode";
93
94 // type
95 std::string btType;
96 if (btNode.contains("type") && btNode["type"].is_string())
97 btType = btNode["type"].get<std::string>();
98
99 def.Type = MapNodeType(btType);
100
101 // Name-based heuristics for special node types:
102 // BT v2 sometimes encodes Decorator-style nodes as "Action" with a descriptive name.
104 {
106 }
107
108 // Populate ChildrenIDs from the "children" array
109 if (btNode.contains("children") && btNode["children"].is_array())
110 {
111 for (const auto& childVal : btNode["children"])
112 {
114 if (childVal.is_number())
115 childID = childVal.get<int32_t>();
116 else if (childVal.is_object() && childVal.contains("id"))
117 childID = childVal["id"].get<int32_t>();
119 def.ChildrenIDs.push_back(childID);
120 }
121 }
122
123 // For Action nodes: determine AtomicTaskID from actionType > parameters.taskType > node name
125 {
126 // 1. Top-level "actionType" field (common in newer BT v2 files)
127 if (btNode.contains("actionType") && btNode["actionType"].is_string())
128 {
129 std::string at = btNode["actionType"].get<std::string>();
130 if (!at.empty())
131 def.AtomicTaskID = at;
132 }
133
134 // 2. Fallback: "taskType" inside parameters
135 if (def.AtomicTaskID.empty() &&
136 btNode.contains("parameters") && btNode["parameters"].is_object())
137 {
138 const auto& params = btNode["parameters"];
139 if (params.contains("taskType") && params["taskType"].is_string())
140 def.AtomicTaskID = params["taskType"].get<std::string>();
141 }
142
143 // 3. Final fallback: use node name as AtomicTaskID
144 if (def.AtomicTaskID.empty() && !def.NodeName.empty())
145 def.AtomicTaskID = def.NodeName;
146 }
147
148 // Copy all string parameters as literal bindings (for all node types)
149 if (btNode.contains("parameters") && btNode["parameters"].is_object())
150 {
151 const auto& params = btNode["parameters"];
152 for (auto it = params.begin(); it != params.end(); ++it)
153 {
154 if (it.value().is_string())
155 {
158 pb.LiteralValue = TaskValue(it.value().get<std::string>());
159 def.Parameters[it.key()] = pb;
160 }
161 }
162 }
163
164 // Store position as editor params
165 if (btNode.contains("position") && btNode["position"].is_object())
166 {
167 const auto& pos = btNode["position"];
168 float px = pos.contains("x") && pos["x"].is_number() ? pos["x"].get<float>() : 0.0f;
169 float py = pos.contains("y") && pos["y"].is_number() ? pos["y"].get<float>() : 0.0f;
170
173 bx.LiteralValue = TaskValue(px);
175 by.LiteralValue = TaskValue(py);
176 def.Parameters["__posX"] = bx;
177 def.Parameters["__posY"] = by;
178 }
179
180 return def;
181}
182
183// ============================================================================
184// ConvertConnections
185// ============================================================================
186
187std::vector<ExecPinConnection> BTtoVSMigrator::ConvertConnections(
189 std::vector<std::string>& /*outErrors*/)
190{
191 std::vector<ExecPinConnection> conns;
192 if (!nodesArray.is_array()) return conns;
193
194 for (const auto& btNode : nodesArray)
195 {
196 if (!btNode.is_object()) continue;
197
199 if (btNode.contains("id") && btNode["id"].is_number())
200 srcID = btNode["id"].get<int32_t>();
201 if (srcID == NODE_INDEX_NONE) continue;
202
203 if (!btNode.contains("children") || !btNode["children"].is_array()) continue;
204
205 for (const auto& childVal : btNode["children"])
206 {
208 if (childVal.is_number())
209 childID = childVal.get<int32_t>();
210 else if (childVal.is_object() && childVal.contains("id"))
211 childID = childVal["id"].get<int32_t>();
212
213 if (childID == NODE_INDEX_NONE) continue;
214
217 ec.SourcePinName = "Out";
218 ec.TargetNodeID = childID;
219 ec.TargetPinName = "In";
220 conns.push_back(ec);
221 }
222 }
223
224 return conns;
225}
226
227// ============================================================================
228// ConvertBlackboard
229// ============================================================================
230
233 std::vector<std::string>& /*outErrors*/)
234{
235 // Try multiple known locations for blackboard data in BT v2 files
236 const nlohmann::json* bbArray = nullptr;
238
239 if (btV2Json.contains("blackboard") && btV2Json["blackboard"].is_array())
240 bbArray = &btV2Json["blackboard"];
241 else if (btV2Json.contains("data") && btV2Json["data"].is_object())
242 {
243 const auto& d1 = btV2Json["data"];
244 if (d1.contains("blackboard") && d1["blackboard"].is_array())
245 bbArray = &d1["blackboard"];
246 else if (d1.contains("data") && d1["data"].is_object())
247 {
248 const auto& d2 = d1["data"];
249 if (d2.contains("blackboard") && d2["blackboard"].is_array())
250 bbArray = &d2["blackboard"];
251 }
252 }
253
254 if (!bbArray) return;
255
256 for (const auto& entry : *bbArray)
257 {
258 if (!entry.is_object()) continue;
259
261 if (entry.contains("key") && entry["key"].is_string())
262 be.Key = entry["key"].get<std::string>();
263 else continue; // key is mandatory
264
265 if (entry.contains("type") && entry["type"].is_string())
266 {
267 std::string t = entry["type"].get<std::string>();
268 if (t == "Bool") be.Type = VariableType::Bool;
269 else if (t == "Int") be.Type = VariableType::Int;
270 else if (t == "Float") be.Type = VariableType::Float;
271 else if (t == "String") be.Type = VariableType::String;
272 else be.Type = VariableType::None;
273 }
274
275 vsGraph.Blackboard.push_back(be);
276 }
277}
278
279// ============================================================================
280// Convert (public)
281// ============================================================================
282
284 std::vector<std::string>& outErrors)
285{
286 TaskGraphTemplate result;
287 result.GraphType = "VisualScript";
288
289 // Extract graph name
290 if (btV2Json.contains("name") && btV2Json["name"].is_string())
291 result.Name = btV2Json["name"].get<std::string>();
292 else
293 result.Name = "MigratedGraph";
294
295 // Navigate nested structure to find node array
296 const nlohmann::json* nodesPtr = nullptr;
298
299 // Try flat structure first (data.nodes or nodes)
300 const nlohmann::json* data1 = nullptr;
301 const nlohmann::json* data2 = nullptr;
302
303 if (btV2Json.contains("nodes") && btV2Json["nodes"].is_array())
304 {
305 nodesPtr = &btV2Json["nodes"];
306 if (btV2Json.contains("rootNodeId") && btV2Json["rootNodeId"].is_number())
307 rootNodeID = btV2Json["rootNodeId"].get<int32_t>();
308 }
309 else if (btV2Json.contains("data") && btV2Json["data"].is_object())
310 {
311 data1 = &btV2Json["data"];
312 if (data1->contains("nodes") && (*data1)["nodes"].is_array())
313 {
314 nodesPtr = &(*data1)["nodes"];
315 if (data1->contains("rootNodeId") && (*data1)["rootNodeId"].is_number())
316 rootNodeID = (*data1)["rootNodeId"].get<int32_t>();
317 }
318 else if (data1->contains("data") && (*data1)["data"].is_object())
319 {
320 data2 = &(*data1)["data"];
321 if (data2->contains("nodes") && (*data2)["nodes"].is_array())
322 {
323 nodesPtr = &(*data2)["nodes"];
324 if (data2->contains("rootNodeId") && (*data2)["rootNodeId"].is_number())
325 rootNodeID = (*data2)["rootNodeId"].get<int32_t>();
326 }
327 }
328 }
329
330 if (!nodesPtr)
331 {
332 outErrors.push_back("BTtoVSMigrator: could not find 'nodes' array in BT v2 JSON");
333 return result;
334 }
335
336 // Convert nodes
337 for (const auto& btNode : *nodesPtr)
338 {
340 if (def.NodeID == NODE_INDEX_NONE) continue;
341
342 // Detect entry point by matching rootNodeID or by name/type heuristic
343 std::string btType;
344 if (btNode.contains("type") && btNode["type"].is_string())
345 btType = btNode["type"].get<std::string>();
346
347 bool isRoot = (def.NodeID == rootNodeID) ||
348 (btType == "Root") ||
349 (btType == "Start" && result.EntryPointID == NODE_INDEX_NONE);
350
351 if (isRoot)
352 {
354 result.EntryPointID = def.NodeID;
355 result.RootNodeID = def.NodeID;
356 }
357
358 result.Nodes.push_back(def);
359 }
360
361 // If no entry point was identified, promote the first node
362 if (result.EntryPointID == NODE_INDEX_NONE && !result.Nodes.empty())
363 {
364 result.Nodes[0].Type = TaskNodeType::EntryPoint;
365 result.EntryPointID = result.Nodes[0].NodeID;
366 result.RootNodeID = result.Nodes[0].NodeID;
367 }
368
369 // Convert connections
371
372 // Convert blackboard
374
375 // Build lookup cache
376 result.BuildLookupCache();
377
378 return result;
379}
380
381} // namespace Olympe
Converts legacy BT v2 JSON graphs to ATS VS v4 JSON format (Phase 6).
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
static TaskGraphTemplate Convert(const nlohmann::json &btV2Json, std::vector< std::string > &outErrors)
Converts a BT v2 JSON document to a TaskGraphTemplate.
static TaskNodeDefinition ConvertNode(const nlohmann::json &btNode, std::vector< std::string > &outErrors)
Converts one BT node JSON object to a TaskNodeDefinition.
static void ConvertBlackboard(const nlohmann::json &btV2Json, TaskGraphTemplate &vsGraph, std::vector< std::string > &outErrors)
Copies blackboard entries from the BT JSON to the VS template.
static TaskNodeType MapNodeType(const std::string &btType)
Maps a BT node type string to the closest TaskNodeType equivalent.
static bool IsDecoratorName(const std::string &nodeName)
Returns true if nodeName indicates a BT Decorator node.
static bool IsBTv2(const nlohmann::json &j)
Returns true if j looks like a BT v2 document.
static std::vector< ExecPinConnection > ConvertConnections(const nlohmann::json &nodesArray, std::vector< std::string > &outErrors)
Builds exec connections from the BT "children" tree structure.
Immutable, shareable task graph asset.
std::vector< TaskNodeDefinition > Nodes
All graph nodes.
std::vector< ExecPinConnection > ExecConnections
Explicit exec connections (ATS VS only)
std::string Name
Friendly name of this template (e.g. "PatrolBehaviour")
int32_t RootNodeID
ID of the root node (must exist in Nodes)
void BuildLookupCache()
Rebuilds the internal ID-to-node lookup map from the Nodes vector.
int32_t EntryPointID
ID of the EntryPoint node (for VS graphs)
std::string GraphType
Graph type: "BehaviorTree" (legacy) or "VisualScript" (ATS VS)
C++14-compliant type-safe value container for task parameters.
< Provides AssetID and INVALID_ASSET_ID
@ Int
32-bit signed integer
@ Float
Single-precision float.
@ String
std::string
@ None
Uninitialized / empty value.
@ Literal
Value is embedded directly in the template.
TaskNodeType
Identifies the role of a node in the task graph.
@ AtomicTask
Leaf node that executes a single atomic task.
@ Sequence
Executes children in order; stops on first failure.
@ Decorator
Wraps a single child and modifies its behaviour.
@ EntryPoint
Unique entry node for VS graphs (replaces Root)
@ Branch
If/Else conditional (Then / Else exec outputs)
constexpr int32_t NODE_INDEX_NONE
Sentinel value for "no node" in node index / ID fields.
nlohmann::json json
Single entry in the graph's declared blackboard schema (local or global).
Explicit connection between a named exec-out pin of a source node and the exec-in pin of a target nod...
Describes how a single parameter value is supplied to a task node.
ParameterBindingType Type
Binding mode.
Full description of a single node in the task graph.
std::vector< int32_t > ChildrenIDs
Child node IDs (control-flow nodes only; empty for AtomicTask/Decorator leaf)
std::string AtomicTaskID
Atomic task type identifier (used when Type == AtomicTask)
TaskNodeType Type
Node role.
std::string NodeName
Human-readable name.
std::unordered_map< std::string, ParameterBinding > Parameters
Named parameter bindings passed to the atomic task.
int32_t NodeID
Unique ID within this template.