Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
VisualScriptEditorPanel_FileOperations.cpp
Go to the documentation of this file.
1/**
2 * @file VisualScriptEditorPanel_FileOperations.cpp
3 * @brief File operations for VisualScriptEditorPanel (Phase 7 refactoring).
4 * @author Olympe Engine
5 * @date 2026-03-09
6 *
7 * @details Extracted methods for file I/O operations:
8 * - LoadTemplate() — Load blueprint from file/memory with preset loading (Phase 24)
9 * - Save() — Save current graph to m_currentPath
10 * - SaveAs() — Save graph to new path
11 * - SyncNodePositionsFromImNodes() — Sync grid-space positions (BUG-003 Fix)
12 * - SyncPresetsFromRegistryToTemplate() — Phase 24 preset synchronization
13 * - SerializeAndWrite() — Complete JSON v4 serialization with all Phase 24 features
14 *
15 * @note C++14 compliant — no std::optional, structured bindings, std::filesystem.
16 */
17
19#include "DebugController.h"
20#include "../system/system_utils.h"
21#include "../system/system_consts.h"
22#include "../NodeGraphCore/GlobalTemplateBlackboard.h"
23
24#include "../third_party/imgui/imgui.h"
25#include "../third_party/imnodes/imnodes.h"
26#include "../json_helper.h"
27#include "../TaskSystem/TaskGraphLoader.h"
28
29#include <fstream>
30#include <iostream>
31#include <algorithm>
32#include <cmath>
33#include <cstring>
34#include <sstream>
35#include <iomanip>
36
37namespace Olympe {
38
39// ============================================================================
40// Load / Save
41// ============================================================================
42
43void VisualScriptEditorPanel::LoadTemplate(const TaskGraphTemplate* tmpl,
44 const std::string& path)
45{
46 if (tmpl == nullptr)
47 return;
48
50 m_currentPath = path;
51 m_dirty = false;
52
53 // Rebuild lookup cache after copy (pointers from old template are now invalid)
55
56 // Phase 24 CRITICAL FIX: Sync both SubGraphPath and Parameters["subgraph_path"]
57 // After loading, both storage locations should be synchronized:
58 // 1. If SubGraphPath has a value, ensure Parameters["subgraph_path"] exists
59 // 2. If Parameters["subgraph_path"] has a value, sync to SubGraphPath
60 // This handles both old (subGraphPath field only) and new (params field) JSON formats
61 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
62 {
64 {
65 auto pathParamIt = m_template.Nodes[i].Parameters.find("subgraph_path");
66
67 // Case 1: SubGraphPath has value, Parameters["subgraph_path"] doesn't
68 if (!m_template.Nodes[i].SubGraphPath.empty() && pathParamIt == m_template.Nodes[i].Parameters.end())
69 {
70 ParameterBinding pathBinding;
72 pathBinding.LiteralValue = TaskValue(m_template.Nodes[i].SubGraphPath);
73 m_template.Nodes[i].Parameters["subgraph_path"] = pathBinding;
74 SYSTEM_LOG << "[VSEditor] LoadTemplate: initialized Parameters[subgraph_path] from SubGraphPath = '"
75 << m_template.Nodes[i].SubGraphPath << "' for node "
76 << m_template.Nodes[i].NodeID << "\n";
77 }
78 // Case 2: Parameters["subgraph_path"] exists, sync to SubGraphPath if needed
79 else if (pathParamIt != m_template.Nodes[i].Parameters.end() &&
81 {
82 std::string paramPath = pathParamIt->second.LiteralValue.to_string();
83 if (!paramPath.empty())
84 {
85 // Use parameter value as canonical
86 if (m_template.Nodes[i].SubGraphPath != paramPath)
87 {
88 m_template.Nodes[i].SubGraphPath = paramPath;
89 SYSTEM_LOG << "[VSEditor] LoadTemplate: synced SubGraphPath from Parameters[subgraph_path] = '"
90 << paramPath << "' for node "
91 << m_template.Nodes[i].NodeID << "\n";
92 }
93 }
94 else if (!m_template.Nodes[i].SubGraphPath.empty())
95 {
96 // Parameters is empty, sync SubGraphPath to it
97 pathParamIt->second.LiteralValue = TaskValue(m_template.Nodes[i].SubGraphPath);
98 SYSTEM_LOG << "[VSEditor] LoadTemplate: synced Parameters[subgraph_path] from SubGraphPath = '"
99 << m_template.Nodes[i].SubGraphPath << "' for node "
100 << m_template.Nodes[i].NodeID << "\n";
101 }
102 }
103 }
104 }
105
106 // Phase 24 — Load embedded presets from the graph
107 // This replaces the old file-based approach with graph-embedded storage
108 if (!m_template.Presets.empty())
109 {
111 SYSTEM_LOG << "[VSEditor] LoadTemplate: loaded " << m_template.Presets.size()
112 << " presets from graph '" << m_template.Name << "'\n";
113 }
114 else
115 {
116 // Clear registry if graph has no presets (fresh start)
118 SYSTEM_LOG << "[VSEditor] LoadTemplate: graph '" << m_template.Name
119 << "' has no embedded presets - starting with empty bank\n";
120 }
121
122 // Phase 24 Global Blackboard Integration: Initialize EntityBlackboard
123 // This merges local (from m_template.Blackboard) + global variables (from registry)
124
125 // Reload global variables from registry (in case they were modified outside this editor instance)
127
129 {
130 m_entityBlackboard->Initialize(m_template);
131 SYSTEM_LOG << "[VSEditor] LoadTemplate: initialized EntityBlackboard with "
132 << m_entityBlackboard->GetLocalVariableCount() << " local + "
133 << m_entityBlackboard->GetGlobalVariableCount() << " global variables\n";
134
135 // Phase 24 Global Blackboard Integration: Restore entity-specific global variable values
136 // If the graph has stored global variable overrides, restore them now
138 {
139 m_entityBlackboard->ImportGlobalsFromJson(m_template.GlobalVariableValues);
140 SYSTEM_LOG << "[VSEditor] LoadTemplate: restored global variable overrides from graph\n";
141 }
142 }
143
144 // NOTE: Do NOT clear the undo stack here. Each VisualScriptEditorPanel
145 // instance owns its own stack (one per tab), so there is no cross-tab
146 // contamination. Preserving the stack lets the user undo edits made
147 // before saving and reloading, and is required for undo to function
148 // correctly after opening a file from the Blueprint Files browser.
149
151
152 // Phase 18: Do NOT pre-populate m_nodeDragStartPositions here.
153 // The former "FIX 2" block pre-populated every node's drag-start position
154 // with its loaded position. Because the guard in the drag-tracking loop is
155 // "insert only if key is absent", the pre-populated value was never
156 // overwritten. On the first drag after load the key already existed, so no
157 // new start position was recorded — eNode.posX/Y (kept current each frame
158 // while mouseDown) serves as the correct "position before this drag" and is
159 // used when the key is absent. Keeping m_nodeDragStartPositions empty here
160 // allows the tracking loop to record the true pre-drag position.
162 m_verificationDone = false;
163}
164
166{
167 SYSTEM_LOG << "[VisualScriptEditorPanel] Save() called. m_currentPath='"
168 << m_currentPath << "'\n";
169
170 if (m_currentPath.empty())
171 {
172 SYSTEM_LOG << "[VisualScriptEditorPanel] Save() aborted: m_currentPath is empty\n";
173 return false;
174 }
175
176 // BUG-003 Fix: Reset viewport panning BEFORE syncing positions so that
177 // any residual editor-space offset from navigation is neutralised.
178 // Positions are stored in grid space (GetNodeGridSpacePos), so this is
179 // belt-and-suspenders safety; panning is restored by AfterSave().
181
182 // Fix #1: Commit any deferred key-name edits before save
184
185 // Fix #1: Remove invalid blackboard entries before save
187
188 // Phase 24: CRITICAL - Sync conditions from panel to template BEFORE serialization
189 // This ensures conditionRefs and conditionOperandRefs are up-to-date before save
190 if (m_selectedNodeID >= 0)
191 {
192 for (size_t ni = 0; ni < m_editorNodes.size(); ++ni)
193 {
194 if (m_editorNodes[ni].nodeID == m_selectedNodeID &&
196 {
197 m_editorNodes[ni].def.conditionRefs = m_conditionsPanel->GetConditionRefs();
198 m_editorNodes[ni].def.conditionOperandRefs = m_conditionsPanel->GetConditionOperandRefs();
199
200 // Also sync to template
201 for (size_t ti = 0; ti < m_template.Nodes.size(); ++ti)
202 {
203 if (m_template.Nodes[ti].NodeID == m_selectedNodeID)
204 {
205 m_template.Nodes[ti].conditionRefs = m_editorNodes[ni].def.conditionRefs;
206 m_template.Nodes[ti].conditionOperandRefs = m_editorNodes[ni].def.conditionOperandRefs;
207 break;
208 }
209 }
210 break;
211 }
212 }
213 }
214
215 // Phase 24: CRITICAL - Sync presets from registry to template BEFORE serialization
216 // This ensures all presets (newly created, modified, duplicated) are included in save
218
219 // Phase 24 Global Blackboard Integration: Sync global variable values from EntityBlackboard to template
220 // This ensures entity-specific global variable overrides are included in save
222 {
223 m_template.GlobalVariableValues = m_entityBlackboard->ExportGlobalsToJson();
224 }
225
226 // CRITICAL FIX: Sync node positions from ImNodes BEFORE serialization.
227 // RenderToolbar() (which calls Save) executes before RenderCanvas() syncs
228 // positions, so we must pull fresh positions here to avoid stale data.
230
232
233 // BUG-003 Fix #5: Restore viewport so the canvas does not visually jump.
234 AfterSave();
235
236 SYSTEM_LOG << "[VisualScriptEditorPanel] Save() "
237 << (ok ? "succeeded" : "FAILED") << ": '" << m_currentPath << "'\n";
238 return ok;
239}
240
241bool VisualScriptEditorPanel::SaveAs(const std::string& path)
242{
243 SYSTEM_LOG << "[VisualScriptEditorPanel] SaveAs() called. path='" << path << "'\n";
244
245 if (path.empty())
246 return false;
247
248 // BUG-003 Fix: Reset viewport before position sync (same as Save()).
250
251 // Fix #1: Commit and validate before save
254
255 // Phase 24: CRITICAL - Sync presets from registry to template BEFORE serialization
256 // This ensures all presets (newly created, modified, duplicated) are included in save
258
259 // Phase 24 Global Blackboard Integration: Sync global variable values from EntityBlackboard to template
260 // This ensures entity-specific global variable overrides are included in save
262 {
263 m_template.GlobalVariableValues = m_entityBlackboard->ExportGlobalsToJson();
264 }
265
266 // CRITICAL FIX: Same position sync as Save() — ensure fresh positions
267 // before serialization regardless of when in the frame SaveAs is called.
269
270 // Phase 24 CRITICAL FIX: Sync SubGraph paths bidirectionally before save
271 // This ensures that whether the path is in SubGraphPath OR Parameters["subgraph_path"],
272 // both will be synchronized before serialization.
273 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
274 {
276 {
277 auto pathIt = m_template.Nodes[i].Parameters.find("subgraph_path");
278
279 // Case 1: Parameters["subgraph_path"] exists with value → sync to SubGraphPath
280 if (pathIt != m_template.Nodes[i].Parameters.end() &&
282 {
283 std::string paramPath = pathIt->second.LiteralValue.to_string();
284 if (!paramPath.empty())
285 {
286 m_template.Nodes[i].SubGraphPath = paramPath;
287 SYSTEM_LOG << "[VisualScriptEditorPanel::SaveAs] Synced SubGraphPath from Parameters[subgraph_path] = '"
288 << paramPath << "' for node " << m_template.Nodes[i].NodeID << "\n";
289 }
290 }
291
292 // Case 2: SubGraphPath has value but Parameters["subgraph_path"] doesn't → create it
293 if (!m_template.Nodes[i].SubGraphPath.empty() &&
294 (pathIt == m_template.Nodes[i].Parameters.end() ||
295 (pathIt->second.Type == ParameterBindingType::Literal &&
296 pathIt->second.LiteralValue.to_string().empty())))
297 {
298 ParameterBinding pathBinding;
300 pathBinding.LiteralValue = TaskValue(m_template.Nodes[i].SubGraphPath);
301 m_template.Nodes[i].Parameters["subgraph_path"] = pathBinding;
302 SYSTEM_LOG << "[VisualScriptEditorPanel::SaveAs] Created Parameters[subgraph_path] = '"
303 << m_template.Nodes[i].SubGraphPath << "' for node "
304 << m_template.Nodes[i].NodeID << "\n";
305 }
306 }
307 }
308
309 bool ok = SerializeAndWrite(path);
310
311 // BUG-003 Fix #5: Restore viewport.
312 AfterSave();
313
314 if (ok)
315 {
316 m_currentPath = path;
317 m_dirty = false;
318 SYSTEM_LOG << "[VisualScriptEditorPanel] SaveAs() succeeded: '" << path << "'\n";
319 }
320 else
321 {
322 SYSTEM_LOG << "[VisualScriptEditorPanel] SaveAs() FAILED: '" << path << "'\n";
323 }
324 return ok;
325}
326
328{
329 for (size_t i = 0; i < m_editorNodes.size(); ++i)
330 {
331 VSEditorNode& eNode = m_editorNodes[i];
332 // Only query nodes that have been rendered at least once to avoid
333 // an ImNodes assertion for nodes that have not yet gone through
334 // BeginNode()/EndNode() this session.
335 if (m_positionedNodes.count(eNode.nodeID) > 0)
336 {
337 // BUG-003 Fix: use GetNodeGridSpacePos() (pan-independent grid
338 // coordinates) instead of GetNodeEditorSpacePos() which returns
339 // Origin + Panning. Storing grid-space positions means the saved
340 // values are never corrupted by the current viewport pan offset.
341 ImVec2 pos = ImNodes::GetNodeGridSpacePos(eNode.nodeID);
342 eNode.posX = pos.x;
343 eNode.posY = pos.y;
344
345 // Keep the template's Parameters in sync so that
346 // SyncEditorNodesFromTemplate() (called on undo/redo) can always
347 // find the live canvas position in Parameters["__posX/__posY"],
348 // even for nodes that were loaded from file and have never been
349 // moved via an explicit MoveNodeCommand.
350 for (size_t j = 0; j < m_template.Nodes.size(); ++j)
351 {
352 if (m_template.Nodes[j].NodeID == eNode.nodeID)
353 {
354 ParameterBinding bx, by;
356 bx.LiteralValue = TaskValue(pos.x);
358 by.LiteralValue = TaskValue(pos.y);
359 m_template.Nodes[j].Parameters["__posX"] = bx;
360 m_template.Nodes[j].Parameters["__posY"] = by;
361 break;
362 }
363 }
364 }
365 }
366}
367
369{
370 // Phase 24 FIX: Sync ALL presets from the registry to the template
371 // This ensures that presets created/modified via UI are included in the save
372 // Previously, only modified presets were synced, missing newly created ones
373
374 // Get all presets from the registry
375 std::vector<std::string> allPresetIDs = m_presetRegistry.GetAllPresetIDs();
376
377 // Clear template presets and rebuild from registry
378 m_template.Presets.clear();
379
380 for (const auto& presetID : allPresetIDs)
381 {
382 const ConditionPreset* preset = m_presetRegistry.GetPreset(presetID);
383 if (preset)
384 {
385 m_template.Presets.push_back(*preset);
386 }
387 }
388
389 SYSTEM_LOG << "[VisualScriptEditorPanel] SyncPresetsFromRegistryToTemplate: synced "
390 << m_template.Presets.size() << " presets from registry to template\n";
391}
392
393bool VisualScriptEditorPanel::SerializeAndWrite(const std::string& path)
394{
395 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: writing to '" << path << "'\n";
396
398
399 json root;
400 root["schema_version"] = 4;
401 root["name"] = m_template.Name;
402 root["graphType"] = "VisualScript";
403
404 // Blackboard
405 // BUG-001 Hotfix: skip invalid entries (empty key or VariableType::None)
406 // to prevent save crash caused by unhandled None type during serialization.
407 int bbSkipped = 0;
408 json bbArray = json::array();
409 for (size_t i = 0; i < m_template.Blackboard.size(); ++i)
410 {
411 const BlackboardEntry& entry = m_template.Blackboard[i];
412
413 if (entry.Key.empty() || entry.Type == VariableType::None)
414 {
415 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: skipping invalid blackboard entry"
416 << " (key='" << entry.Key << "', type=None)\n";
417 ++bbSkipped;
418 continue;
419 }
420
421 json e;
422 e["key"] = entry.Key;
423 e["isGlobal"] = entry.IsGlobal;
424
425 // Guard each accessor against type mismatch: if Default was not
426 // initialised to the right type (e.g. loaded as Int when Float was
427 // expected), fall back to a zero-value rather than throwing.
428 switch (entry.Type)
429 {
431 e["type"] = "Bool";
432 e["value"] = (entry.Default.GetType() == VariableType::Bool)
433 ? entry.Default.AsBool() : false;
434 break;
436 e["type"] = "Int";
437 e["value"] = (entry.Default.GetType() == VariableType::Int)
438 ? entry.Default.AsInt() : 0;
439 break;
441 e["type"] = "Float";
442 e["value"] = (entry.Default.GetType() == VariableType::Float)
443 ? entry.Default.AsFloat() : 0.0f;
444 break;
446 e["type"] = "String";
447 e["value"] = (entry.Default.GetType() == VariableType::String)
448 ? entry.Default.AsString() : std::string("");
449 break;
451 e["type"] = "EntityID";
452 e["value"] = std::to_string(
453 (entry.Default.GetType() == VariableType::EntityID)
454 ? entry.Default.AsEntityID() : 0);
455 break;
457 {
458 // Vector default is auto-assigned at runtime from entity position.
459 // Persist as a zero-initialised object so the type tag is preserved
460 // across save/load and does not degrade to "None".
461 const ::Vector v = (entry.Default.GetType() == VariableType::Vector)
462 ? entry.Default.AsVector()
463 : ::Vector{0.f, 0.f, 0.f};
464 json vec;
465 vec["x"] = v.x;
466 vec["y"] = v.y;
467 vec["z"] = v.z;
468 e["type"] = "Vector";
469 e["value"] = vec;
470 break;
471 }
472 default:
473 e["type"] = "None";
474 e["value"] = nullptr;
475 break;
476 }
477 bbArray.push_back(e);
478 }
479 if (bbSkipped > 0)
480 {
481 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: " << bbSkipped
482 << " invalid blackboard entries skipped (BUG-001)\n";
483 }
484 root["blackboard"] = bbArray;
485
486 // Nodes
487 json nodesArray = json::array();
488 for (size_t i = 0; i < m_template.Nodes.size(); ++i)
489 {
490 const TaskNodeDefinition& def = m_template.Nodes[i];
491 json n;
492 n["id"] = def.NodeID;
493 n["label"] = def.NodeName;
494 n["type"] = GetNodeTypeLabel(def.Type);
495
496 if (def.Type == TaskNodeType::AtomicTask)
497 n["taskType"] = def.AtomicTaskID;
498 if (def.Type == TaskNodeType::Delay)
499 n["delaySeconds"] = def.DelaySeconds;
500 if (!def.BBKey.empty())
501 n["bbKey"] = def.BBKey;
502 if (!def.SubGraphPath.empty())
503 n["subGraphPath"] = def.SubGraphPath;
504 if (!def.ConditionID.empty())
505 n["conditionKey"] = def.ConditionID;
506 if (!def.MathOperator.empty())
507 n["mathOp"] = def.MathOperator;
508
509 // Serialize parameters (AtomicTask and other node types with parameters)
510 if (!def.Parameters.empty())
511 {
512 json paramsObj = json::object();
513 for (const auto& paramPair : def.Parameters)
514 {
515 const std::string& paramName = paramPair.first;
516 const ParameterBinding& binding = paramPair.second;
517
518 json bindingObj = json::object();
519
520 // Phase 24 Debug: Log SubGraph path serialization
521 if (def.Type == TaskNodeType::SubGraph && paramName == "subgraph_path")
522 {
523 SYSTEM_LOG << "[SerializeAndWrite] Node " << def.NodeID << " param '" << paramName
524 << "': Type=" << static_cast<int>(binding.Type)
525 << " Value='" << binding.LiteralValue.to_string() << "'\n";
526 }
527
528 switch (binding.Type)
529 {
531 bindingObj["Type"] = "Literal";
532 // Serialize the literal value based on its type
533 if (!binding.LiteralValue.IsNone())
534 {
535 switch (binding.LiteralValue.GetType())
536 {
538 bindingObj["LiteralValue"] = binding.LiteralValue.AsBool();
539 break;
541 bindingObj["LiteralValue"] = binding.LiteralValue.AsInt();
542 break;
544 bindingObj["LiteralValue"] = binding.LiteralValue.AsFloat();
545 break;
547 bindingObj["LiteralValue"] = binding.LiteralValue.AsString();
548 break;
550 {
551 const ::Vector v = binding.LiteralValue.AsVector();
552 json vec;
553 vec["x"] = v.x;
554 vec["y"] = v.y;
555 vec["z"] = v.z;
556 bindingObj["LiteralValue"] = vec;
557 break;
558 }
560 bindingObj["LiteralValue"] = std::to_string(binding.LiteralValue.AsEntityID());
561 break;
562 default:
563 break;
564 }
565 }
566 break;
567
569 bindingObj["Type"] = "LocalVariable";
570 bindingObj["VariableName"] = binding.VariableName;
571 break;
572
574 bindingObj["Type"] = "AtomicTaskID";
575 bindingObj["value"] = binding.VariableName;
576 break;
577
579 bindingObj["Type"] = "ConditionID";
580 bindingObj["value"] = binding.VariableName;
581 break;
582
584 bindingObj["Type"] = "MathOperator";
585 bindingObj["value"] = binding.VariableName;
586 break;
587
589 bindingObj["Type"] = "ComparisonOp";
590 bindingObj["value"] = binding.VariableName;
591 break;
592
594 bindingObj["Type"] = "SubGraphPath";
595 bindingObj["value"] = binding.VariableName;
596 break;
597
598 default:
599 bindingObj["Type"] = "Literal";
600 break;
601 }
602
604 }
605 n["params"] = paramsObj;
606 }
607
608 // Switch enhancements (Phase 22-A)
609 if (def.Type == TaskNodeType::Switch)
610 {
611 if (!def.switchVariable.empty())
612 n["switchVariable"] = def.switchVariable;
613
614 if (!def.switchCases.empty())
615 {
616 json casesArray = json::array();
617 for (size_t c = 0; c < def.switchCases.size(); ++c)
618 {
619 const SwitchCaseDefinition& sc = def.switchCases[c];
621 caseObj["value"] = sc.value;
622 caseObj["pin"] = sc.pinName;
623 if (!sc.customLabel.empty())
624 caseObj["label"] = sc.customLabel;
625 casesArray.push_back(caseObj);
626 }
627 n["switchCases"] = casesArray;
628 }
629 }
630
631 // Phase 24 Milestone 2 — MathOp operand serialization
632 // Serialize the complete MathOpRef (left operand, operator, right operand)
633 if (def.Type == TaskNodeType::MathOp && !def.mathOpRef.mathOperator.empty())
634 {
635 n["mathOpRef"] = def.mathOpRef.ToJson();
636 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: serialized mathOpRef for MathOp node "
637 << def.NodeID << "\n";
638 }
639
640 // Structured conditions (Phase 23-B.4 — Branch/While)
641 if ((def.Type == TaskNodeType::Branch || def.Type == TaskNodeType::While) &&
642 !def.conditions.empty())
643 {
644 json condArray = json::array();
645 for (size_t ci = 0; ci < def.conditions.size(); ++ci)
646 {
647 const Condition& cond = def.conditions[ci];
648 json cj;
649
650 // Left side
651 cj["leftMode"] = cond.leftMode;
652 if (!cond.leftPin.empty())
653 cj["leftPin"] = cond.leftPin;
654 if (!cond.leftVariable.empty())
655 cj["leftVariable"] = cond.leftVariable;
656 if (cond.leftMode == "Const" && !cond.leftConstValue.IsNone())
657 {
658 const TaskValue& lv = cond.leftConstValue;
659 switch (lv.GetType()) {
660 case VariableType::Bool: cj["leftConstValue"] = lv.AsBool(); break;
661 case VariableType::Int: cj["leftConstValue"] = lv.AsInt(); break;
662 case VariableType::Float: cj["leftConstValue"] = lv.AsFloat(); break;
663 case VariableType::String: cj["leftConstValue"] = lv.AsString();break;
664 default: break;
665 }
666 }
667
668 // Operator
669 cj["operator"] = cond.operatorStr;
670
671 // Right side
672 cj["rightMode"] = cond.rightMode;
673 if (!cond.rightPin.empty())
674 cj["rightPin"] = cond.rightPin;
675 if (!cond.rightVariable.empty())
676 cj["rightVariable"] = cond.rightVariable;
677 if (cond.rightMode == "Const" && !cond.rightConstValue.IsNone())
678 {
679 const TaskValue& rv = cond.rightConstValue;
680 switch (rv.GetType()) {
681 case VariableType::Bool: cj["rightConstValue"] = rv.AsBool(); break;
682 case VariableType::Int: cj["rightConstValue"] = rv.AsInt(); break;
683 case VariableType::Float: cj["rightConstValue"] = rv.AsFloat(); break;
684 case VariableType::String: cj["rightConstValue"] = rv.AsString();break;
685 default: break;
686 }
687 }
688
689 // Type hint
690 if (cond.compareType != VariableType::None)
691 cj["compareType"] = VariableTypeToString(cond.compareType);
692
693 condArray.push_back(cj);
694 }
695 n["conditions"] = condArray;
696 }
697
698 // Phase 24 Milestone 2.2 — conditionRefs serialization (new inline system)
699 // Saves OperandRef data including dynamicPinID for Pin-mode operands.
700 // Coexists with legacy def.conditions[] during transition.
701 if ((def.Type == TaskNodeType::Branch || def.Type == TaskNodeType::While) &&
702 !def.conditionOperandRefs.empty())
703 {
704 json condRefsArray = json::array();
705
706 for (size_t i = 0; i < def.conditionOperandRefs.size(); ++i)
707 {
708 const ConditionRef& ref = def.conditionOperandRefs[i];
709 json refObj;
710 refObj["conditionIndex"] = static_cast<int>(i);
711
712 // Left operand
713 {
714 json lj;
715 switch (ref.leftOperand.mode)
716 {
718 lj["mode"] = "Variable";
719 lj["variableName"] = ref.leftOperand.variableName;
720 break;
722 lj["mode"] = "Const";
723 lj["constValue"] = ref.leftOperand.constValue;
724 break;
726 lj["mode"] = "Pin";
727 lj["dynamicPinID"] = ref.leftOperand.dynamicPinID;
728 break;
729 default:
730 lj["mode"] = "Const";
731 break;
732 }
733 refObj["leftOperand"] = lj;
734 }
735
736 refObj["operator"] = ref.operatorStr;
737
738 // Right operand
739 {
740 json rj;
741 switch (ref.rightOperand.mode)
742 {
744 rj["mode"] = "Variable";
745 rj["variableName"] = ref.rightOperand.variableName;
746 break;
748 rj["mode"] = "Const";
749 rj["constValue"] = ref.rightOperand.constValue;
750 break;
752 rj["mode"] = "Pin";
753 rj["dynamicPinID"] = ref.rightOperand.dynamicPinID;
754 break;
755 default:
756 rj["mode"] = "Const";
757 break;
758 }
759 refObj["rightOperand"] = rj;
760 }
761
762 if (ref.compareType != VariableType::None)
763 refObj["compareType"] = VariableTypeToString(ref.compareType);
764
765 condRefsArray.push_back(refObj);
766 }
767
768 n["conditionRefs"] = condRefsArray;
769
770 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: Phase 24: serialized "
771 << def.conditionOperandRefs.size() << " conditionRefs for node "
772 << def.NodeID << "\n";
773 }
774
775 // Phase 24 Milestone 2.3 — Node condition references (preset IDs + logical operators)
776 // Save which presets are used and their logical operator chain
777 if ((def.Type == TaskNodeType::Branch || def.Type == TaskNodeType::While) &&
778 !def.conditionRefs.empty())
779 {
780 json nodeCondRefsArray = json::array();
781 for (const auto& ncref : def.conditionRefs)
782 {
783 json nobj = ncref.ToJson();
784 nodeCondRefsArray.push_back(nobj);
785 }
786 n["nodeConditionRefs"] = nodeCondRefsArray;
787
788 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: Phase 24: serialized "
789 << def.conditionRefs.size() << " nodeConditionRefs for node "
790 << def.NodeID << "\n";
791 }
792
793 // Dynamic exec-out pins (VSSequence and Switch)
794 if ((def.Type == TaskNodeType::VSSequence || def.Type == TaskNodeType::Switch) &&
795 !def.DynamicExecOutputPins.empty())
796 {
797 json dynPins = json::array();
798 for (size_t p = 0; p < def.DynamicExecOutputPins.size(); ++p)
799 dynPins.push_back(def.DynamicExecOutputPins[p]);
800 n["dynamicExecPins"] = dynPins;
801 }
802
803 // SubGraph input and output parameters (Phase 3)
804 if (def.Type == TaskNodeType::SubGraph)
805 {
806 // Input parameters: map of name -> ParameterBinding
807 if (!def.InputParams.empty())
808 {
809 json inputParamsObj = json::object();
810 for (const auto& paramPair : def.InputParams)
811 {
812 const std::string& paramName = paramPair.first;
813 const ParameterBinding& binding = paramPair.second;
814
815 json bindingObj = json::object();
816
817 switch (binding.Type)
818 {
820 bindingObj["Type"] = "Literal";
821 if (!binding.LiteralValue.IsNone())
822 {
823 switch (binding.LiteralValue.GetType())
824 {
826 bindingObj["LiteralValue"] = binding.LiteralValue.AsBool();
827 break;
829 bindingObj["LiteralValue"] = binding.LiteralValue.AsInt();
830 break;
832 bindingObj["LiteralValue"] = binding.LiteralValue.AsFloat();
833 break;
835 bindingObj["LiteralValue"] = binding.LiteralValue.AsString();
836 break;
838 {
839 const ::Vector v = binding.LiteralValue.AsVector();
840 json vec;
841 vec["x"] = v.x;
842 vec["y"] = v.y;
843 vec["z"] = v.z;
844 bindingObj["LiteralValue"] = vec;
845 break;
846 }
848 bindingObj["LiteralValue"] = std::to_string(binding.LiteralValue.AsEntityID());
849 break;
850 default:
851 break;
852 }
853 }
854 break;
855
857 bindingObj["Type"] = "LocalVariable";
858 bindingObj["VariableName"] = binding.VariableName;
859 break;
860
862 bindingObj["Type"] = "AtomicTaskID";
863 bindingObj["value"] = binding.VariableName;
864 break;
865
867 bindingObj["Type"] = "ConditionID";
868 bindingObj["value"] = binding.VariableName;
869 break;
870
872 bindingObj["Type"] = "MathOperator";
873 bindingObj["value"] = binding.VariableName;
874 break;
875
877 bindingObj["Type"] = "ComparisonOp";
878 bindingObj["value"] = binding.VariableName;
879 break;
880
882 bindingObj["Type"] = "SubGraphPath";
883 bindingObj["value"] = binding.VariableName;
884 break;
885
886 default:
887 bindingObj["Type"] = "Literal";
888 break;
889 }
890
892 }
893 n["InputParams"] = inputParamsObj;
894 }
895
896 // Output parameters: map of name -> blackboard key
897 if (!def.OutputParams.empty())
898 {
899 json outputParamsObj = json::object();
900 for (const auto& paramPair : def.OutputParams)
901 {
902 outputParamsObj[paramPair.first] = paramPair.second;
903 }
904 n["OutputParams"] = outputParamsObj;
905 }
906 }
907
908 // Position from editor node
909 for (size_t j = 0; j < m_editorNodes.size(); ++j)
910 {
911 if (m_editorNodes[j].nodeID == def.NodeID)
912 {
913 json pos;
914 pos["x"] = m_editorNodes[j].posX;
915 pos["y"] = m_editorNodes[j].posY;
916 n["position"] = pos;
917 break;
918 }
919 }
920
921 nodesArray.push_back(n);
922 }
923 root["nodes"] = nodesArray;
924
925 // Exec connections
926 json execArray = json::array();
927 for (size_t i = 0; i < m_template.ExecConnections.size(); ++i)
928 {
929 const ExecPinConnection& conn = m_template.ExecConnections[i];
930 json c;
931 c["fromNode"] = conn.SourceNodeID;
932 c["fromPin"] = conn.SourcePinName;
933 c["toNode"] = conn.TargetNodeID;
934 c["toPin"] = conn.TargetPinName;
935 execArray.push_back(c);
936 }
937 root["execConnections"] = execArray;
938
939 // Data connections
940 json dataArray = json::array();
941 for (size_t i = 0; i < m_template.DataConnections.size(); ++i)
942 {
943 const DataPinConnection& conn = m_template.DataConnections[i];
944 json c;
945 c["fromNode"] = conn.SourceNodeID;
946 c["fromPin"] = conn.SourcePinName;
947 c["toNode"] = conn.TargetNodeID;
948 c["toPin"] = conn.TargetPinName;
949 dataArray.push_back(c);
950 }
951 root["dataConnections"] = dataArray;
952
953 // Phase 24 Global Blackboard Integration: Serialize global variable values
954 // These are entity-specific values stored in the template before serialization
956 {
957 root["globalVariableValues"] = m_template.GlobalVariableValues;
958 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: Phase 24 - serialized "
959 << "global variable values\n";
960 }
961
962 // Phase 24 — Condition Preset Bank (embedded in graph JSON)
963 // Presets are now serialized as part of the graph, making blueprints self-contained.
964 if (!m_template.Presets.empty())
965 {
966 json presetsArray = json::array();
967 for (size_t i = 0; i < m_template.Presets.size(); ++i)
968 {
969 const ConditionPreset& preset = m_template.Presets[i];
970 json presetObj = preset.ToJson(); // Delegate serialization to preset's own method
971 presetsArray.push_back(presetObj);
972 }
973 root["presets"] = presetsArray;
974
975 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: Phase 24 - serialized "
976 << m_template.Presets.size() << " embedded presets\n";
977 }
978
979 // Write file
980 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite: opening '"
981 << path << "' for writing\n";
982 std::ofstream ofs(path);
983 if (!ofs.is_open())
984 {
985 std::cerr << "[VisualScriptEditorPanel] Cannot open file for write: " << path << std::endl;
986 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite FAILED: cannot open '"
987 << path << "'\n";
988 return false;
989 }
990 ofs << root.dump(2);
991 ofs.close();
992 m_dirty = false;
993 SYSTEM_LOG << "[VisualScriptEditorPanel] SerializeAndWrite succeeded: '" << path << "'\n";
994 return true;
995}
996
997} // namespace Olympe
nlohmann::json json
Runtime debug controller for ATS Visual Scripting (Phase 5).
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
ImNodes-based graph editor for ATS Visual Script graphs (Phase 5).
void LoadFromPresetList(const std::vector< ConditionPreset > &presets)
Loads presets from a vector of ConditionPreset objects (clears existing data first).
void Clear()
Clears all presets and resets error state.
std::vector< std::string > GetAllPresetIDs() const
Returns all preset UUIDs in display order.
ConditionPreset * GetPreset(const std::string &id)
Returns a mutable pointer to the preset, or nullptr if not found.
static void Reload()
Force reload of the registry from file (useful for hot reload)
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")
std::vector< BlackboardEntry > Blackboard
Local blackboard declared in this graph.
void BuildLookupCache()
Rebuilds the internal ID-to-node lookup map from the Nodes vector.
json GlobalVariableValues
Stores JSON representation of global variable values for this specific graph instance.
std::vector< DataPinConnection > DataConnections
Explicit data connections (ATS VS only)
std::vector< ConditionPreset > Presets
Presets are now stored in the graph JSON, not in external files.
bool Save()
Saves the current canvas state to JSON v4 at the loaded path.
TaskGraphTemplate m_template
The template currently being edited.
void CommitPendingBlackboardEdits()
Commits any pending key-name edits stored in m_pendingBlackboardEdits.
std::unique_ptr< EntityBlackboard > m_entityBlackboard
Per-entity blackboard instance (combines local + global variables) Created in Initialize() and manage...
std::unique_ptr< NodeConditionsPanel > m_conditionsPanel
Properties-panel sub-widget for the selected Branch node.
void SyncCanvasFromTemplate()
Builds the editor canvas from the in-memory TaskGraphTemplate.
void SyncTemplateFromCanvas()
Builds the in-memory TaskGraphTemplate from the editor nodes/links.
void AfterSave()
Restores the ImNodes canvas panning saved by ResetViewportBeforeSave().
void ValidateAndCleanBlackboardEntries()
Removes blackboard entries with empty keys or VariableType::None.
void SyncPresetsFromRegistryToTemplate()
Syncs ALL presets from the registry to the template.
void ResetViewportBeforeSave()
Resets the ImNodes canvas panning to (0,0) before saving node positions.
std::unordered_set< int > m_positionedNodes
Nodes for which ImNodes has been given a position.
bool SaveAs(const std::string &path)
Saves the current canvas state to a new JSON v4 file.
void SyncNodePositionsFromImNodes()
Pulls the current node positions from ImNodes into m_editorNodes.
bool SerializeAndWrite(const std::string &path)
Serializes the template to JSON v4 and writes to a file.
std::unordered_map< int, std::pair< float, float > > m_nodeDragStartPositions
Per-node drag-start positions used to record a single MoveNodeCommand per drag gesture instead of one...
bool m_verificationDone
True once RunVerification() has been called at least once for the current graph.
std::vector< VSEditorNode > m_editorNodes
Editor nodes (mirrors m_template.Nodes + position/selection state)
int m_selectedNodeID
Currently selected node (for properties panel)
ConditionPresetRegistry m_presetRegistry
Global registry of ConditionPreset objects.
void LoadTemplate(const TaskGraphTemplate *tmpl, const std::string &path)
Loads a VS graph template into the editor canvas.
float x
Definition vector.h:25
@ Condition
Boolean checks (HasTarget, InRange, etc.)
< Provides AssetID and INVALID_ASSET_ID
const char * GetNodeTypeLabel(TaskNodeType type)
Returns a human-readable label for a TaskNodeType.
@ Int
32-bit signed integer
@ Float
Single-precision float.
@ String
std::string
@ 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)
@ AtomicTask
Leaf node that executes a single atomic task.
@ While
Conditional loop (Loop / Completed exec outputs)
@ SubGraph
Sub-graph call (SubTask)
@ Delay
Timer (Completed exec output after N seconds)
@ MathOp
Data node – arithmetic operation (+, -, *, /)
@ Switch
Multi-branch on value (N exec outputs)
@ Branch
If/Else conditional (Then / Else exec outputs)
@ VSSequence
Execute N outputs in order ("VS" prefix avoids collision with BT Sequence=1)
static std::string VariableTypeToString(VariableType type)
Converts a VariableType to its canonical string representation.
@ Variable
References a blackboard variable by name.
@ Const
Literal constant value.
@ Pin
External data-input pin on the owning node.
#define SYSTEM_LOG