Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
BTNodeGraphManager.cpp
Go to the documentation of this file.
1/*
2 * Olympe Blueprint Editor - Node Graph Manager Implementation
3 */
4
8#include "SubgraphMigrator.h"
9#include "../json_helper.h"
10#include <fstream>
11#include <iostream>
12#include <algorithm>
13#include <string>
14#include <queue>
15#include <set>
16#include <map>
17#include <chrono>
18#include <sstream>
19#include <iomanip>
20
22
23namespace Olympe
24{
25 // ========== NodeGraph Implementation ==========
26
27 NodeGraph::NodeGraph()
28 : name("Untitled Graph")
29 , type("BehaviorTree")
30 , rootNodeId(-1)
31 , m_NextNodeId(1)
32 , m_commandHistory(new CommandHistory())
33 {
34 }
35
36 NodeGraph::NodeGraph(const NodeGraph& other)
37 : name(other.name)
38 , type(other.type)
39 , rootNodeId(other.rootNodeId)
40 , editorMetadata(other.editorMetadata)
41 , m_Nodes(other.m_Nodes)
42 , m_NextNodeId(other.m_NextNodeId)
43 , m_IsDirty(other.m_IsDirty)
44 , m_Filepath(other.m_Filepath)
45 , m_eventRootIds(other.m_eventRootIds)
46 , m_commandHistory(new CommandHistory()) // New empty history
47 {
48 }
49
50 NodeGraph& NodeGraph::operator=(const NodeGraph& other)
51 {
52 if (this != &other)
53 {
54 name = other.name;
55 type = other.type;
56 rootNodeId = other.rootNodeId;
57 editorMetadata = other.editorMetadata;
58 m_Nodes = other.m_Nodes;
59 m_NextNodeId = other.m_NextNodeId;
60 m_IsDirty = other.m_IsDirty;
61 m_Filepath = other.m_Filepath;
62 m_eventRootIds = other.m_eventRootIds;
63 // Reset command history
64 m_commandHistory = std::unique_ptr<CommandHistory>(new CommandHistory());
65 }
66 return *this;
67 }
68
69 NodeGraph::NodeGraph(NodeGraph&& other) noexcept
70 : name(std::move(other.name))
71 , type(std::move(other.type))
72 , rootNodeId(other.rootNodeId)
73 , editorMetadata(std::move(other.editorMetadata))
74 , m_Nodes(std::move(other.m_Nodes))
75 , m_NextNodeId(other.m_NextNodeId)
76 , m_IsDirty(other.m_IsDirty)
77 , m_Filepath(std::move(other.m_Filepath))
78 , m_eventRootIds(std::move(other.m_eventRootIds))
79 , m_commandHistory(std::move(other.m_commandHistory))
80 {
81 }
82
83 NodeGraph& NodeGraph::operator=(NodeGraph&& other) noexcept
84 {
85 if (this != &other)
86 {
87 name = std::move(other.name);
88 type = std::move(other.type);
89 rootNodeId = other.rootNodeId;
90 editorMetadata = std::move(other.editorMetadata);
91 m_Nodes = std::move(other.m_Nodes);
92 m_NextNodeId = other.m_NextNodeId;
93 m_IsDirty = other.m_IsDirty;
94 m_Filepath = std::move(other.m_Filepath);
95 m_commandHistory = std::move(other.m_commandHistory);
96 }
97 return *this;
98 }
99
100 int NodeGraph::CreateNode(NodeType nodeType, float x, float y, const std::string& nodeName)
101 {
103 node.id = m_NextNodeId++;
104 node.type = nodeType;
105 node.name = nodeName.empty() ? NodeTypeToString(nodeType) : nodeName;
106 node.posX = x;
107 node.posY = y;
108
109 m_Nodes.push_back(node);
110
111 MarkDirty(); // Mark graph as modified
112
113 std::cout << "[NodeGraph] Created node " << node.id << " (" << node.name << ")\n";
114 return node.id;
115 }
116
117 bool NodeGraph::DeleteNode(int nodeId)
118 {
119 int index = FindNodeIndex(nodeId);
120 if (index < 0)
121 return false;
122
123 // Remove node
124 m_Nodes.erase(m_Nodes.begin() + index);
125
126 // Clean up references to this node
127 for (auto& node : m_Nodes)
128 {
129 // Remove from child lists
130 auto it = std::find(node.childIds.begin(), node.childIds.end(), nodeId);
131 if (it != node.childIds.end())
132 node.childIds.erase(it);
133
134 // Clear decorator child if it matches
135 if (node.decoratorChildId == nodeId)
136 node.decoratorChildId = -1;
137 }
138
139 MarkDirty(); // Mark graph as modified
140
141 std::cout << "[NodeGraph] Deleted node " << nodeId << "\n";
142 return true;
143 }
144
145 GraphNode* NodeGraph::GetNode(int nodeId)
146 {
147 int index = FindNodeIndex(nodeId);
148 if (index < 0)
149 return nullptr;
150 return &m_Nodes[index];
151 }
152
153 const GraphNode* NodeGraph::GetNode(int nodeId) const
154 {
155 int index = FindNodeIndex(nodeId);
156 if (index < 0)
157 return nullptr;
158 return &m_Nodes[index];
159 }
160
161 std::vector<GraphNode*> NodeGraph::GetAllNodes()
162 {
163 std::vector<GraphNode*> result;
164 for (auto& node : m_Nodes)
165 result.push_back(&node);
166 return result;
167 }
168
169 std::vector<const GraphNode*> NodeGraph::GetAllNodes() const
170 {
171 std::vector<const GraphNode*> result;
172 for (const auto& node : m_Nodes)
173 result.push_back(&node);
174 return result;
175 }
176
177 bool NodeGraph::LinkNodes(int parentId, int childId)
178 {
179 GraphNode* parent = GetNode(parentId);
180 if (!parent)
181 return false;
182
183 // Check if already linked
184 if (std::find(parent->childIds.begin(), parent->childIds.end(), childId) != parent->childIds.end())
185 return false;
186
187 // For decorator nodes, use decoratorChildId instead
188 if (parent->type == NodeType::BT_Decorator)
189 {
190 parent->decoratorChildId = childId;
191 }
192 else
193 {
194 parent->childIds.push_back(childId);
195 }
196
197 MarkDirty(); // Mark graph as modified
198
199 std::cout << "[NodeGraph] Linked node " << parentId << " -> " << childId << "\n";
200 return true;
201 }
202
203 bool NodeGraph::UnlinkNodes(int parentId, int childId)
204 {
205 GraphNode* parent = GetNode(parentId);
206 if (!parent)
207 return false;
208
209 bool unlinked = false;
210
211 // Remove from child list
212 auto it = std::find(parent->childIds.begin(), parent->childIds.end(), childId);
213 if (it != parent->childIds.end())
214 {
215 parent->childIds.erase(it);
216 std::cout << "[NodeGraph] Unlinked node " << parentId << " -> " << childId << "\n";
217 unlinked = true;
218 }
219
220 // Check decorator child
221 if (parent->decoratorChildId == childId)
222 {
223 parent->decoratorChildId = -1;
224 std::cout << "[NodeGraph] Unlinked decorator child " << parentId << " -> " << childId << "\n";
225 unlinked = true;
226 }
227
228 if (unlinked)
229 MarkDirty(); // Mark graph as modified
230
231 return unlinked;
232 }
233
234 std::vector<GraphLink> NodeGraph::GetAllLinks() const
235 {
236 std::vector<GraphLink> links;
237
238 for (const auto& node : m_Nodes)
239 {
240 // Add links to children
241 for (int childId : node.childIds)
242 {
243 links.push_back(GraphLink(node.id, childId));
244 }
245
246 // Add decorator child link
247 if (node.decoratorChildId >= 0)
248 {
249 links.push_back(GraphLink(node.id, node.decoratorChildId));
250 }
251 }
252
253 return links;
254 }
255
256 bool NodeGraph::SetNodeParameter(int nodeId, const std::string& paramName, const std::string& value)
257 {
258 GraphNode* node = GetNode(nodeId);
259 if (!node)
260 return false;
261
262 node->parameters[paramName] = value;
263 MarkDirty(); // Mark graph as modified
264 return true;
265 }
266
267 std::string NodeGraph::GetNodeParameter(int nodeId, const std::string& paramName) const
268 {
269 const GraphNode* node = GetNode(nodeId);
270 if (!node)
271 return "";
272
273 auto it = node->parameters.find(paramName);
274 if (it != node->parameters.end())
275 return it->second;
276
277 return "";
278 }
279
280 nlohmann::json NodeGraph::ToJson() const
281 {
282 json j;
283
284 // v2 Schema wrapper
285 j["schema_version"] = 2;
286 j["blueprintType"] = type.empty() ? "BehaviorTree" : type;
287 j["name"] = name;
288 j["description"] = ""; // Could be added to NodeGraph if needed
289
290 // Metadata section
291 j["metadata"]["author"] = "User"; // Could be made configurable
292 j["metadata"]["created"] = ""; // Could be tracked if needed
293 j["metadata"]["lastModified"] = editorMetadata.lastModified;
294 j["metadata"]["tags"] = json::array();
295
296 // Editor state
297 j["editorState"]["zoom"] = editorMetadata.zoom;
298 j["editorState"]["scrollOffset"]["x"] = editorMetadata.scrollOffsetX;
299 j["editorState"]["scrollOffset"]["y"] = editorMetadata.scrollOffsetY;
300
301 // Data section containing the actual tree
302 j["data"]["rootNodeId"] = rootNodeId;
303 j["data"]["nodes"] = json::array();
304 j["data"]["eventRoots"] = json::array(); // NEW: Array of OnEvent root node IDs
305
306 for (const auto& node : m_Nodes)
307 {
308 json nj;
309 nj["id"] = node.id;
310 nj["type"] = NodeTypeToString(node.type);
311 nj["name"] = node.name;
312
313 // Save position in a structured format
314 nj["position"]["x"] = node.posX;
315 nj["position"]["y"] = node.posY;
316
317 if (!node.actionType.empty())
318 nj["actionType"] = node.actionType;
319 if (!node.conditionType.empty())
320 nj["conditionType"] = node.conditionType;
321 if (!node.decoratorType.empty())
322 nj["decoratorType"] = node.decoratorType;
323 if (!node.subgraphUUID.empty())
324 nj["subgraphUUID"] = node.subgraphUUID;
325
326 // NEW: Event-driven execution fields (for OnEvent nodes)
327 if (!node.eventType.empty())
328 nj["eventType"] = node.eventType;
329 if (!node.eventMessage.empty())
330 nj["eventMessage"] = node.eventMessage;
331
332 // Parameters as nested object (v2 format)
333 nj["parameters"] = json::object();
334 if (!node.parameters.empty())
335 {
336 for (const auto& pair : node.parameters)
337 nj["parameters"][pair.first] = pair.second;
338 }
339
340 // Children array
341 nj["children"] = json::array();
342 for (int childId : node.childIds)
343 nj["children"].push_back(childId);
344
345 if (node.decoratorChildId >= 0)
346 nj["decoratorChild"] = node.decoratorChildId;
347
348 j["data"]["nodes"].push_back(nj);
349 }
350
351 // NEW: Save event root IDs separately
353 {
354 j["data"]["eventRoots"].push_back((int)eventRootId);
355 }
356
357 return j;
358 }
359
360 NodeGraph NodeGraph::FromJson(const nlohmann::json& j)
361 {
362 std::cout << "[NodeGraph::FromJson] Starting parsing..." << std::endl;
363
365
366 try {
367 // Detect schema version - v2 has nested "data" structure, v1 doesn't
368 bool isV2 = j.contains("schema_version") || j.contains("data");
369 std::cout << "[NodeGraph::FromJson] Format: " << (isV2 ? "v2" : "v1") << std::endl;
370
371 const json* dataSection = &j;
372
373 if (isV2 && j.contains("data"))
374 {
375 const json& dataObj = j["data"];
376 graph.name = JsonHelper::GetString(j, "name", "Untitled Graph");
377 graph.type = JsonHelper::GetString(j, "blueprintType", "BehaviorTree");
378 std::cout << "[NodeGraph::FromJson] Extracted 'data' section from v2" << std::endl;
379
380 // Phase 8: support the flat-dictionary subgraph format where
381 // nodes live in data.rootGraph rather than directly in data.
382 if (dataObj.contains("rootGraph") && dataObj["rootGraph"].is_object())
383 {
384 dataSection = &dataObj["rootGraph"];
385 std::cout << "[NodeGraph::FromJson] Using data.rootGraph (Phase 8 format)" << std::endl;
386 }
387 else
388 {
390 }
391 }
392 else
393 {
394 graph.name = JsonHelper::GetString(j, "name", "Untitled Graph");
395 graph.type = JsonHelper::GetString(j, "type", "BehaviorTree");
396 std::cout << "[NodeGraph::FromJson] Using root as data section (v1)" << std::endl;
397 }
398
399 graph.rootNodeId = JsonHelper::GetInt(*dataSection, "rootNodeId", -1);
400 std::cout << "[NodeGraph::FromJson] Root node ID: " << graph.rootNodeId << std::endl;
401
402 // Parse nodes
403 if (!JsonHelper::IsArray(*dataSection, "nodes"))
404 {
405 std::cerr << "[NodeGraph::FromJson] ERROR: No 'nodes' array in data section" << std::endl;
406 return graph;
407 }
408
409 // Get node count
410 int nodeCount = 0;
411 JsonHelper::ForEachInArray(*dataSection, "nodes", [&](const json& nj, size_t idx) { nodeCount++; });
412 std::cout << "[NodeGraph::FromJson] Parsing " << nodeCount << " nodes..." << std::endl;
413
414 int maxId = 0;
415 bool hasPositions = false;
416
417 // First pass: load nodes
418 JsonHelper::ForEachInArray(*dataSection, "nodes", [&](const json& nj, size_t idx)
419 {
421 node.id = JsonHelper::GetInt(nj, "id", 0);
422 node.type = StringToNodeType(JsonHelper::GetString(nj, "type", "Action"));
423 node.name = JsonHelper::GetString(nj, "name", "");
424
425 std::string typeStr = JsonHelper::GetString(nj, "type", "Action");
426
427 // Load position - try v2 format first
428 if (nj.contains("position") && nj["position"].is_object())
429 {
430 node.posX = JsonHelper::GetFloat(nj["position"], "x", 0.0f);
431 node.posY = JsonHelper::GetFloat(nj["position"], "y", 0.0f);
432 hasPositions = true;
433 }
434 else
435 {
436 // v1 format has no position - will calculate later
437 node.posX = 0.0f;
438 node.posY = 0.0f;
439 }
440
441 node.actionType = JsonHelper::GetString(nj, "actionType", "");
442 node.conditionType = JsonHelper::GetString(nj, "conditionType", "");
443 node.decoratorType = JsonHelper::GetString(nj, "decoratorType", "");
444
445 // Phase 8: load subgraph UUID reference for BT_SubGraph / HFSM_SubGraph nodes.
446 node.subgraphUUID = JsonHelper::GetString(nj, "subgraphUUID", "");
447
448 // NEW: Load event-driven execution fields (for OnEvent nodes)
449 node.eventType = JsonHelper::GetString(nj, "eventType", "");
450 node.eventMessage = JsonHelper::GetString(nj, "eventMessage", "");
451
452 // Load parameters - v2 has nested "parameters" object, v1 has flat structure
453 if (nj.contains("parameters") && nj["parameters"].is_object())
454 {
455 // v2 format
456 const json& params = nj["parameters"];
457 for (auto it = params.begin(); it != params.end(); ++it)
458 {
459 node.parameters[it.key()] = it.value().is_string() ? it.value().get<std::string>()
460 : it.value().dump();
461 }
462 }
463 else
464 {
465 // v1 format - parameters are flat in node object
466 if (nj.contains("param"))
467 node.parameters["param"] = nj["param"].dump();
468 if (nj.contains("param1"))
469 node.parameters["param1"] = nj["param1"].dump();
470 if (nj.contains("param2"))
471 node.parameters["param2"] = nj["param2"].dump();
472 }
473
474 // Load children
475 if (JsonHelper::IsArray(nj, "children"))
476 {
477 JsonHelper::ForEachInArray(nj, "children", [&](const json& childJson, size_t childIdx)
478 {
479 if (childJson.is_number())
480 node.childIds.push_back(childJson.get<int>());
481 });
482 }
483
484 node.decoratorChildId = JsonHelper::GetInt(nj, "decoratorChild", -1);
485
486 graph.m_Nodes.push_back(node);
487
488 std::cout << "[NodeGraph::FromJson] Node " << node.id << ": " << node.name
489 << " (" << typeStr << ") at (" << node.posX << "," << node.posY << ")"
490 << " children: " << node.childIds.size() << std::endl;
491
492 if (node.id > maxId)
493 maxId = node.id;
494 });
495
496 graph.m_NextNodeId = maxId + 1;
497
498 // NEW: Load event root IDs (OnEvent nodes)
499 if (JsonHelper::IsArray(*dataSection, "eventRoots"))
500 {
501 std::cout << "[NodeGraph::FromJson] Loading event roots..." << std::endl;
502 JsonHelper::ForEachInArray(*dataSection, "eventRoots", [&](const json& eventRootJson, size_t idx)
503 {
504 if (eventRootJson.is_number())
505 {
506 uint32_t eventRootId = eventRootJson.get<uint32_t>();
507 graph.m_eventRootIds.push_back(eventRootId);
508 std::cout << "[NodeGraph::FromJson] Event root: " << eventRootId << std::endl;
509 }
510 });
511 }
512
513 // Calculate positions if v1 (no positions)
514 if (!hasPositions)
515 {
516 std::cout << "[NodeGraph::FromJson] No positions found, calculating hierarchical layout..." << std::endl;
517 graph.CalculateNodePositionsHierarchical();
518 std::cout << "[NodeGraph::FromJson] Position calculation complete" << std::endl;
519 }
520 else
521 {
522 std::cout << "[NodeGraph::FromJson] Using existing node positions from file" << std::endl;
523 }
524
525 // Load editor metadata if present (v2 only)
526 if (isV2)
527 {
528 if (j.contains("editorState") && j["editorState"].is_object())
529 {
530 const json& state = j["editorState"];
531 graph.editorMetadata.zoom = JsonHelper::GetFloat(state, "zoom", 1.0f);
532
533 if (state.contains("scrollOffset") && state["scrollOffset"].is_object())
534 {
535 graph.editorMetadata.scrollOffsetX = JsonHelper::GetFloat(state["scrollOffset"], "x", 0.0f);
536 graph.editorMetadata.scrollOffsetY = JsonHelper::GetFloat(state["scrollOffset"], "y", 0.0f);
537 }
538 }
539 }
540
541 std::cout << "[NodeGraph::FromJson] Parsing complete: " << graph.m_Nodes.size() << " nodes loaded" << std::endl;
542
543 return graph;
544
545 } catch (const std::exception& e) {
546 std::cerr << "[NodeGraph::FromJson] EXCEPTION: " << e.what() << std::endl;
547 return graph;
548 }
549 }
550
551 bool NodeGraph::ValidateGraph(std::string& errorMsg) const
552 {
553 // Check for cycles (simplified check)
554 // Check that all child references are valid
555 for (const auto& node : m_Nodes)
556 {
557 for (int childId : node.childIds)
558 {
559 if (!GetNode(childId))
560 {
561 errorMsg = "Node " + std::to_string(node.id) + " has invalid child " + std::to_string(childId);
562 return false;
563 }
564 }
565
566 if (node.decoratorChildId >= 0 && !GetNode(node.decoratorChildId))
567 {
568 errorMsg = "Node " + std::to_string(node.id) + " has invalid decorator child";
569 return false;
570 }
571 }
572
573 return true;
574 }
575
576 void NodeGraph::Clear()
577 {
578 m_Nodes.clear();
579 m_NextNodeId = 1;
580 rootNodeId = -1;
582 m_commandHistory->Clear();
583 }
584
585 CommandHistory* NodeGraph::GetCommandHistory()
586 {
587 return m_commandHistory.get();
588 }
589
590 const CommandHistory* NodeGraph::GetCommandHistory() const
591 {
592 return m_commandHistory.get();
593 }
594
595 bool NodeGraph::CanUndo() const
596 {
597 return m_commandHistory && m_commandHistory->CanUndo();
598 }
599
600 bool NodeGraph::CanRedo() const
601 {
602 return m_commandHistory && m_commandHistory->CanRedo();
603 }
604
605 std::string NodeGraph::GetUndoDescription() const
606 {
607 return m_commandHistory ? m_commandHistory->GetUndoDescription() : "";
608 }
609
610 std::string NodeGraph::GetRedoDescription() const
611 {
612 return m_commandHistory ? m_commandHistory->GetRedoDescription() : "";
613 }
614
615 bool NodeGraph::Undo()
616 {
617 return m_commandHistory && m_commandHistory->Undo();
618 }
619
620 bool NodeGraph::Redo()
621 {
622 return m_commandHistory && m_commandHistory->Redo();
623 }
624
625 // ========== Event Root Methods ==========
626
627 bool NodeGraph::IsValidRoot(uint32_t nodeId) const
628 {
629 // Main Root node
630 if (static_cast<int>(nodeId) == rootNodeId)
631 return true;
632
633 // OnEvent roots
634 return std::find(m_eventRootIds.begin(), m_eventRootIds.end(), nodeId) != m_eventRootIds.end();
635 }
636
637 void NodeGraph::AddEventRoot(uint32_t nodeId)
638 {
639 // Check if already in list
640 if (std::find(m_eventRootIds.begin(), m_eventRootIds.end(), nodeId) == m_eventRootIds.end())
641 {
642 m_eventRootIds.push_back(nodeId);
643 MarkDirty();
644 std::cout << "[NodeGraph] Added event root: " << nodeId << std::endl;
645 }
646 }
647
648 void NodeGraph::RemoveEventRoot(uint32_t nodeId)
649 {
650 auto it = std::find(m_eventRootIds.begin(), m_eventRootIds.end(), nodeId);
651 if (it != m_eventRootIds.end())
652 {
653 m_eventRootIds.erase(it);
654 MarkDirty();
655 std::cout << "[NodeGraph] Removed event root: " << nodeId << std::endl;
656 }
657 }
658
659 const std::vector<uint32_t>& NodeGraph::GetEventRootIds() const
660 {
661 return m_eventRootIds;
662 }
663
664 int NodeGraph::FindNodeIndex(int nodeId) const
665 {
666 for (size_t i = 0; i < m_Nodes.size(); ++i)
667 {
668 if (m_Nodes[i].id == nodeId)
669 return static_cast<int>(i);
670 }
671 return -1;
672 }
673
674 void NodeGraph::CalculateNodePositionsHierarchical()
675 {
676 const float HORIZONTAL_SPACING = 350.0f;
677 const float VERTICAL_SPACING = 200.0f;
678 const float START_X = 200.0f;
679 const float START_Y = 300.0f;
680
681 std::cout << "[NodeGraph] Calculating hierarchical positions for " << m_Nodes.size() << " nodes\n";
682
683 // Build parent-child map
684 std::map<int, std::vector<int>> childrenMap;
685 for (const auto& node : m_Nodes)
686 {
687 if (!node.childIds.empty())
688 {
689 childrenMap[node.id] = node.childIds;
690 }
691 }
692
693 // BFS from root to assign positions by depth
694 if (rootNodeId < 0)
695 {
696 std::cerr << "[NodeGraph] No root node ID, cannot calculate positions\n";
697 return;
698 }
699
700 std::queue<std::pair<int, int>> queue; // nodeId, depth
701 queue.push({rootNodeId, 0});
702
703 std::map<int, int> depthCounter; // tracks sibling index at each depth
704 std::set<int> visited;
705
706 while (!queue.empty())
707 {
708 std::pair<int, int> front = queue.front();
709 int nodeId = front.first;
710 int depth = front.second;
711 queue.pop();
712
713 if (visited.count(nodeId)) continue;
714 visited.insert(nodeId);
715
716 int siblingIndex = depthCounter[depth]++;
717
718 // Find node and set position
719 int nodeIndex = FindNodeIndex(nodeId);
720 if (nodeIndex >= 0)
721 {
722 m_Nodes[nodeIndex].posX = START_X + depth * HORIZONTAL_SPACING;
723 m_Nodes[nodeIndex].posY = START_Y + siblingIndex * VERTICAL_SPACING;
724
725 std::cout << "[NodeGraph] Node " << nodeId << " positioned at ("
726 << m_Nodes[nodeIndex].posX << ", " << m_Nodes[nodeIndex].posY << ")\n";
727 }
728
729 // Queue children
730 if (childrenMap.count(nodeId))
731 {
732 for (int childId : childrenMap[nodeId])
733 {
734 if (!visited.count(childId))
735 {
736 queue.push({childId, depth + 1});
737 }
738 }
739 }
740 }
741
742 std::cout << "[NodeGraph] Position calculation complete\n";
743 }
744
745 // ========== NodeGraphManager Implementation ==========
746
752
756
761
763 {
764 if (m_Initialized)
765 return;
766
767 std::cout << "[NodeGraphManager] Initializing...\n";
768 m_Initialized = true;
769 }
770
772 {
773 if (!m_Initialized)
774 return;
775
776 std::cout << "[NodeGraphManager] Shutting down...\n";
777 m_Graphs.clear();
778 m_ActiveGraphId = -1;
779 m_Initialized = false;
780 }
781
782 int NodeGraphManager::CreateGraph(const std::string& name, const std::string& type)
783 {
784 auto graph = std::make_unique<NodeGraph>();
785 graph->name = name;
786 graph->type = type;
787
788 int graphId = m_NextGraphId++;
789 m_Graphs[graphId] = std::move(graph);
790 m_GraphOrder.push_back(graphId); // Track insertion order
791 m_ActiveGraphId = graphId;
792 m_LastActiveGraphId = graphId; // Update last active
793
794 std::cout << "[NodeGraphManager] Created graph " << graphId << " (" << name << ")\n";
795 return graphId;
796 }
797
799 {
800 auto it = m_Graphs.find(graphId);
801 if (it == m_Graphs.end())
802 return false;
803
804 // Remove from graph order
805 auto orderIt = std::find(m_GraphOrder.begin(), m_GraphOrder.end(), graphId);
806 if (orderIt != m_GraphOrder.end())
807 m_GraphOrder.erase(orderIt);
808
809 m_Graphs.erase(it);
810
811 if (m_ActiveGraphId == graphId)
812 {
813 // Try to select a neighbor from graph order for better UX
814 // Find closest neighbor (prefer next, then previous)
815 if (!m_GraphOrder.empty())
816 {
817 // Find where the closed graph was in order
818 size_t closedIndex = 0;
819 for (size_t i = 0; i < m_GraphOrder.size(); ++i)
820 {
821 if (m_GraphOrder[i] > graphId)
822 {
823 closedIndex = i;
824 break;
825 }
826 closedIndex = i + 1;
827 }
828
829 // Pick the next available tab, or previous if at end
830 if (closedIndex < m_GraphOrder.size())
832 else if (!m_GraphOrder.empty())
834 else
835 m_ActiveGraphId = -1;
836 }
837 else
838 {
839 m_ActiveGraphId = -1;
840 }
841
842 if (m_ActiveGraphId != -1)
844 }
845
846 std::cout << "[NodeGraphManager] Closed graph " << graphId << "\n";
847 return true;
848 }
849
851 {
852 auto it = m_Graphs.find(graphId);
853 if (it == m_Graphs.end())
854 return nullptr;
855 return it->second.get();
856 }
857
858 const NodeGraph* NodeGraphManager::GetGraph(int graphId) const
859 {
860 auto it = m_Graphs.find(graphId);
861 if (it == m_Graphs.end())
862 return nullptr;
863 return it->second.get();
864 }
865
867 {
868 if (m_Graphs.find(graphId) != m_Graphs.end())
869 {
870 m_ActiveGraphId = graphId;
871 m_LastActiveGraphId = graphId; // Update last active for persistence
872 }
873 }
874
879
884
885 std::vector<int> NodeGraphManager::GetAllGraphIds() const
886 {
887 // Return graphs in insertion order for consistent tab rendering
888 return m_GraphOrder;
889 }
890
891 std::string NodeGraphManager::GetGraphName(int graphId) const
892 {
893 const NodeGraph* graph = GetGraph(graphId);
894 return graph ? graph->name : "";
895 }
896
897 void NodeGraphManager::SetGraphOrder(const std::vector<int>& newOrder)
898 {
899 // Update the graph order (e.g., after tab reordering in UI)
900 // Only update if the order contains valid graph IDs
901 if (newOrder.size() != m_GraphOrder.size())
902 return;
903
904 // Verify all IDs in newOrder exist in m_Graphs
905 for (int graphId : newOrder)
906 {
907 if (m_Graphs.find(graphId) == m_Graphs.end())
908 return; // Invalid ID, don't update
909 }
910
912 }
913
914 bool NodeGraphManager::SaveGraph(int graphId, const std::string& filepath)
915 {
916 NodeGraph* graph = GetGraph(graphId);
917 if (!graph)
918 return false;
919
920 // Update lastModified timestamp
921 auto now = std::chrono::system_clock::now();
922 auto time = std::chrono::system_clock::to_time_t(now);
923 std::stringstream ss;
924
925 #ifdef _MSC_VER
926 std::tm timeinfo;
928 ss << std::put_time(&timeinfo, "%Y-%m-%dT%H:%M:%S");
929 #else
930 // Use localtime_r for thread safety on POSIX systems
931 std::tm timeinfo;
933 ss << std::put_time(&timeinfo, "%Y-%m-%dT%H:%M:%S");
934 #endif
935
936 graph->editorMetadata.lastModified = ss.str();
937
938 json j = graph->ToJson();
939
940 std::ofstream file(filepath);
941 if (!file.is_open())
942 return false;
943
944 file << j.dump(2);
945 file.close();
946
947 // Update filepath and clear dirty flag on successful save
948 graph->SetFilepath(filepath);
949 graph->ClearDirty();
950
951 std::cout << "[NodeGraphManager] Saved graph " << graphId << " to " << filepath << "\n";
952 return true;
953 }
954
955 int NodeGraphManager::LoadGraph(const std::string& filepath)
956 {
957 std::cout << "\n========================================" << std::endl;
958 std::cout << "[NodeGraphManager::LoadGraph] CALLED" << std::endl;
959 std::cout << "[NodeGraphManager::LoadGraph] Path: " << filepath << std::endl;
960 std::cout << "========================================+n" << std::endl;
961
962 try {
963 // 1. Check file exists
964 std::cout << "[NodeGraphManager] Step 1: Checking file exists..." << std::endl;
965 std::ifstream testFile(filepath);
966 if (!testFile.is_open())
967 {
968 std::cerr << "[NodeGraphManager] ERROR: File not found: " << filepath << std::endl;
969 std::cout << "========================================+n" << std::endl;
970 return -1;
971 }
972 testFile.close();
973 std::cout << "[NodeGraphManager] File exists: OK" << std::endl;
974
975 // 2. Load file content
976 std::cout << "[NodeGraphManager] Step 2: Loading file content..." << std::endl;
977 std::ifstream file(filepath);
978 if (!file.is_open())
979 {
980 std::cerr << "[NodeGraphManager] ERROR: Cannot open file: " << filepath << std::endl;
981 std::cout << "========================================+n" << std::endl;
982 return -1;
983 }
984
985 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
986 file.close();
987 std::cout << "[NodeGraphManager] File loaded: " << content.size() << " bytes" << std::endl;
988
989 if (content.empty())
990 {
991 std::cerr << "[NodeGraphManager] ERROR: File is empty" << std::endl;
992 std::cout << "========================================+n" << std::endl;
993 return -1;
994 }
995
996 // 3. Parse JSON
997 std::cout << "[NodeGraphManager] Step 3: Parsing JSON..." << std::endl;
998 json j;
999 try {
1000 j = json::parse(content);
1001 std::cout << "[NodeGraphManager] JSON parsed: OK" << std::endl;
1002 } catch (const std::exception& e) {
1003 std::cerr << "[NodeGraphManager] ERROR parsing JSON: " << e.what() << std::endl;
1004 std::cout << "========================================+n" << std::endl;
1005 return -1;
1006 }
1007
1008 // 3b. Phase 8: auto-migrate legacy format to flat-dictionary subgraph format.
1009 {
1011 if (migrator.NeedsMigration(j))
1012 {
1013 std::cout << "[NodeGraphManager] Applying Phase 8 subgraph migration..." << std::endl;
1014 j = migrator.Migrate(j);
1015
1016 // Persist the migrated file immediately so it is not re-migrated next load.
1017 try {
1018 std::ofstream migOut(filepath);
1019 if (migOut.is_open())
1020 {
1021 migOut << j.dump(2);
1022 migOut.close();
1023 std::cout << "[NodeGraphManager] Migrated file saved: " << filepath << std::endl;
1024 }
1025 } catch (const std::exception& saveEx) {
1026 std::cerr << "[NodeGraphManager] WARNING: Could not save migrated file: "
1027 << saveEx.what() << std::endl;
1028 }
1029 }
1030 }
1031
1032 // 4. Detect version
1033 std::cout << "[NodeGraphManager] Step 4: Detecting version..." << std::endl;
1034 bool isV2 = j.contains("schema_version") &&
1035 (j["schema_version"].get<int>() >= 2);
1036 bool isV1 = !isV2 && (j.contains("nodes") || j.contains("rootNodeId"));
1037
1038 std::cout << "[NodeGraphManager] Version: " << (isV2 ? "v2+" : (isV1 ? "v1" : "Unknown")) << std::endl;
1039
1040 if (!isV1 && !isV2)
1041 {
1042 std::cerr << "[NodeGraphManager] ERROR: Invalid blueprint format (neither v1 nor v2)" << std::endl;
1043 std::cout << "========================================+n" << std::endl;
1044 return -1;
1045 }
1046
1047 // 5. Parse graph from JSON
1048 std::cout << "[NodeGraphManager] Step 5: Parsing graph with FromJson..." << std::endl;
1050 try {
1051 graph = NodeGraph::FromJson(j);
1052 std::cout << "[NodeGraphManager] FromJson returned: " << graph.GetAllNodes().size() << " nodes" << std::endl;
1053 } catch (const std::exception& e) {
1054 std::cerr << "[NodeGraphManager] ERROR in FromJson: " << e.what() << std::endl;
1055 std::cout << "========================================+n" << std::endl;
1056 return -1;
1057 }
1058
1059 // 6. Handle v1 migration if needed
1060 if (isV1)
1061 {
1062 std::cout << "[NodeGraphManager] Step 6: Detected v1 format, migrating to v2..." << std::endl;
1063
1064 // Create v2 structure
1065 json v2Json = json::object();
1066 v2Json["schema_version"] = 2;
1067 v2Json["blueprintType"] = graph.type.empty() ? "BehaviorTree" : graph.type;
1068 v2Json["name"] = graph.name;
1069 v2Json["description"] = "";
1070
1071 // Metadata
1072 json metadata = json::object();
1073 metadata["author"] = "Atlasbruce";
1074 metadata["created"] = "2026-01-09T18:26:00Z";
1075 metadata["lastModified"] = "2026-01-09T18:26:00Z";
1076 metadata["tags"] = json::array();
1077 v2Json["metadata"] = metadata;
1078
1079 // Editor state
1080 json editorState = json::object();
1081 editorState["zoom"] = 1.0;
1082 json scrollOffset = json::object();
1083 scrollOffset["x"] = 0;
1084 scrollOffset["y"] = 0;
1085 editorState["scrollOffset"] = scrollOffset;
1086 v2Json["editorState"] = editorState;
1087
1088 // Data (re-serialize current graph)
1089 v2Json["data"] = graph.ToJson();
1090
1091 // Save migrated version
1092 std::cout << "[NodeGraphManager] Saving migrated v2 file..." << std::endl;
1093 try {
1094 // Backup original
1095 std::string backupPath = filepath + ".v1.backup";
1096 std::ifstream src(filepath, std::ios::binary);
1097 std::ofstream dst(backupPath, std::ios::binary);
1098 dst << src.rdbuf();
1099 src.close();
1100 dst.close();
1101 std::cout << "[NodeGraphManager] Original backed up to: " << backupPath << std::endl;
1102
1103 // Save new version
1104 std::ofstream outFile(filepath);
1105 if (outFile.is_open())
1106 {
1107 outFile << v2Json.dump(2);
1108 outFile.close();
1109 std::cout << "[NodeGraphManager] Migrated file saved: " << filepath << std::endl;
1110 }
1111 } catch (const std::exception& e) {
1112 std::cerr << "[NodeGraphManager] WARNING: Could not save migrated file: " << e.what() << std::endl;
1113 }
1114 }
1115
1116 // 7. Create graph in manager
1117 std::cout << "[NodeGraphManager] Step 7: Creating graph in manager..." << std::endl;
1118 int graphId = m_NextGraphId++;
1119 auto graphPtr = std::make_unique<NodeGraph>(std::move(graph));
1120
1121 // Set filepath and clear dirty flag for freshly loaded graph
1122 graphPtr->SetFilepath(filepath);
1123 graphPtr->ClearDirty();
1124
1125 m_Graphs[graphId] = std::move(graphPtr);
1126 m_GraphOrder.push_back(graphId); // Track insertion order
1127 m_ActiveGraphId = graphId;
1128 m_LastActiveGraphId = graphId; // Update last active
1129
1130 std::cout << "[NodeGraphManager] Graph registered with ID: " << graphId << std::endl;
1131 std::cout << "[NodeGraphManager] Graph name: " << m_Graphs[graphId]->name << std::endl;
1132 std::cout << "[NodeGraphManager] Graph type: " << m_Graphs[graphId]->type << std::endl;
1133 std::cout << "[NodeGraphManager] Total graphs loaded: " << m_Graphs.size() << std::endl;
1134 std::cout << "[NodeGraphManager] Active graph ID: " << m_ActiveGraphId << std::endl;
1135
1136 std::cout << "\n========================================" << std::endl;
1137 std::cout << "[NodeGraphManager::LoadGraph] SUCCESS ->" << std::endl;
1138 std::cout << "========================================+n" << std::endl;
1139
1140 return graphId;
1141
1142 } catch (const std::exception& e) {
1143 std::cerr << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl;
1144 std::cerr << "[NodeGraphManager] EXCEPTION: " << e.what() << std::endl;
1145 std::cerr << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl;
1146 std::cout << "========================================+n" << std::endl;
1147 return -1;
1148 } catch (...) {
1149 std::cerr << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl;
1150 std::cerr << "[NodeGraphManager] UNKNOWN EXCEPTION" << std::endl;
1151 std::cerr << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl;
1152 std::cout << "========================================+n" << std::endl;
1153 return -1;
1154 }
1155 }
1156
1157 bool NodeGraphManager::IsGraphDirty(int graphId) const
1158 {
1159 const NodeGraph* graph = GetGraph(graphId);
1160 return graph ? graph->IsDirty() : false;
1161 }
1162
1164 {
1165 for (const auto& pair : m_Graphs)
1166 {
1167 if (pair.second && pair.second->IsDirty())
1168 return true;
1169 }
1170 return false;
1171 }
1172
1173 // ========== Copy/Paste Methods in NodeGraph ==========
1174 void NodeGraph::CopyNodesToClipboard(const std::vector<int>& nodeIds)
1175 {
1176 m_clipboardData.clear();
1177
1178 for (int nodeId : nodeIds)
1179 {
1180 auto nodeIt = std::find_if(m_Nodes.begin(), m_Nodes.end(),
1181 [nodeId](const GraphNode& n) { return n.id == nodeId; });
1182
1183 if (nodeIt != m_Nodes.end())
1184 {
1186 clipNode.nodeId = nodeIt->id;
1187 clipNode.nodeType = static_cast<int>(nodeIt->type);
1188 clipNode.name = nodeIt->name;
1189 clipNode.posX = nodeIt->posX;
1190 clipNode.posY = nodeIt->posY;
1191 clipNode.actionType = nodeIt->actionType;
1192 clipNode.conditionType = nodeIt->conditionType;
1193 clipNode.decoratorType = nodeIt->decoratorType;
1194 clipNode.subgraphUUID = nodeIt->subgraphUUID;
1195 clipNode.parameters = nodeIt->parameters;
1196 clipNode.childIds = nodeIt->childIds;
1197 clipNode.decoratorChildId = nodeIt->decoratorChildId;
1198
1199 m_clipboardData.push_back(clipNode);
1200 }
1201 }
1202
1203 m_IsDirty = true;
1204 }
1205
1206 std::vector<int> NodeGraph::PasteNodesFromClipboard(float offsetX, float offsetY)
1207 {
1208 std::vector<int> pastedIds;
1209
1210 if (m_clipboardData.empty())
1211 return pastedIds;
1212
1213 std::map<int, int> idMapping; // Old ID -> New ID
1214 int maxId = m_NextNodeId;
1215
1216 // Create new nodes from clipboard
1218 {
1220 newNode.id = maxId;
1221 newNode.type = static_cast<NodeType>(clipNode.nodeType);
1222 newNode.name = clipNode.name;
1223 newNode.posX = clipNode.posX + offsetX;
1224 newNode.posY = clipNode.posY + offsetY;
1225 newNode.actionType = clipNode.actionType;
1226 newNode.conditionType = clipNode.conditionType;
1227 newNode.decoratorType = clipNode.decoratorType;
1228 newNode.subgraphUUID = clipNode.subgraphUUID;
1229 newNode.parameters = clipNode.parameters;
1230 // Don't copy connections initially
1231 newNode.childIds.clear();
1232 newNode.decoratorChildId = -1;
1233
1234 idMapping[clipNode.nodeId] = maxId;
1235 pastedIds.push_back(maxId);
1236 m_Nodes.push_back(newNode);
1237 maxId++;
1238 }
1239
1241 m_IsDirty = true;
1242
1243 return pastedIds;
1244 }
1245
1246 std::vector<int> NodeGraph::DuplicateNodes(const std::vector<int>& nodeIds, float offsetX, float offsetY)
1247 {
1248 std::vector<int> duplicatedIds;
1249
1250 for (int origId : nodeIds)
1251 {
1252 auto nodeIt = std::find_if(m_Nodes.begin(), m_Nodes.end(),
1253 [origId](const GraphNode& n) { return n.id == origId; });
1254
1255 if (nodeIt != m_Nodes.end())
1256 {
1259 newNode.posX += offsetX;
1260 newNode.posY += offsetY;
1261 newNode.name = nodeIt->name + " (copy)";
1262 newNode.childIds.clear();
1263 newNode.decoratorChildId = -1;
1264
1265 duplicatedIds.push_back(newNode.id);
1266 m_Nodes.push_back(newNode);
1267 }
1268 }
1269
1270 m_IsDirty = true;
1271
1272 return duplicatedIds;
1273 }
1274}
Concrete command implementations for BehaviorTree operations.
nlohmann::json json
Undo/redo history manager for graph commands.
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Phase 8 — Migrates legacy blueprint data to the flat-dictionary subgraph format.
Manages undo/redo stacks for graph operations.
NodeGraphManager - Manages multiple node graphs Allows opening multiple behavior trees/FSMs simultane...
bool IsGraphDirty(int graphId) const
std::vector< int > GetAllGraphIds() const
void SetGraphOrder(const std::vector< int > &newOrder)
int LoadGraph(const std::string &filepath)
NodeGraph * GetGraph(int graphId)
std::string GetGraphName(int graphId) const
int CreateGraph(const std::string &name, const std::string &type)
std::vector< int > m_GraphOrder
static NodeGraphManager & Instance()
bool SaveGraph(int graphId, const std::string &filepath)
std::map< int, std::unique_ptr< NodeGraph > > m_Graphs
EditorMetadata editorMetadata
std::vector< ClipboardNode > m_clipboardData
std::unique_ptr< CommandHistory > m_commandHistory
std::vector< uint32_t > m_eventRootIds
Separate array of node IDs that are OnEvent root nodes These nodes represent independent execution tr...
std::vector< GraphNode > m_Nodes
int FindNodeIndex(int nodeId) const
GraphNode * GetNode(int nodeId)
Converts legacy blueprint JSON to the Phase 8 subgraph flat-dict format.
nlohmann::json Migrate(const nlohmann::json &blueprint) const
Migrates a legacy blueprint to the flat-dictionary format.
std::string GetString(const json &j, const std::string &key, const std::string &defaultValue="")
Safely get a string value from JSON.
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.
< Provides AssetID and INVALID_ASSET_ID
nlohmann::json json
const char * NodeTypeToString(NodeType type)
NodeType StringToNodeType(const std::string &str)
nlohmann::json json
Serializable node data for copy/paste operations.
std::vector< int > childIds