Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
TaskGraphLoader.cpp
Go to the documentation of this file.
1/**
2 * @file TaskGraphLoader.cpp
3 * @brief Clean schema v4 parser for ATS Visual Script task graphs.
4 * @author Olympe Engine
5 * @date 2026-03-08
6 *
7 * @details
8 * Primary path: schema v4 flat ATS VisualScript JSON.
9 * For schema v3: delegates to TaskGraphMigrator_v3_to_v4, then parses v4.
10 * For schema v2/legacy: minimal BehaviorTree-style parsing for backward compat.
11 *
12 * C++14 compliant - no std::filesystem, no C++17/20 features.
13 */
14
15#include "TaskGraphLoader.h"
17#include "../BlueprintEditor/BTtoVSMigrator.h"
18#include "../BlueprintEditor/MathOpOperand.h"
19
20#include <string>
21#include <vector>
22#include <unordered_map>
23#include <fstream>
24#include <algorithm>
25
26#ifdef _WIN32
27# ifndef WIN32_LEAN_AND_MEAN
28# define WIN32_LEAN_AND_MEAN
29# endif
30# include <windows.h>
31#else
32# include <dirent.h>
33#endif
34
35#include "../system/system_utils.h"
36#include "../json_helper.h"
37
38namespace Olympe {
39
40// ============================================================================
41// Public: LoadFromFile
42// ============================================================================
43
45 std::vector<std::string>& outErrors)
46{
47 // Phase 26: Normalize path - convert forward slashes to backslashes for Windows
48 std::string normalizedPath = path;
49 for (size_t i = 0; i < normalizedPath.size(); ++i)
50 {
51 if (normalizedPath[i] == '/') normalizedPath[i] = '\\';
52 }
53
54 SYSTEM_LOG << "[TaskGraphLoader] Loading from file: " << normalizedPath << std::endl;
55
56 // Warn if the file does not carry the expected .ats extension.
57 {
58 const size_t dotPos = normalizedPath.find_last_of('.');
59 const bool hasAtsExt = (dotPos != std::string::npos)
60 && (normalizedPath.substr(dotPos + 1) == "ats");
61 if (!hasAtsExt)
62 {
63 SYSTEM_LOG << "[TaskGraphLoader] WARNING: Expected .ats extension for: "
64 << normalizedPath << std::endl;
65 }
66 }
67
68 json data;
70 {
71 outErrors.push_back("Failed to open or parse JSON file: " + normalizedPath);
72 SYSTEM_LOG << "[TaskGraphLoader] ERROR: Failed to open or parse: " << normalizedPath << std::endl;
73 return nullptr;
74 }
75
76 return LoadFromJson(data, outErrors);
77}
78
79// ============================================================================
80// Public: LoadFromJson
81// ============================================================================
82
84 std::vector<std::string>& outErrors)
85{
86 const int schemaVersion = JsonHelper::GetInt(data, "schema_version", 2);
87 SYSTEM_LOG << "[TaskGraphLoader] Schema version: " << schemaVersion << std::endl;
88
89 TaskGraphTemplate* tmpl = nullptr;
90
91 if (schemaVersion == 4)
92 {
93 // Primary path: parse v4 flat ATS VS format directly.
95 }
96 else if (schemaVersion == 3)
97 {
98 // Delegate v3 to migrator, then parse the resulting v4 JSON.
99 // The migrator expects flat nodes with NextOnSuccess/NextOnFailure fields.
100 // If the v3 JSON uses the older BT-style "data.nodes" structure instead,
101 // the migrator will produce 0 nodes, and we fall back to direct legacy parsing.
102 std::vector<std::string> migrateErrors;
104
105 bool migratorProducedNodes = !v4data.empty()
106 && v4data.contains("nodes")
107 && v4data["nodes"].is_array()
108 && !v4data["nodes"].empty();
109
111 {
112 // Migrator produced no nodes — fall back to direct v4 parse.
113 SYSTEM_LOG << "[TaskGraphLoader] v3 migrator produced no nodes; "
114 "using direct v4 parse\n";
116 }
117 else
118 {
120 }
121 }
122 else
123 {
124 // Schema v2: route through BTtoVSMigrator only for doubly-nested
125 // BT v2 documents (data.data.nodes). Singly-nested documents
126 // (data.nodes) are handled by ParseSchemaV4, which already understands
127 // both formats and preserves BT node types (Sequence, Decorator, etc.).
128 bool doublyNested = data.contains("data") &&
129 data["data"].is_object() &&
130 data["data"].contains("data");
131
133 {
134 std::vector<std::string> migrateErrors;
136 for (const auto& e : migrateErrors)
137 outErrors.push_back(e);
138
140 }
141 else
142 {
143 // Flat, singly-nested, or unknown v2 format: use ParseSchemaV4
144 // which already handles data.nodes and preserves BT structure.
146 }
147 }
148
149 if (tmpl == nullptr)
150 {
151 return nullptr;
152 }
153
154 tmpl->BuildLookupCache();
155
156 // Phase 24.3 - Poka-Yoke: Clean up any invalid exec connections (e.g., to data-pure nodes)
157 int sanitizedCount = tmpl->SanitizeExecConnections();
158 if (sanitizedCount > 0)
159 {
160 SYSTEM_LOG << "[TaskGraphLoader] Sanitized " << sanitizedCount
161 << " invalid exec connection(s) - graph is now consistent\n";
162 }
163
164 if (!tmpl->Validate())
165 {
166 outErrors.push_back("TaskGraphTemplate::Validate() failed for template '" + tmpl->Name + "'");
167 delete tmpl;
168 return nullptr;
169 }
170
171 SYSTEM_LOG << "[TaskGraphLoader] Loaded '" << tmpl->Name
172 << "' (" << tmpl->Nodes.size() << " nodes)\n";
173 return tmpl;
174}
175
176// ============================================================================
177// Public: ValidateJson
178// ============================================================================
179
181 std::vector<std::string>& outErrors)
182{
183 bool valid = true;
184
185 // Check for nodes array: flat (v4) or nested (v2/v3).
186 bool hasNodes = JsonHelper::IsArray(data, "nodes");
187 bool hasDataSection = JsonHelper::IsObject(data, "data");
188
189 if (!hasNodes && !hasDataSection)
190 {
191 outErrors.push_back("Missing required 'data' object or 'nodes' array in JSON");
192 valid = false;
193 }
194 else if (!hasNodes && hasDataSection)
195 {
196 const json& dataSection = data["data"];
197 if (!JsonHelper::IsArray(dataSection, "nodes"))
198 {
199 outErrors.push_back("Missing required 'nodes' array in data section");
200 valid = false;
201 }
202 if (JsonHelper::GetInt(dataSection, "rootNodeId", -1) < 0 &&
203 JsonHelper::GetInt(data, "rootNodeId", -1) < 0)
204 {
205 outErrors.push_back("Missing required 'rootNodeId' in data section");
206 valid = false;
207 }
208 }
209
210 return valid;
211}
212
213// ============================================================================
214// v4 parsing: ParseSchemaV4
215// ============================================================================
216
218 std::vector<std::string>& outErrors)
219{
220 // Determine if JSON uses flat format (nodes at root) or nested (data.nodes).
221 const bool flatFormat = JsonHelper::IsArray(data, "nodes");
222 const json* rootForNodes = flatFormat ? &data : nullptr;
223
224 // If not flat, try nested data section for backward compat with old assets.
225 if (!flatFormat)
226 {
227 if (!JsonHelper::IsObject(data, "data"))
228 {
229 outErrors.push_back("[v4] Missing 'nodes' array or 'data' object in JSON");
230 return nullptr;
231 }
232 const json& dataSection = data["data"];
233 if (!JsonHelper::IsArray(dataSection, "nodes"))
234 {
235 outErrors.push_back("[v4] Missing 'nodes' array in data section");
236 return nullptr;
237 }
239 }
240
242
243 // Read metadata — accept both camelCase and PascalCase field names.
244 tmpl->Name = JsonHelper::GetString(data, "name", "Unnamed");
245
246 // Infer the default graph type:
247 // - Explicit "blueprintType": "BehaviorTree" -> BehaviorTree legacy asset.
248 // - Nested data.nodes format without an explicit graphType -> treat as BehaviorTree.
249 // - Otherwise -> VisualScript (the primary v4 format).
250 {
251 const std::string blueprintType = JsonHelper::GetString(data, "blueprintType", "");
252 const std::string defaultGraphType =
253 (blueprintType == "BehaviorTree" || (!flatFormat && blueprintType.empty()
254 && !data.contains("graphType") && !data.contains("graph_type")))
255 ? "BehaviorTree"
256 : "VisualScript";
257 tmpl->GraphType = JsonHelper::GetString(data, "graphType",
258 JsonHelper::GetString(data, "graph_type", defaultGraphType));
259 }
260
261 // Entry point ID (flat v4) or rootNodeId (legacy nested).
262 if (flatFormat)
263 {
264 // For the flat v4 format, the first EntryPoint node implicitly is the root.
265 // entryPointId is optional; if missing, VSGraphExecutor scans for EntryPoint node.
266 tmpl->EntryPointID = JsonHelper::GetInt(data, "entryPointId", NODE_INDEX_NONE);
267 tmpl->RootNodeID = tmpl->EntryPointID;
268 }
269 else
270 {
271 const json& ds = data["data"];
272 tmpl->EntryPointID = JsonHelper::GetInt(ds, "entryPointId", NODE_INDEX_NONE);
273 tmpl->RootNodeID = ResolveRootNodeId(data, ds);
274 if (tmpl->RootNodeID == NODE_INDEX_NONE && tmpl->EntryPointID != NODE_INDEX_NONE)
275 {
276 tmpl->RootNodeID = tmpl->EntryPointID;
277 }
278 }
279
280 // Parse nodes.
282 [&](const json& nodeJson, size_t /*idx*/)
283 {
285 tmpl->Nodes.push_back(nd);
286 // First EntryPoint node sets the root if not already set.
287 if (tmpl->RootNodeID == NODE_INDEX_NONE &&
289 {
290 tmpl->RootNodeID = nd.NodeID;
291 tmpl->EntryPointID = nd.NodeID;
292 }
293 });
294
295 // Parse blackboard.
297
298 // Parse exec connections (handles camelCase, PascalCase, and nested variants).
300
301 // Parse data connections.
303
304 // Phase 24 — Condition Preset Bank (embedded in graph JSON)
305 // Presets are now part of the graph, making blueprints self-contained.
306 // Deserialize the "presets" array if it exists.
307 if (JsonHelper::IsArray(data, "presets"))
308 {
309 JsonHelper::ForEachInArray(data, "presets",
310 [&](const json& presetJson, size_t /*idx*/)
311 {
313 if (!preset.id.empty()) // Only add if preset has valid ID
314 {
315 tmpl->Presets.push_back(preset);
316 }
317 });
318
319 SYSTEM_LOG << "[TaskGraphLoader] ParseSchemaV4: Phase 24 - deserialized "
320 << tmpl->Presets.size() << " embedded presets\n";
321 }
322
323 // Phase 24 Global Blackboard Integration: Deserialize global variable overrides
324 // These are entity-specific values persisted with the graph (not templates)
325 if (JsonHelper::IsObject(data, "globalVariableValues"))
326 {
327 // Store the JSON object in the template for later restoration by EntityBlackboard
328 tmpl->GlobalVariableValues = data["globalVariableValues"];
329 SYSTEM_LOG << "[TaskGraphLoader] ParseSchemaV4: Phase 24 - loaded "
330 << "globalVariableValues from graph\n";
331 }
332
333 // SubGraph metadata (Phase 3)
334 tmpl->IsSubGraph = JsonHelper::GetBool(data, "isSubGraph", false);
335
336 if (JsonHelper::IsArray(data, "inputParameters"))
337 {
338 JsonHelper::ForEachInArray(data, "inputParameters",
339 [&](const json& paramJson, size_t /*idx*/)
340 {
344 if (!param.Name.empty()) tmpl->InputParameters.push_back(param);
345 });
346 }
347
348 if (JsonHelper::IsArray(data, "outputParameters"))
349 {
350 JsonHelper::ForEachInArray(data, "outputParameters",
351 [&](const json& paramJson, size_t /*idx*/)
352 {
356 if (!param.Name.empty()) tmpl->OutputParameters.push_back(param);
357 });
358 }
359
360 return tmpl;
361}
362
363// ============================================================================
364// v4 node parsing
365// ============================================================================
366
368 const std::string& graphType,
369 std::vector<std::string>& outErrors)
370{
372
373 // Accept both new ("id") and legacy ("nodeID") field names.
375 JsonHelper::GetInt(nodeJson, "nodeID", -1));
376
377 // Accept "label", "name", or "nodeName".
378 nd.NodeName = JsonHelper::GetString(nodeJson, "label",
380 JsonHelper::GetString(nodeJson, "nodeName", "")));
381
382 // Node type: accept "type" (new) or "nodeType" (legacy).
383 const std::string typeStr = JsonHelper::GetString(nodeJson, "type",
384 JsonHelper::GetString(nodeJson, "nodeType", ""));
385 bool typeOk = true;
386 nd.Type = StringToNodeType(typeStr, graphType, typeOk);
387 if (!typeOk)
388 {
389 outErrors.push_back("Node " + std::to_string(nd.NodeID) +
390 " has unknown type '" + typeStr + "'");
391 }
392
393 // Name-based Decorator detection for BT-style assets.
394 // Some BT v2 files encode Decorator nodes as type "Action" with a
395 // descriptive name. IsDecoratorName() centralises this heuristic.
396 if (nd.Type == TaskNodeType::AtomicTask &&
398 {
400 }
401
402 // AtomicTask type name: "taskType" (new) or "AtomicTaskID"/"atomicTaskId"/"actionType"/"conditionType" (legacy).
403 nd.AtomicTaskID = JsonHelper::GetString(nodeJson, "taskType",
404 JsonHelper::GetString(nodeJson, "AtomicTaskID",
405 JsonHelper::GetString(nodeJson, "atomicTaskId",
406 JsonHelper::GetString(nodeJson, "actionType",
407 JsonHelper::GetString(nodeJson, "conditionType", "")))));
408
409 // VS-specific node fields (new flat format).
410 nd.DelaySeconds = JsonHelper::GetFloat(nodeJson, "delaySeconds", 0.0f);
411 nd.BBKey = JsonHelper::GetString(nodeJson, "bbKey", "");
412 nd.SubGraphPath = JsonHelper::GetString(nodeJson, "subGraphPath", "");
413 nd.ConditionID = JsonHelper::GetString(nodeJson, "conditionKey",
414 JsonHelper::GetString(nodeJson, "conditionId", ""));
415
416 // Phase 24 FIX: Initialize Parameters["subgraph_path"] for SubGraph nodes
417 // This ensures the UI panel can find and edit the path without creating it dynamically
418 // Only initialize if:
419 // 1. This is a SubGraph node
420 // 2. SubGraphPath is not empty (has a valid path from JSON or legacy fields)
421 // 3. Parameters["subgraph_path"] doesn't already exist (wasn't parsed from params field)
422 if (nd.Type == TaskNodeType::SubGraph && !nd.SubGraphPath.empty())
423 {
424 auto pathParamIt = nd.Parameters.find("subgraph_path");
425 if (pathParamIt == nd.Parameters.end())
426 {
427 // Parameters["subgraph_path"] doesn't exist - initialize it from SubGraphPath
430 pathBinding.LiteralValue = TaskValue(nd.SubGraphPath);
431 nd.Parameters["subgraph_path"] = pathBinding;
432 SYSTEM_LOG << "[TaskGraphLoader] ParseNodeV4: initialized Parameters[subgraph_path] = '"
433 << nd.SubGraphPath << "' for SubGraph node " << nd.NodeID << "\n";
434 }
435 else
436 {
437 // Parameters["subgraph_path"] already exists (parsed from JSON params field)
438 // Ensure SubGraphPath stays in sync with it
439 if (pathParamIt->second.Type == ParameterBindingType::Literal)
440 {
441 std::string paramPath = pathParamIt->second.LiteralValue.to_string();
442 if (!paramPath.empty() && nd.SubGraphPath != paramPath)
443 {
444 // Use the value from Parameters if SubGraphPath is less specific
445 nd.SubGraphPath = paramPath;
446 SYSTEM_LOG << "[TaskGraphLoader] ParseNodeV4: synced SubGraphPath from Parameters[subgraph_path] = '"
447 << paramPath << "' for SubGraph node " << nd.NodeID << "\n";
448 }
449 }
450 }
451 }
452
453 // Math operation fields.
454 nd.MathOperator = JsonHelper::GetString(nodeJson, "mathOp", "");
455
456 // Phase 24 Milestone 2 — MathOp operand deserialization
457 // Deserialize the complete MathOpRef (left operand, operator, right operand)
458 if (nd.Type == TaskNodeType::MathOp && JsonHelper::IsObject(nodeJson, "mathOpRef"))
459 {
460 nd.mathOpRef = MathOpRef::FromJson(nodeJson["mathOpRef"]);
461 // Ensure MathOperator is updated from the deserialized mathOpRef
462 if (nd.MathOperator.empty() && !nd.mathOpRef.mathOperator.empty())
463 {
464 nd.MathOperator = nd.mathOpRef.mathOperator;
465 }
466 SYSTEM_LOG << "[TaskGraphLoader] ParseNodeV4: deserialized mathOpRef for MathOp node "
467 << nd.NodeID << "\n";
468 }
469
470 // Parse parameters: accept "params" (new) or "parameters" (legacy).
471 if (JsonHelper::IsObject(nodeJson, "params"))
472 {
473 ParseParameters(nodeJson["params"], nd.Parameters);
474 }
475 else if (JsonHelper::IsObject(nodeJson, "parameters"))
476 {
477 ParseParameters(nodeJson["parameters"], nd.Parameters);
478
479 // Legacy nested fields inside parameters block.
480 const json& params = nodeJson["parameters"];
481 if (nd.BBKey.empty())
482 {
483 nd.BBKey = JsonHelper::GetString(params, "key", "");
484 }
485 if (nd.MathOperator.empty())
486 {
487 nd.MathOperator = JsonHelper::GetString(params, "operator", "");
488 }
489 if (nd.DelaySeconds == 0.0f)
490 {
491 nd.DelaySeconds = JsonHelper::GetFloat(params, "delaySeconds", 0.0f);
492 }
493 if (nd.SubGraphPath.empty())
494 {
495 nd.SubGraphPath = JsonHelper::GetString(params, "subGraphPath", "");
496 }
497 }
498
499 // Legacy DelaySeconds at node level (patrol.json uses "DelaySeconds").
500 if (nd.DelaySeconds == 0.0f)
501 {
502 nd.DelaySeconds = JsonHelper::GetFloat(nodeJson, "DelaySeconds", 0.0f);
503 }
504
505 // Switch cases.
506 if (JsonHelper::IsArray(nodeJson, "switch_cases"))
507 {
508 JsonHelper::ForEachInArray(nodeJson, "switch_cases",
509 [&](const json& c, size_t /*i*/) {
510 if (c.is_string()) nd.SwitchCases.push_back(c.get<std::string>());
511 });
512 }
513
514 // Switch variable (Phase 22-A) — the BB key whose value is switched on.
515 if (nd.Type == TaskNodeType::Switch)
516 {
517 nd.switchVariable = JsonHelper::GetString(nodeJson, "switchVariable", "");
518 }
519
520 // Structured switch cases (Phase 22-A).
521 if (nd.Type == TaskNodeType::Switch && JsonHelper::IsArray(nodeJson, "switchCases"))
522 {
523 std::unordered_map<std::string, size_t> seenValues; // E011: duplicate value check
525 [&](const json& c, size_t idx) {
526 if (!c.is_object())
527 return;
529 scd.value = JsonHelper::GetString(c, "value", "");
530 scd.pinName = JsonHelper::GetString(c, "pin", "");
531 scd.customLabel = JsonHelper::GetString(c, "label", "");
532
533 // E011 — duplicate case value
534 if (!scd.value.empty())
535 {
536 auto it = seenValues.find(scd.value);
537 if (it != seenValues.end())
538 {
539 std::string msg = "[E011] Switch node #"
540 + std::to_string(nd.NodeID)
541 + ": duplicate case value '" + scd.value
542 + "' at index " + std::to_string(idx)
543 + " (first seen at index "
544 + std::to_string(it->second) + ")";
545 outErrors.push_back(msg);
546 SYSTEM_LOG << "[TaskGraphLoader] " << msg << "\n";
547 }
548 else
549 {
550 seenValues[scd.value] = idx;
551 }
552 }
553
554 // E012 — pin name must match expected pattern (non-empty)
555 if (scd.pinName.empty())
556 {
557 std::string msg = "[E012] Switch node #"
558 + std::to_string(nd.NodeID)
559 + ": switchCases[" + std::to_string(idx)
560 + "] has empty pin name";
561 outErrors.push_back(msg);
562 SYSTEM_LOG << "[TaskGraphLoader] " << msg << "\n";
563 }
564
565 nd.switchCases.push_back(scd);
566 });
567
568 // ── PHASE 2 FIX: Regenerate DynamicExecOutputPins from switchCases ───
569 // After loading switchCases from JSON, regenerate the derived cache
570 // (DynamicExecOutputPins) so canvas pins are visible immediately.
571 // Pins are derived from switchCases[1..end] (Case_0 is base, not dynamic).
572 nd.DynamicExecOutputPins.clear();
573 for (size_t caseIdx = 1; caseIdx < nd.switchCases.size(); ++caseIdx)
574 {
575 const SwitchCaseDefinition& caseData = nd.switchCases[caseIdx];
576 nd.DynamicExecOutputPins.push_back(caseData.pinName);
577 }
578
579 SYSTEM_LOG << "[TaskGraphLoader] ParseNodeV4: Phase 2 FIX - regenerated "
580 << nd.DynamicExecOutputPins.size() << " dynamic pins for Switch node #"
581 << nd.NodeID << "\n";
582 }
583
584 // Dynamic exec-out pins (VSSequence and Switch, Phase 20-C / Phase 21-D).
585 // IMPORTANT: For Switch nodes, DO NOT load dynamicExecPins from JSON!
586 // Phase 2 FIX ensures Switch pins are REGENERATED from switchCases above.
587 // Loading from JSON would append stale/incorrect pins.
588 if ((nd.Type == TaskNodeType::VSSequence || nd.Type == TaskNodeType::Switch) &&
589 nodeJson.contains("dynamicExecPins") &&
590 nodeJson["dynamicExecPins"].is_array())
591 {
592 // Skip for Switch nodes: Phase 2 FIX regenerates from switchCases
593 if (nd.Type == TaskNodeType::VSSequence)
594 {
595 const json& dynPins = nodeJson["dynamicExecPins"];
596 for (size_t p = 0; p < dynPins.size(); ++p)
597 {
598 if (dynPins[p].is_string())
599 nd.DynamicExecOutputPins.push_back(dynPins[p].get<std::string>());
600 }
601 }
602 else if (nd.Type == TaskNodeType::Switch)
603 {
604 // Phase 2 FIX: DynamicExecOutputPins already regenerated from switchCases
605 // Do not load from JSON to avoid stale pins
606 SYSTEM_LOG << "[TaskGraphLoader] ParseNodeV4: Switch node #" << nd.NodeID
607 << " - skipped loading dynamicExecPins from JSON (Phase 2 FIX)\n";
608 }
609 }
610
611 // Backward-compat children array (BT-style, may appear in migrated v3).
612 if (JsonHelper::IsArray(nodeJson, "children"))
613 {
614 const json& ch = nodeJson["children"];
615 for (size_t i = 0; i < ch.size(); ++i)
616 {
617 if (ch[i].is_number()) nd.ChildrenIDs.push_back(ch[i].get<int>());
618 }
619 }
620
621 // Compat flow fields.
622 nd.NextOnSuccess = JsonHelper::GetInt(nodeJson, "nextOnSuccess", NODE_INDEX_NONE);
623 nd.NextOnFailure = JsonHelper::GetInt(nodeJson, "nextOnFailure", NODE_INDEX_NONE);
624
625 // SubGraph: parse InputParams (Phase 3)
626 if (nd.Type == TaskNodeType::SubGraph && JsonHelper::IsObject(nodeJson, "InputParams"))
627 {
628 const json& inputParamsJson = nodeJson["InputParams"];
629 for (auto it = inputParamsJson.begin(); it != inputParamsJson.end(); ++it)
630 {
631 const std::string& paramName = it.key();
632 const json& bindingJson = it.value();
633
635 std::string bindingType = JsonHelper::GetString(bindingJson, "Type", "Literal");
636
637 if (bindingType == "Literal")
638 {
640 if (bindingJson.contains("LiteralValue"))
641 {
642 binding.LiteralValue = ParsePrimitiveValue(bindingJson["LiteralValue"]);
643 }
644 }
645 else if (bindingType == "LocalVariable")
646 {
648 binding.VariableName = JsonHelper::GetString(bindingJson, "VariableName", "");
649 }
650 else if (bindingType == "AtomicTaskID")
651 {
653 binding.VariableName = JsonHelper::GetString(bindingJson, "value",
654 JsonHelper::GetString(bindingJson, "VariableName", ""));
655 }
656 else if (bindingType == "ConditionID")
657 {
659 binding.VariableName = JsonHelper::GetString(bindingJson, "value",
660 JsonHelper::GetString(bindingJson, "VariableName", ""));
661 }
662 else if (bindingType == "MathOperator")
663 {
665 binding.VariableName = JsonHelper::GetString(bindingJson, "value",
666 JsonHelper::GetString(bindingJson, "VariableName", ""));
667 }
668 else if (bindingType == "ComparisonOp")
669 {
671 binding.VariableName = JsonHelper::GetString(bindingJson, "value",
672 JsonHelper::GetString(bindingJson, "VariableName", ""));
673 }
674 else if (bindingType == "SubGraphPath")
675 {
677 binding.VariableName = JsonHelper::GetString(bindingJson, "value",
678 JsonHelper::GetString(bindingJson, "VariableName", ""));
679 }
680
681 nd.InputParams[paramName] = binding;
682 }
683 }
684
685 // SubGraph: parse OutputParams (Phase 3)
686 if (nd.Type == TaskNodeType::SubGraph && JsonHelper::IsObject(nodeJson, "OutputParams"))
687 {
688 const json& outputParamsJson = nodeJson["OutputParams"];
689 for (auto it = outputParamsJson.begin(); it != outputParamsJson.end(); ++it)
690 {
691 const std::string& paramName = it.key();
692 if (it.value().is_string())
693 {
694 nd.OutputParams[paramName] = it.value().get<std::string>();
695 }
696 }
697 }
698
699 // Editor position (schema v4 only — ignored at runtime).
700 if (JsonHelper::IsObject(nodeJson, "position"))
701 {
702 nd.EditorPosX = JsonHelper::GetFloat(nodeJson["position"], "x", 0.0f);
703 nd.EditorPosY = JsonHelper::GetFloat(nodeJson["position"], "y", 0.0f);
704 nd.HasEditorPos = true;
705 }
706
707 // Structured conditions (Phase 23-B.4 — Branch/While).
708 // Also supports v4 legacy format: { "variable", "operator", "compareValue" }
709 if (JsonHelper::IsArray(nodeJson, "conditions"))
710 {
712 [&](const json& cj, size_t /*idx*/)
713 {
714 if (!cj.is_object())
715 return;
716
718
719 // -- Left side --
720 cond.leftMode = JsonHelper::GetString(cj, "leftMode", "Variable");
721 cond.leftPin = JsonHelper::GetString(cj, "leftPin", "");
722 cond.leftVariable = JsonHelper::GetString(cj, "leftVariable", "");
723 if (cj.contains("leftConstValue"))
724 cond.leftConstValue = ParsePrimitiveValue(cj["leftConstValue"]);
725
726 // v4 legacy: "variable" field -> Variable mode
727 if (cond.leftMode == "Variable" && cond.leftVariable.empty())
728 {
729 const std::string legacyVar = JsonHelper::GetString(cj, "variable", "");
730 if (!legacyVar.empty())
731 {
732 cond.leftVariable = legacyVar;
733 SYSTEM_LOG << "[TaskGraphLoader] v4->v5 migration: condition "
734 << "variable='" << legacyVar << "' mapped to leftVariable\n";
735 }
736 }
737
738 // -- Operator --
739 cond.operatorStr = JsonHelper::GetString(cj, "operator", "==");
740
741 // -- Right side --
742 cond.rightMode = JsonHelper::GetString(cj, "rightMode", "Const");
743 cond.rightPin = JsonHelper::GetString(cj, "rightPin", "");
744 cond.rightVariable = JsonHelper::GetString(cj, "rightVariable", "");
745 if (cj.contains("rightConstValue"))
746 cond.rightConstValue = ParsePrimitiveValue(cj["rightConstValue"]);
747
748 // v4 legacy: "compareValue" field -> Const mode (or Variable if it's a bare name)
749 if (cond.rightMode == "Const" && cond.rightConstValue.IsNone())
750 {
751 if (cj.contains("compareValue"))
752 {
753 const json& cv = cj["compareValue"];
754 // Heuristic: if it's a string with no spaces -> treat as Variable reference
755 if (cv.is_string())
756 {
757 const std::string cvStr = cv.get<std::string>();
758 bool hasSpace = false;
759 for (size_t i = 0; i < cvStr.size(); ++i)
760 {
761 if (cvStr[i] == ' ') { hasSpace = true; break; }
762 }
763 if (!hasSpace && !cvStr.empty())
764 {
765 cond.rightMode = "Variable";
766 cond.rightVariable = cvStr;
767 SYSTEM_LOG << "[TaskGraphLoader] v4->v5 migration: "
768 << "compareValue='" << cvStr
769 << "' looks like a variable name -> rightMode=Variable\n";
770 }
771 else
772 {
773 cond.rightConstValue = ParsePrimitiveValue(cv);
774 }
775 }
776 else
777 {
778 cond.rightConstValue = ParsePrimitiveValue(cv);
779 }
780 }
781 }
782
783 // -- Type hint --
784 const std::string typeStr = JsonHelper::GetString(cj, "compareType", "None");
785 cond.compareType = StringToVariableType(typeStr);
786
787 nd.conditions.push_back(cond);
788 });
789 }
790
791 // Phase 24 Milestone 2.2 — conditionRefs deserialization (new inline system).
792 // Reconstructs OperandRef data including dynamicPinID for Pin-mode operands.
793 if (JsonHelper::IsArray(nodeJson, "conditionRefs"))
794 {
795 JsonHelper::ForEachInArray(nodeJson, "conditionRefs",
796 [&](const json& refJson, size_t idx)
797 {
798 if (!refJson.is_object())
799 return;
800
802
803 if (refJson.contains("conditionIndex") && refJson["conditionIndex"].is_number_integer())
804 ref.conditionIndex = refJson["conditionIndex"].get<int>();
805 else
806 ref.conditionIndex = static_cast<int>(idx);
807
808 // Left operand
809 if (refJson.contains("leftOperand") && refJson["leftOperand"].is_object())
810 {
811 const json& lj = refJson["leftOperand"];
812 const std::string modeStr = JsonHelper::GetString(lj, "mode", "Const");
813 if (modeStr == "Variable")
814 {
815 ref.leftOperand.mode = OperandRef::Mode::Variable;
816 ref.leftOperand.variableName = JsonHelper::GetString(lj, "variableName", "");
817 }
818 else if (modeStr == "Pin")
819 {
820 ref.leftOperand.mode = OperandRef::Mode::Pin;
821 ref.leftOperand.dynamicPinID = JsonHelper::GetString(lj, "dynamicPinID", "");
822 }
823 else
824 {
825 ref.leftOperand.mode = OperandRef::Mode::Const;
826 ref.leftOperand.constValue = JsonHelper::GetString(lj, "constValue", "");
827 }
828 }
829
830 ref.operatorStr = JsonHelper::GetString(refJson, "operator", "==");
831
832 // Right operand
833 if (refJson.contains("rightOperand") && refJson["rightOperand"].is_object())
834 {
835 const json& rj = refJson["rightOperand"];
836 const std::string modeStr = JsonHelper::GetString(rj, "mode", "Const");
837 if (modeStr == "Variable")
838 {
839 ref.rightOperand.mode = OperandRef::Mode::Variable;
840 ref.rightOperand.variableName = JsonHelper::GetString(rj, "variableName", "");
841 }
842 else if (modeStr == "Pin")
843 {
844 ref.rightOperand.mode = OperandRef::Mode::Pin;
845 ref.rightOperand.dynamicPinID = JsonHelper::GetString(rj, "dynamicPinID", "");
846 }
847 else
848 {
849 ref.rightOperand.mode = OperandRef::Mode::Const;
850 ref.rightOperand.constValue = JsonHelper::GetString(rj, "constValue", "");
851 }
852 }
853
854 const std::string typeStr = JsonHelper::GetString(refJson, "compareType", "Float");
855 ref.compareType = StringToVariableType(typeStr);
856
857 nd.conditionOperandRefs.push_back(ref);
858 });
859
860 SYSTEM_LOG << "[TaskGraphLoader] Phase 24: deserialized "
861 << nd.conditionOperandRefs.size() << " conditionRefs for node "
862 << nd.NodeID << "\n";
863 }
864
865 // Phase 24 Milestone 2.3 — nodeConditionRefs deserialization (preset IDs + logical operators)
866 // Reconstructs NodeConditionRef objects (which preset is used and its logical operator)
867 if (JsonHelper::IsArray(nodeJson, "nodeConditionRefs"))
868 {
869 JsonHelper::ForEachInArray(nodeJson, "nodeConditionRefs",
870 [&](const json& nrefJson, size_t /*idx*/)
871 {
872 if (!nrefJson.is_object())
873 return;
874
875 // Use NodeConditionRef::FromJson() to properly deserialize all fields
876 // including leftPinID and rightPinID for dynamic data pins
878
879 nd.conditionRefs.push_back(ncref);
880 });
881
882 SYSTEM_LOG << "[TaskGraphLoader] Phase 24: deserialized "
883 << nd.conditionRefs.size() << " nodeConditionRefs for node "
884 << nd.NodeID << "\n";
885 }
886
887 // Phase 24 FINAL FIX: Bidirectional sync SubGraphPath ↔ Parameters["subgraph_path"]
888 // This ensures both are synchronized after all parsing is complete.
889 // Handles all cases: new JSON (params field), old JSON (subGraphPath field), or missing path
890 if (nd.Type == TaskNodeType::SubGraph)
891 {
892 auto pathParamIt = nd.Parameters.find("subgraph_path");
893
894 // Case A: Parameters["subgraph_path"] exists with value → it's canonical, ensure SubGraphPath matches
895 if (pathParamIt != nd.Parameters.end() &&
897 {
898 std::string paramPath = pathParamIt->second.LiteralValue.to_string();
899 if (!paramPath.empty())
900 {
901 // Use Parameters value as source-of-truth
902 if (nd.SubGraphPath != paramPath)
903 {
904 nd.SubGraphPath = paramPath;
905 SYSTEM_LOG << "[TaskGraphLoader] ParseNodeV4 final sync: restored SubGraphPath from Parameters = '"
906 << paramPath << "' for node " << nd.NodeID << "\n";
907 }
908 }
909 }
910
911 // Case B: SubGraphPath has value but Parameters doesn't → create Parameters from SubGraphPath
912 if (!nd.SubGraphPath.empty() &&
913 (pathParamIt == nd.Parameters.end() ||
914 (pathParamIt->second.Type == ParameterBindingType::Literal &&
915 pathParamIt->second.LiteralValue.to_string().empty())))
916 {
919 pathBinding.LiteralValue = TaskValue(nd.SubGraphPath);
920 nd.Parameters["subgraph_path"] = pathBinding;
921 SYSTEM_LOG << "[TaskGraphLoader] ParseNodeV4 final sync: initialized Parameters[subgraph_path] = '"
922 << nd.SubGraphPath << "' for node " << nd.NodeID << "\n";
923 }
924 }
925
926 return nd;
927}
928
929// ============================================================================
930// v4 blackboard parsing
931// ============================================================================
932
935 std::vector<std::string>& /*outErrors*/)
936{
937 // Accept "blackboard" (new), "localBlackboard" (legacy), or "data.blackboard".
938 const json* bbArray = nullptr;
939
940 if (JsonHelper::IsArray(root, "blackboard"))
941 {
942 bbArray = &root["blackboard"];
943 }
944 else if (JsonHelper::IsArray(root, "localBlackboard"))
945 {
946 bbArray = &root["localBlackboard"];
947 }
948 else if (JsonHelper::IsObject(root, "data"))
949 {
950 const json& ds = root["data"];
951 if (JsonHelper::IsArray(ds, "blackboard"))
952 {
953 bbArray = &ds["blackboard"];
954 }
955 else if (JsonHelper::IsArray(ds, "localBlackboard"))
956 {
957 bbArray = &ds["localBlackboard"];
958 }
959 }
960
961 if (bbArray == nullptr) return;
962
963 for (const json& entryJson : *bbArray)
964 {
966
967 // Accept "key" (new) or "Key" (PascalCase legacy).
969 JsonHelper::GetString(entryJson, "Key", ""));
970
971 // Accept "type" (new) or "Type" (PascalCase legacy).
972 const std::string typeStr = JsonHelper::GetString(entryJson, "type",
973 JsonHelper::GetString(entryJson, "Type", "None"));
975
976 // Accept "value" (new) or "default"/"Default" (legacy) for initial value.
979 || GetChildValue(entryJson, "default", defaultVal)
980 || GetChildValue(entryJson, "Default", defaultVal);
981 if (hasDefault)
982 {
984 }
985
986 // Coerce: ensure the stored default value type matches the declared entry
987 // type. Mismatches arise when (a) the value field is absent, (b) a JSON
988 // integer literal like "100" was used instead of "100.0" for a Float entry
989 // (nlohmann parses integer-looking numbers as number_integer), or
990 // (c) a Vector entry was serialised as a JSON object that ParsePrimitiveValue
991 // cannot convert.
992 //
993 // BUG-028 Fix: For Float entries whose parsed default is an Int (case b),
994 // coerce the int value to float to preserve the actual numeric value instead
995 // of falling back to 0.0f. All other mismatches fall back to the type
996 // default so that subsequent AsFloat() / AsInt() / AsVector() calls do not
997 // throw.
998 if (entry.Type != VariableType::None &&
999 (entry.Default.IsNone() || entry.Default.GetType() != entry.Type))
1000 {
1001 if (entry.Type == VariableType::Float &&
1002 !entry.Default.IsNone() &&
1003 entry.Default.GetType() == VariableType::Int)
1004 {
1005 // Preserve numeric value: int literal in JSON for a Float field
1006 entry.Default = TaskValue(static_cast<float>(entry.Default.AsInt()));
1007 }
1008 else
1009 {
1010 entry.Default = GetDefaultValueForType(entry.Type);
1011 }
1012 }
1013
1014 entry.IsGlobal = JsonHelper::GetBool(entryJson, "global",
1015 JsonHelper::GetBool(entryJson, "IsGlobal", false));
1016
1017 if (!entry.Key.empty())
1018 {
1019 tmpl->Blackboard.push_back(entry);
1020 }
1021 }
1022}
1023
1024// ============================================================================
1025// v4 exec connections parsing
1026// ============================================================================
1027
1030{
1031 const json* arr = nullptr;
1032
1033 // Priority: camelCase new -> PascalCase legacy -> nested data section.
1034 if (JsonHelper::IsArray(root, "execConnections"))
1035 {
1036 arr = &root["execConnections"];
1037 }
1038 else if (JsonHelper::IsArray(root, "ExecConnections"))
1039 {
1040 arr = &root["ExecConnections"];
1041 }
1042 else if (JsonHelper::IsObject(root, "data"))
1043 {
1044 const json& ds = root["data"];
1045 if (JsonHelper::IsArray(ds, "exec_connections"))
1046 {
1047 arr = &ds["exec_connections"];
1048 }
1049 else if (JsonHelper::IsArray(ds, "ExecConnections"))
1050 {
1051 arr = &ds["ExecConnections"];
1052 }
1053 }
1054
1055 if (arr == nullptr) return;
1056
1057 for (const json& c : *arr)
1058 {
1060
1061 // New format: fromNode/fromPin/toNode.
1062 // Legacy format: SourceNodeID/SourcePinName/TargetNodeID/TargetPinName.
1063 conn.SourceNodeID = JsonHelper::GetInt(c, "fromNode",
1064 JsonHelper::GetInt(c, "SourceNodeID", NODE_INDEX_NONE));
1065 conn.SourcePinName = JsonHelper::GetString(c, "fromPin",
1066 JsonHelper::GetString(c, "SourcePinName", ""));
1067 conn.TargetNodeID = JsonHelper::GetInt(c, "toNode",
1068 JsonHelper::GetInt(c, "TargetNodeID", NODE_INDEX_NONE));
1069 conn.TargetPinName = JsonHelper::GetString(c, "toPin",
1070 JsonHelper::GetString(c, "TargetPinName", "In"));
1071
1072 if (conn.SourceNodeID != NODE_INDEX_NONE && conn.TargetNodeID != NODE_INDEX_NONE)
1073 {
1074 tmpl->ExecConnections.push_back(conn);
1075 }
1076 }
1077}
1078
1079// ============================================================================
1080// v4 data connections parsing
1081// ============================================================================
1082
1085{
1086 const json* arr = nullptr;
1087
1088 if (JsonHelper::IsArray(root, "dataConnections"))
1089 {
1090 arr = &root["dataConnections"];
1091 }
1092 else if (JsonHelper::IsArray(root, "DataConnections"))
1093 {
1094 arr = &root["DataConnections"];
1095 }
1096 else if (JsonHelper::IsObject(root, "data"))
1097 {
1098 const json& ds = root["data"];
1099 if (JsonHelper::IsArray(ds, "data_connections"))
1100 {
1101 arr = &ds["data_connections"];
1102 }
1103 else if (JsonHelper::IsArray(ds, "DataConnections"))
1104 {
1105 arr = &ds["DataConnections"];
1106 }
1107 }
1108
1109 if (arr == nullptr) return;
1110
1111 for (const json& c : *arr)
1112 {
1114 conn.SourceNodeID = JsonHelper::GetInt(c, "fromNode",
1115 JsonHelper::GetInt(c, "SourceNodeID", NODE_INDEX_NONE));
1116 conn.SourcePinName = JsonHelper::GetString(c, "fromPin",
1117 JsonHelper::GetString(c, "SourcePinName", ""));
1118 conn.TargetNodeID = JsonHelper::GetInt(c, "toNode",
1119 JsonHelper::GetInt(c, "TargetNodeID", NODE_INDEX_NONE));
1120 conn.TargetPinName = JsonHelper::GetString(c, "toPin",
1121 JsonHelper::GetString(c, "TargetPinName", ""));
1122
1123 if (conn.SourceNodeID != NODE_INDEX_NONE && conn.TargetNodeID != NODE_INDEX_NONE)
1124 {
1125 tmpl->DataConnections.push_back(conn);
1126 }
1127 }
1128}
1129
1130// ============================================================================
1131// Shared helpers
1132// ============================================================================
1133
1135 std::unordered_map<std::string, ParameterBinding>& outParams)
1136{
1137 for (auto it = paramsJson.begin(); it != paramsJson.end(); ++it)
1138 {
1139 const std::string& key = it.key();
1140 const json& val = it.value();
1141
1143
1144 if (val.is_object())
1145 {
1146 // Structured binding — check "bindingType" / "Type" field.
1147 std::string btype = JsonHelper::GetString(val, "bindingType",
1148 JsonHelper::GetString(val, "Type", "Literal"));
1149
1150 if (btype == "Variable" || btype == "LocalVariable")
1151 {
1153 binding.VariableName = JsonHelper::GetString(val, "variableName",
1154 JsonHelper::GetString(val, "VariableName", ""));
1155 }
1156 else if (btype == "AtomicTaskID")
1157 {
1159 binding.VariableName = JsonHelper::GetString(val, "value",
1160 JsonHelper::GetString(val, "VariableName", ""));
1161 }
1162 else if (btype == "ConditionID")
1163 {
1165 binding.VariableName = JsonHelper::GetString(val, "value",
1166 JsonHelper::GetString(val, "VariableName", ""));
1167 }
1168 else if (btype == "MathOperator")
1169 {
1171 binding.VariableName = JsonHelper::GetString(val, "value",
1172 JsonHelper::GetString(val, "VariableName", ""));
1173 }
1174 else if (btype == "ComparisonOp")
1175 {
1177 binding.VariableName = JsonHelper::GetString(val, "value",
1178 JsonHelper::GetString(val, "VariableName", ""));
1179 }
1180 else if (btype == "SubGraphPath")
1181 {
1183 binding.VariableName = JsonHelper::GetString(val, "value",
1184 JsonHelper::GetString(val, "VariableName", ""));
1185 }
1186 else
1187 {
1188 // Literal binding with nested value.
1190 json vj;
1191 if (GetChildValue(val, "value", vj) ||
1192 GetChildValue(val, "LiteralValue", vj))
1193 {
1194 binding.LiteralValue = ParsePrimitiveValue(vj);
1195 }
1196 }
1197 }
1198 else
1199 {
1200 // Primitive value: Literal binding.
1202 binding.LiteralValue = ParsePrimitiveValue(val);
1203 }
1204
1206 }
1207}
1208
1209// ============================================================================
1210
1212{
1213 if (val.is_boolean()) return TaskValue(val.get<bool>());
1214 if (val.is_number_integer()) return TaskValue(val.get<int>());
1215 if (val.is_number_float()) return TaskValue(static_cast<float>(val.get<double>()));
1216 if (val.is_string()) return TaskValue(val.get<std::string>());
1217 return TaskValue(); // None
1218}
1219
1220// ============================================================================
1221
1223 const std::string& key,
1224 json& outVal)
1225{
1226 if (obj.contains(key))
1227 {
1228 outVal = obj[key];
1229 return true;
1230 }
1231 return false;
1232}
1233
1234// ============================================================================
1235
1237{
1238 int top = JsonHelper::GetInt(data, "rootNodeId", -1);
1239 if (top >= 0) return top;
1240 return JsonHelper::GetInt(dataSection, "rootNodeId", -1);
1241}
1242
1243// ============================================================================
1244// String -> enum helpers
1245// ============================================================================
1246
1248 const std::string& graphType,
1249 bool& outOk)
1250{
1251 outOk = true;
1252
1253 if (s == "EntryPoint") return TaskNodeType::EntryPoint;
1254 if (s == "Branch") return TaskNodeType::Branch;
1255 if (s == "Switch") return TaskNodeType::Switch;
1256 if (s == "While") return TaskNodeType::While;
1257 if (s == "ForEach") return TaskNodeType::ForEach;
1258 if (s == "DoOnce") return TaskNodeType::DoOnce;
1259 if (s == "Delay") return TaskNodeType::Delay;
1260 if (s == "GetBBValue") return TaskNodeType::GetBBValue;
1261 if (s == "SetBBValue") return TaskNodeType::SetBBValue;
1262 if (s == "Math" || s == "MathOp") return TaskNodeType::MathOp;
1263 if (s == "SubGraph") return TaskNodeType::SubGraph;
1264 if (s == "AtomicTask") return TaskNodeType::AtomicTask;
1265 if (s == "Action") return TaskNodeType::AtomicTask;
1266 if (s == "Condition") return TaskNodeType::AtomicTask;
1267
1268 if (s == "Sequence")
1269 {
1270 return (graphType == "VisualScript")
1273 }
1274 if (s == "VSSequence") return TaskNodeType::VSSequence;
1275 if (s == "Selector") return TaskNodeType::Selector;
1276 if (s == "Parallel") return TaskNodeType::Parallel;
1277 if (s == "Decorator") return TaskNodeType::Decorator;
1278 if (s == "Repeater") return TaskNodeType::Decorator;
1279 if (s == "Root") return TaskNodeType::Root;
1280
1281 outOk = false;
1283}
1284
1285// ============================================================================
1286
1288{
1289 if (s == "Bool") return VariableType::Bool;
1290 if (s == "Int") return VariableType::Int;
1291 if (s == "Float") return VariableType::Float;
1292 if (s == "Vector") return VariableType::Vector;
1293 if (s == "EntityID") return VariableType::EntityID;
1294 if (s == "String") return VariableType::String;
1295 if (s == "List") return VariableType::List;
1296 if (s == "GlobalRef") return VariableType::GlobalRef;
1297 return VariableType::None;
1298}
1299
1300// ============================================================================
1301
1303{
1304 if (s == "Output") return DataPinDir::Output;
1305 return DataPinDir::Input;
1306}
1307
1308// ============================================================================
1309
1311{
1312 if (s == "Out") return ExecPinRole::Out;
1313 if (s == "OutElse") return ExecPinRole::OutElse;
1314 if (s == "OutLoop") return ExecPinRole::OutLoop;
1315 if (s == "OutCompleted") return ExecPinRole::OutCompleted;
1316 if (s == "OutCase") return ExecPinRole::OutCase;
1317 return ExecPinRole::In;
1318}
1319
1320// ============================================================================
1321// Public: FileExists
1322// ============================================================================
1323
1324bool TaskGraphLoader::FileExists(const std::string& path)
1325{
1326 std::ifstream f(path);
1327 return f.good();
1328}
1329
1330// ============================================================================
1331// Public: ScanTaskGraphDirectory
1332// ============================================================================
1333
1334std::vector<std::string> TaskGraphLoader::ScanTaskGraphDirectory(const std::string& dir)
1335{
1336 std::vector<std::string> result;
1337
1338#ifdef _WIN32
1340 std::string searchPath = dir + "\\*";
1343 {
1344 SYSTEM_LOG << "[TaskGraphLoader] WARNING: Directory not found: " << dir << std::endl;
1345 return result;
1346 }
1347
1348 do
1349 {
1350 std::string name = findData.cFileName;
1351 if (name == "." || name == "..") continue;
1352
1353 std::string fullPath = dir + "/" + name;
1354 if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
1355 {
1356 std::vector<std::string> sub = ScanTaskGraphDirectory(fullPath);
1357 result.insert(result.end(), sub.begin(), sub.end());
1358 }
1359 else if (name.size() > 4 && name.substr(name.size() - 4) == ".ats")
1360 {
1361 result.push_back(fullPath);
1362 }
1363 } while (FindNextFileA(hFind, &findData));
1364
1366
1367#else
1368 DIR* d = opendir(dir.c_str());
1369 if (!d)
1370 {
1371 SYSTEM_LOG << "[TaskGraphLoader] WARNING: Directory not found: " << dir << std::endl;
1372 return result;
1373 }
1374
1375 struct dirent* entry;
1376 while ((entry = readdir(d)) != nullptr)
1377 {
1378 std::string name = entry->d_name;
1379 if (name == "." || name == "..") continue;
1380
1381 std::string fullPath = dir + "/" + name;
1382 if (entry->d_type == DT_DIR)
1383 {
1384 std::vector<std::string> sub = ScanTaskGraphDirectory(fullPath);
1385 result.insert(result.end(), sub.begin(), sub.end());
1386 }
1387 else if (name.size() > 4 && name.substr(name.size() - 4) == ".ats")
1388 {
1389 result.push_back(fullPath);
1390 }
1391 }
1392
1393 closedir(d);
1394#endif
1395
1396 std::sort(result.begin(), result.end());
1397 return result;
1398}
1399
1400} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Clean schema v4 parser for ATS Visual Script task graphs.
Migrates task graph JSON from schema v3 (BT-style) to schema v4 (VS-style).
static TaskGraphTemplate Convert(const nlohmann::json &btV2Json, std::vector< std::string > &outErrors)
Converts a BT v2 JSON document to a TaskGraphTemplate.
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 ExecPinRole StringToExecPinRole(const std::string &s)
static DataPinDir StringToDataPinDir(const std::string &s)
static TaskNodeDefinition ParseNodeV4(const json &nodeJson, const std::string &graphType, std::vector< std::string > &outErrors)
Parses a single node JSON (v4 flat format) into a TaskNodeDefinition.
static TaskNodeType StringToNodeType(const std::string &s, const std::string &graphType, bool &outOk)
static bool ValidateJson(const json &data, std::vector< std::string > &outErrors)
Validates a JSON object against the expected task graph schema.
static void ParseDataConnectionsV4(const json &root, TaskGraphTemplate *tmpl)
Parses dataConnections (v4) and fills tmpl->DataConnections.
static VariableType StringToVariableType(const std::string &s)
static TaskGraphTemplate * LoadFromFile(const std::string &path, std::vector< std::string > &outErrors)
Loads a TaskGraphTemplate from a JSON file on disk.
static std::vector< std::string > ScanTaskGraphDirectory(const std::string &dir)
Recursively scans a directory for .ats task graph files.
static bool FileExists(const std::string &path)
Returns true if the given file path exists and can be opened.
static int ResolveRootNodeId(const json &data, const json &dataSection)
static TaskGraphTemplate * ParseSchemaV4(const json &data, std::vector< std::string > &outErrors)
Parses a schema v4 flat JSON into a TaskGraphTemplate.
static bool GetChildValue(const json &obj, const std::string &key, json &outVal)
static void ParseExecConnectionsV4(const json &root, TaskGraphTemplate *tmpl)
Parses execConnections (v4) and fills tmpl->ExecConnections.
static TaskValue ParsePrimitiveValue(const json &val)
static void ParseBlackboardV4(const json &root, TaskGraphTemplate *tmpl, std::vector< std::string > &outErrors)
Parses the blackboard array (v4) and fills tmpl->Blackboard.
static void ParseParameters(const json &paramsJson, std::unordered_map< std::string, ParameterBinding > &outParams)
static TaskGraphTemplate * LoadFromJson(const json &data, std::vector< std::string > &outErrors)
Loads a TaskGraphTemplate from an already-parsed JSON object.
static json MigrateJson(const json &v3data, std::vector< std::string > &outErrors)
Performs the v3->v4 JSON transformation in memory.
Immutable, shareable task graph asset.
C++14-compliant type-safe value container for task parameters.
std::string GetString(const json &j, const std::string &key, const std::string &defaultValue="")
Safely get a string value from JSON.
bool LoadJsonFromFile(const std::string &filepath, json &j)
Load and parse a JSON file.
Definition json_helper.h:42
int GetInt(const json &j, const std::string &key, int defaultValue=0)
Safely get an integer value from JSON.
void ForEachInArray(const json &j, const std::string &key, std::function< void(const json &, size_t)> callback)
Iterate over an array with a callback function.
float GetFloat(const json &j, const std::string &key, float defaultValue=0.0f)
Safely get a float value from JSON.
bool IsArray(const json &j, const std::string &key)
Check if a key contains an array.
bool IsObject(const json &j, const std::string &key)
Check if a key contains an object.
bool GetBool(const json &j, const std::string &key, bool defaultValue=false)
Safely get a boolean value from JSON.
< Provides AssetID and INVALID_ASSET_ID
nlohmann::json json
VariableType
Type tags used by TaskValue to identify stored data.
@ Int
32-bit signed integer
@ Float
Single-precision float.
@ String
std::string
@ GlobalRef
Reference to a global blackboard key (scope "global:")
@ List
std::vector<TaskValue> (used by ForEach node)
@ Vector
3-component vector (Vector from vector.h)
@ None
Uninitialized / empty value.
@ EntityID
Entity identifier (uint64_t)
@ MathOperator
Math operator symbol (+, -, *, /, %) (from OperatorRegistry)
@ ConditionID
ID of a condition type (from ConditionRegistry)
@ AtomicTaskID
ID of an atomic task (from AtomicTaskUIRegistry)
@ SubGraphPath
File path to a sub-graph .ats file.
@ LocalVariable
Value is read from the local blackboard at runtime.
@ Literal
Value is embedded directly in the template.
@ ComparisonOp
Comparison operator (==, !=, <, <=, >, >=) (from OperatorRegistry)
TaskNodeType
Identifies the role of a node in the task graph.
@ Selector
Executes children in order; stops on first success.
@ AtomicTask
Leaf node that executes a single atomic task.
@ While
Conditional loop (Loop / Completed exec outputs)
@ Sequence
Executes children in order; stops on first failure.
@ Decorator
Wraps a single child and modifies its behaviour.
@ SubGraph
Sub-graph call (SubTask)
@ DoOnce
Single-fire execution (reset via Reset pin)
@ Delay
Timer (Completed exec output after N seconds)
@ Parallel
Executes all children simultaneously.
@ GetBBValue
Data node – reads a Blackboard key.
@ MathOp
Data node – arithmetic operation (+, -, *, /)
@ SetBBValue
Data node – writes a Blackboard key.
@ ForEach
Iterate over BB list (Loop Body / Completed exec outputs)
@ Switch
Multi-branch on value (N exec outputs)
@ EntryPoint
Unique entry node for VS graphs (replaces Root)
@ Branch
If/Else conditional (Then / Else exec outputs)
@ Root
Entry point of the graph (exactly one per template)
@ VSSequence
Execute N outputs in order ("VS" prefix avoids collision with BT Sequence=1)
constexpr int32_t NODE_INDEX_NONE
Sentinel value for "no node" in node index / ID fields.
DataPinDir
Direction of a data pin on a Visual Script node.
@ Output
Value produced by the node.
@ Input
Value consumed by the node.
ExecPinRole
Role of an exec pin on a Visual Script node.
@ OutCompleted
End-of-loop output (While, ForEach, Delay, DoOnce)
@ OutLoop
Loop body output (While, ForEach)
@ Out
Normal output / Then.
@ OutCase
Switch case output (dynamically named)
@ OutElse
Else output (Branch)
@ In
Triggers execution of the node.
static TaskValue GetDefaultValueForType(VariableType type)
Returns a correctly-typed default TaskValue for the given VariableType.
Single entry in the graph's declared blackboard schema (local or global).
A globally-stored, reusable condition expression.
static ConditionPreset FromJson(const nlohmann::json &data)
Deserializes a ConditionPreset from a JSON object.
Stores the complete reference for one condition including operand-to-DynamicDataPin mapping.
int conditionIndex
Index in the Branch/While node's conditions[] array.
Describes a single condition expression for Branch/While nodes.
std::string leftMode
"Pin" | "Variable" | "Const"
Explicit connection between an output data pin of a source node and an input data pin of a target nod...
Explicit connection between a named exec-out pin of a source node and the exec-in pin of a target nod...
static MathOpRef FromJson(const nlohmann::json &data)
Deserializes a MathOpRef from a JSON object.
One entry in a NodeBranch's conditions list.
static NodeConditionRef FromJson(const nlohmann::json &data)
Deserializes a NodeConditionRef from a JSON object.
@ Variable
References a blackboard variable by name.
@ Const
Literal constant value.
@ Pin
External data-input pin on the owning node.
Describes how a single parameter value is supplied to a task node.
ParameterBindingType Type
Binding mode.
Describes an input or output parameter declared on a SubGraph file.
std::string Name
Parameter name (must match binding keys)
Describes a single case branch on a Switch node.
std::string value
The value to match (int as decimal string or raw string)
Full description of a single node in the task graph.
int32_t NodeID
Unique ID within this template.
#define SYSTEM_LOG