Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
blueprinteditor.cpp
Go to the documentation of this file.
1/*
2 * Olympe Blueprint Editor - Backend Implementation
3 *
4 * Singleton backend managing business logic, state, and data
5 * Completely separated from UI rendering (handled by BlueprintEditorGUI)
6 */
7
8#include "BlueprintEditor.h"
9#include "EntityBlueprint.h"
10#include "EditorContext.h"
11#include "WorldBridge.h"
12#include "EnumCatalogManager.h"
13#include "BTNodeGraphManager.h"
15#include "TemplateManager.h"
16#include "BPCommandSystem.h"
17#include "BlueprintMigrator.h"
18#include "BlueprintValidator.h"
22#include "SubgraphMigrator.h"
23#include "TabManager.h"
24#include "../TaskSystem/TaskGraphLoader.h"
25#include "../Core/AssetManager.h"
26#include "../json_helper.h"
27#include "../system/system_utils.h"
28#include <algorithm>
29#include <iostream>
30#include <fstream>
31#ifndef _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING
32#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING
33#endif
34#include <experimental/filesystem>
35
36namespace fs = std::experimental::filesystem;
37
38using namespace Olympe::Blueprint;
39
40namespace Olympe
41{
42 // ========================================================================
43 // BlueprintEditor Singleton Backend Implementation
44 // ========================================================================
45
51
53 : m_IsActive(false)
54 , m_HasUnsavedChanges(false)
55 , m_AssetRootPath("Blueprints")
56 , m_GamedataRootPath("Gamedata")
57 , m_SelectedEntity(0) // 0 = INVALID_ENTITY_ID
58 , m_CommandStack(nullptr)
59 , m_ShowMigrationDialog(false)
60 {
61 }
62
64 {
66 {
67 delete m_CommandStack;
68 m_CommandStack = nullptr;
69 }
70 }
71
73 {
74 // Initialize backend state
75 m_IsActive = false;
76 m_HasUnsavedChanges = false;
79
80 // Initialize asset management
81 m_AssetTreeRoot = nullptr;
82 m_LastError.clear();
83
84 // Initialize catalog manager
86
87 // Initialize node graph manager
89
90 // Initialize entity inspector manager
92
93 // Initialize template manager
95
96 // Initialize command stack
98
99 // Initialize plugin system
101
102 // Load configuration
103 LoadConfig("blueprint_editor_config.json");
104
105 // Scan assets on initialization
107 }
108
110 {
111 std::cout << "[BlueprintEditor] Initializing Runtime Editor mode\n";
112 // Initialize the EditorContext in Runtime mode
114 }
115
117 {
118 std::cout << "[BlueprintEditor] Initializing Standalone Editor mode\n";
119 // Initialize the EditorContext in Standalone mode
121
122 // Pre-load all ATS graphs found in Blueprints/ and Gamedata/ so that
123 // they are validated and ready for the asset browser and graph panels.
125 }
126
128 {
129 // Save configuration before shutting down
130 SaveConfig("blueprint_editor_config.json");
131
132 // Unregister the task callback before shutting down panels so that
133 // TaskSystem cannot fire callbacks into already-destroyed editor objects.
135
136 // Shutdown managers in reverse order
137 if (m_CommandStack)
138 {
139 delete m_CommandStack;
140 m_CommandStack = nullptr;
141 }
142
147
148 // Clean up backend resources
151 m_HasUnsavedChanges = false;
152 }
153
154 void BlueprintEditor::Update(float deltaTime)
155 {
156 // Backend update logic (non-UI)
157 // This is called by GameEngine when the editor is active
158
159 // Update entity inspector (sync with World)
161
162 // Can be used for background tasks, auto-save, etc.
163 // For now, this is a placeholder for future backend logic
164 // such as:
165 // - Auto-save timer
166 // - Asset watching/hot-reload
167 // - Background compilation
168 // - Validation
169 }
170
171 // Blueprint operations
172 void BlueprintEditor::NewBlueprint(const std::string& name, const std::string& description)
173 {
175 m_CurrentBlueprint.description = description;
177 m_HasUnsavedChanges = true;
178 }
179
180 bool BlueprintEditor::LoadBlueprint(const std::string& filepath)
181 {
183
184 if (loaded.name.empty())
185 {
186 return false;
187 }
188
190 m_CurrentFilepath = filepath;
191 m_HasUnsavedChanges = false;
192 return true;
193 }
194
196 {
197 if (m_CurrentBlueprint.name.empty())
198 {
199 return false;
200 }
201
202 if (m_CurrentFilepath.empty())
203 {
204 // No filepath set - caller should use SaveBlueprintAs
205 return false;
206 }
207
209
210 if (success)
211 {
212 m_HasUnsavedChanges = false;
213 }
214
215 return success;
216 }
217
218 bool BlueprintEditor::SaveBlueprintAs(const std::string& filepath)
219 {
220 if (m_CurrentBlueprint.name.empty())
221 {
222 return false;
223 }
224
225 bool success = m_CurrentBlueprint.SaveToFile(filepath);
226
227 if (success)
228 {
229 m_CurrentFilepath = filepath;
230 m_HasUnsavedChanges = false;
231 }
232
233 return success;
234 }
235
236 // ========================================================================
237 // Asset Management Implementation
238 // ========================================================================
239
240 void BlueprintEditor::SetAssetRootPath(const std::string& path)
241 {
242 m_AssetRootPath = path;
244 }
245
247 {
248 m_LastError.clear();
249
250 if (m_AssetRootPath.empty())
251 {
252 m_LastError = "Asset root path is not set";
253 std::cerr << "BlueprintEditor: " << m_LastError << std::endl;
254 m_AssetTreeRoot = nullptr;
255 return;
256 }
257
258 // Build a virtual root node that holds both Blueprints/ and Gamedata/ trees.
259 auto virtualRoot = std::make_shared<AssetNode>("Assets", ".", true);
260
261 std::cout << "BlueprintEditor: Scanning assets directory: " << m_AssetRootPath << std::endl;
262
263 try
264 {
265 if (fs::exists(m_AssetRootPath) && fs::is_directory(m_AssetRootPath))
266 {
268 virtualRoot->children.push_back(blueprintsTree);
269 std::cout << "BlueprintEditor: Blueprints scan complete" << std::endl;
270 }
271 else
272 {
273 std::cerr << "BlueprintEditor: Blueprints directory not found: "
274 << m_AssetRootPath << std::endl;
275 }
276 }
277 catch (const std::exception& e)
278 {
279 m_LastError = std::string("Error scanning blueprints: ") + e.what();
280 std::cerr << "BlueprintEditor: " << m_LastError << std::endl;
281 }
282
283 // Also scan the Gamedata directory (contains .ats ATS task graphs).
284 if (!m_GamedataRootPath.empty())
285 {
286 std::cout << "BlueprintEditor: Scanning gamedata directory: "
287 << m_GamedataRootPath << std::endl;
288 try
289 {
290 if (fs::exists(m_GamedataRootPath) && fs::is_directory(m_GamedataRootPath))
291 {
293 virtualRoot->children.push_back(gamedataTree);
294 std::cout << "BlueprintEditor: Gamedata scan complete" << std::endl;
295 }
296 else
297 {
298 std::cerr << "BlueprintEditor: Gamedata directory not found: "
299 << m_GamedataRootPath << std::endl;
300 }
301 }
302 catch (const std::exception& e)
303 {
304 std::cerr << "BlueprintEditor: Error scanning gamedata: " << e.what() << std::endl;
305 }
306 }
307
309
310 // ===== PERFORMANCE: Invalidate cache when assets are rescanned =====
311 // Asset files may have changed, so cached metadata would be stale
313 }
314
315 std::shared_ptr<AssetNode> BlueprintEditor::ScanDirectory(const std::string& path)
316 {
317 auto node = std::make_shared<AssetNode>(
318 fs::path(path).filename().string(),
319 path,
320 true
321 );
322
323 try
324 {
325 for (const auto& entry : fs::directory_iterator(path))
326 {
327 std::string entryPath = entry.path().string();
328 std::string filename = entry.path().filename().string();
329
330 // Skip hidden files and directories
331 if (filename[0] == '.')
332 continue;
333
334 if (fs::is_directory(entry.path()))
335 {
336 // Recursively scan subdirectories
338 node->children.push_back(childNode);
339 }
340 else if (fs::is_regular_file(entry.path()))
341 {
342 // Accept .json blueprint files and .ats ATS task graph files.
343 const std::string ext = entry.path().extension().string();
344 if (ext == ".json" || ext == ".ats")
345 {
346 auto fileNode = std::make_shared<AssetNode>(
347 filename,
348 entryPath,
349 false
350 );
351
352 // Detect asset type
354
355 node->children.push_back(fileNode);
356 }
357 }
358 }
359
360 // Sort children: directories first, then files alphabetically
361 std::sort(node->children.begin(), node->children.end(),
362 [](const std::shared_ptr<AssetNode>& a, const std::shared_ptr<AssetNode>& b)
363 {
364 if (a->isDirectory != b->isDirectory)
365 return a->isDirectory > b->isDirectory;
366 return a->name < b->name;
367 });
368 }
369 catch (const std::exception& e)
370 {
371 std::cerr << "BlueprintEditor: Error scanning directory " << path << ": " << e.what() << std::endl;
372 }
373
374 return node;
375 }
376
377 std::string BlueprintEditor::DetectAssetType(const std::string& filepath)
378 {
379 try
380 {
381 // .ats files are ATS task graphs — detect type from their graphType field
382 // without going through the full blueprint type-detection logic.
383 const std::string ext = fs::path(filepath).extension().string();
384 if (ext == ".ats")
385 {
386 json j;
387 if (!JsonHelper::LoadJsonFromFile(filepath, j))
388 return "TaskGraph";
389
390 if (j.contains("graphType"))
391 return j["graphType"].get<std::string>();
392
393 return "TaskGraph";
394 }
395
396 json j;
397 if (!JsonHelper::LoadJsonFromFile(filepath, j))
398 return "Unknown";
399
400 // Extract filename once for warning messages
401 std::string filename = fs::path(filepath).filename().string();
402
403 // Priority 1: Check explicit "graphType" field (VS v4 / ATS format).
404 // This is the canonical field for schema_version 4 graphs stored as .json.
405 // Must be checked before the legacy "type" field to avoid falling through
406 // to structural detection and emitting a spurious WARNING.
407 if (j.contains("graphType") && j["graphType"].is_string())
408 {
409 return j["graphType"].get<std::string>();
410 }
411
412 // Priority 2: Check explicit "type" field (v1 + v2 standardized)
413 if (j.contains("type"))
414 {
415 std::string type = j["type"].get<std::string>();
416
417 // Validate against blueprintType if present
418 if (j.contains("blueprintType"))
419 {
420 std::string blueprintType = j["blueprintType"].get<std::string>();
421 if (type != blueprintType)
422 {
423 std::cerr << "[BlueprintEditor] WARNING: type (" << type
424 << ") != blueprintType (" << blueprintType << ") in " << filename << std::endl;
425 }
426 }
427
428 return type;
429 }
430
431 // Priority 3: FALLBACK - Check "blueprintType" for old v2 files
432 if (j.contains("blueprintType"))
433 {
434 std::string type = j["blueprintType"].get<std::string>();
435 std::cerr << "[BlueprintEditor] WARNING: Using 'blueprintType' field (missing 'type') in "
436 << filename << std::endl;
437 return type;
438 }
439
440 // Helper lambda for logging structural detection warnings
441 auto logStructuralDetection = [&filename](const std::string& detectedType) {
442 std::cerr << "[BlueprintEditor] WARNING: No type information found in " << filename
443 << ", using structural detection (detected: " << detectedType << ")" << std::endl;
444 };
445
446 // Priority 4: Structural detection for schema v2 (data wrapper)
447 if (j.contains("data"))
448 {
449 const json& data = j["data"];
450 if (data.contains("rootNodeId") && data.contains("nodes"))
451 {
452 logStructuralDetection("BehaviorTree");
453 return "BehaviorTree";
454 }
455 if (data.contains("components"))
456 {
457 logStructuralDetection("EntityPrefab");
458 return "EntityPrefab";
459 }
460 }
461
462 // Priority 5: Structural detection for schema v1 (direct fields)
463 if (j.contains("rootNodeId") && j.contains("nodes"))
464 {
465 logStructuralDetection("BehaviorTree");
466 return "BehaviorTree";
467 }
468
469 if (j.contains("states") || j.contains("initialState"))
470 {
472 return "HFSM";
473 }
474
475 // Priority 6: Structural detection for Visual Script graphs
476 if (j.contains("schema_version") || j.contains("ExecConnections") || j.contains("DataConnections"))
477 {
478 logStructuralDetection("VisualScript");
479 return "VisualScript";
480 }
481
482 if (j.contains("components"))
483 {
484 logStructuralDetection("EntityBlueprint");
485 return "EntityBlueprint";
486 }
487
488 std::cerr << "[BlueprintEditor] WARNING: Could not determine type for " << filename
489 << ", defaulting to Generic" << std::endl;
490 return "Generic";
491 }
492 catch (const std::exception& e)
493 {
494 std::string filename = fs::path(filepath).filename().string();
495 std::cerr << "Error detecting asset type in " << filename << ": " << e.what() << std::endl;
496 return "Unknown";
497 }
498 }
499
500 // ========================================================================
501 // PreloadATSGraphs – recursively load all .ats files from Blueprints/ and
502 // Gamedata/ to validate them and warm up any caches.
503 // ========================================================================
504
506 {
507 std::cout << "[BlueprintEditor] PreloadATSGraphs: scanning "
508 << m_AssetRootPath << " and " << m_GamedataRootPath << std::endl;
509
510 int loaded = 0;
511 int failed = 0;
512
513 // Collect .ats files from the already-scanned asset tree (BFS).
514 std::vector<std::string> atsFiles;
515
516 auto collectAts = [&atsFiles](const std::shared_ptr<AssetNode>& root)
517 {
518 if (!root) return;
519 std::vector<std::shared_ptr<AssetNode>> stack;
520 stack.push_back(root);
521 while (!stack.empty())
522 {
523 auto node = stack.back();
524 stack.pop_back();
525 if (!node) continue;
526 if (!node->isDirectory)
527 {
528 const std::string& p = node->fullPath;
529 if (p.size() > 4 && p.substr(p.size() - 4) == ".ats")
530 {
531 atsFiles.push_back(p);
532 }
533 }
534 for (const auto& child : node->children)
535 stack.push_back(child);
536 }
537 };
538
540
541 // Load and cache each .ats file via AssetManager so that the template
542 // remains resident in memory and can be retrieved without further I/O.
543 for (const auto& path : atsFiles)
544 {
545 std::vector<std::string> errors;
546 AssetID id = AssetManager::Get().LoadTaskGraph(path, errors);
547 if (id != INVALID_ASSET_ID)
548 {
549 std::cout << "[BlueprintEditor] PreloadATSGraphs: OK " << path << std::endl;
550 ++loaded;
551 }
552 else
553 {
554 std::cerr << "[BlueprintEditor] PreloadATSGraphs: FAIL " << path << std::endl;
555 for (const auto& err : errors)
556 std::cerr << " " << err << std::endl;
557 ++failed;
558 }
559 }
560
561 std::cout << "[BlueprintEditor] PreloadATSGraphs complete: "
562 << loaded << " loaded, " << failed << " failed" << std::endl;
563 }
564
565 std::vector<AssetMetadata> BlueprintEditor::GetAllAssets() const
566 {
567 std::vector<AssetMetadata> assets;
568 if (m_AssetTreeRoot)
569 {
571 }
572 return assets;
573 }
574
575 void BlueprintEditor::CollectAllAssets(const std::shared_ptr<AssetNode>& node, std::vector<AssetMetadata>& assets) const
576 {
577 if (!node)
578 return;
579
580 // Add files only, not directories
581 if (!node->isDirectory)
582 {
583 AssetMetadata metadata;
584 metadata.filepath = node->fullPath;
585 metadata.name = node->name;
586 metadata.type = node->type;
587 metadata.isDirectory = false;
588
589 // Parse full metadata (const_cast to call non-const method)
590 const_cast<BlueprintEditor*>(this)->ParseAssetMetadata(node->fullPath, metadata);
591
592 assets.push_back(metadata);
593 }
594
595 // Recursively process children
596 for (const auto& child : node->children)
597 {
599 }
600 }
601
602 std::vector<AssetMetadata> BlueprintEditor::GetAssetsByType(const std::string& type) const
603 {
604 std::vector<AssetMetadata> allAssets = GetAllAssets();
605 std::vector<AssetMetadata> filtered;
606
607 for (const auto& asset : allAssets)
608 {
609 if (asset.type == type)
610 {
611 filtered.push_back(asset);
612 }
613 }
614
615 return filtered;
616 }
617
618 std::vector<AssetMetadata> BlueprintEditor::SearchAssets(const std::string& query) const
619 {
620 std::vector<AssetMetadata> allAssets = GetAllAssets();
621 std::vector<AssetMetadata> results;
622
623 if (query.empty())
624 return allAssets;
625
626 // Case-insensitive search
627 std::string lowerQuery = query;
628 std::transform(lowerQuery.begin(), lowerQuery.end(), lowerQuery.begin(), ::tolower);
629
630 for (const auto& asset : allAssets)
631 {
632 std::string lowerName = asset.name;
633 std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
634
635 if (lowerName.find(lowerQuery) != std::string::npos)
636 {
637 results.push_back(asset);
638 }
639 }
640
641 return results;
642 }
643
645 {
646 // ===== PERFORMANCE OPTIMIZATION: Cache Asset Metadata =====
647 // Check if this metadata is already cached
648 auto cacheIt = m_AssetMetadataCache.find(filepath);
649 if (cacheIt != m_AssetMetadataCache.end())
650 {
651 // Return cached metadata (avoids JSON re-parsing every frame)
652 return cacheIt->second;
653 }
654
655 // ===== Not in cache, load and parse =====
656 AssetMetadata metadata;
657 metadata.filepath = filepath;
658
659 try
660 {
661 // Get filename
662 metadata.name = fs::path(filepath).filename().string();
663
664 // Detect type
665 metadata.type = DetectAssetType(filepath);
666
667 // Parse detailed metadata
668 ParseAssetMetadata(filepath, metadata);
669 }
670 catch (const std::exception& e)
671 {
672 metadata.isValid = false;
673 metadata.errorMessage = std::string("Error loading asset: ") + e.what();
674 std::cerr << "BlueprintEditor: " << metadata.errorMessage << std::endl;
675 }
676
677 // ===== Cache the result =====
678 m_AssetMetadataCache[filepath] = metadata;
679
680 return metadata;
681 }
682
683 void BlueprintEditor::ParseAssetMetadata(const std::string& filepath, AssetMetadata& metadata)
684 {
685 try
686 {
687 json j;
688 if (!JsonHelper::LoadJsonFromFile(filepath, j))
689 {
690 metadata.isValid = false;
691 metadata.errorMessage = "Failed to load JSON file";
692 return;
693 }
694
695 // Priority 1: Check explicit "type" field
696 if (j.contains("type"))
697 {
698 std::string type = JsonHelper::GetString(j, "type", "");
699 metadata.type = type;
700
701 if (type == "EntityBlueprint" || type == "EntityPrefab")
702 {
703 ParseEntityBlueprint(j, metadata);
704 }
705 else if (type == "BehaviorTree")
706 {
707 ParseBehaviorTree(j, metadata);
708 }
709 else if (type == "HFSM")
710 {
711 ParseHFSM(j, metadata);
712 }
713 else
714 {
715 metadata.name = JsonHelper::GetString(j, "name", metadata.name);
716 metadata.description = JsonHelper::GetString(j, "description", "");
717 }
718 }
719 // Priority 2: FALLBACK - Check "blueprintType" for old v2 files
720 else if (j.contains("blueprintType"))
721 {
722 std::string type = JsonHelper::GetString(j, "blueprintType", "");
723 metadata.type = type;
724
725 std::string filename = fs::path(filepath).filename().string();
726 std::cerr << "[ParseAssetMetadata] Warning: Using deprecated 'blueprintType' field in " << filename << std::endl;
727
728 if (type == "BehaviorTree")
729 {
730 ParseBehaviorTree(j, metadata);
731 }
732 else if (type == "HFSM")
733 {
734 ParseHFSM(j, metadata);
735 }
736 else if (type == "EntityBlueprint" || type == "EntityPrefab")
737 {
738 ParseEntityBlueprint(j, metadata);
739 }
740 else
741 {
742 metadata.name = JsonHelper::GetString(j, "name", metadata.name);
743 metadata.description = JsonHelper::GetString(j, "description", "");
744 }
745 }
746 // Priority 3: Structural detection for schema v2 (data wrapper)
747 else if (j.contains("data"))
748 {
749 const json& data = j["data"];
750 if (data.contains("rootNodeId") && data.contains("nodes"))
751 {
752 metadata.type = "BehaviorTree";
753 ParseBehaviorTree(j, metadata);
754 }
755 else if (data.contains("components"))
756 {
757 metadata.type = "EntityPrefab";
758 ParseEntityBlueprint(j, metadata);
759 }
760 else
761 {
762 metadata.type = "Generic";
763 metadata.name = JsonHelper::GetString(j, "name", metadata.name);
764 metadata.description = JsonHelper::GetString(j, "description", "");
765 }
766 }
767 // Priority 4: Structural detection for schema v1 (direct fields)
768 else if (j.contains("rootNodeId") && j.contains("nodes"))
769 {
770 metadata.type = "BehaviorTree";
771 ParseBehaviorTree(j, metadata);
772 }
773 else if (j.contains("states") || j.contains("initialState"))
774 {
775 metadata.type = "HFSM";
776 ParseHFSM(j, metadata);
777 }
778 else if (j.contains("components"))
779 {
780 metadata.type = "EntityBlueprint";
781 ParseEntityBlueprint(j, metadata);
782 }
783 else
784 {
785 metadata.type = "Generic";
786 metadata.name = JsonHelper::GetString(j, "name", metadata.name);
787 metadata.description = JsonHelper::GetString(j, "description", "");
788 }
789
790 metadata.isValid = true;
791 }
792 catch (const std::exception& e)
793 {
794 metadata.isValid = false;
795 metadata.errorMessage = std::string("JSON Parse Error: ") + e.what();
796 std::cerr << "BlueprintEditor: " << metadata.errorMessage << std::endl;
797 }
798 }
799
801 {
802 metadata.name = JsonHelper::GetString(j, "name", "Unnamed Entity");
803 metadata.description = JsonHelper::GetString(j, "description", "");
804
805 // Schema v2: Check for components in "data" wrapper
806 if (j.contains("data") && j["data"].contains("components") && j["data"]["components"].is_array())
807 {
808 const auto& components = j["data"]["components"];
809 metadata.componentCount = (int)components.size();
810
811 // Extract component types
812 for (size_t i = 0; i < components.size(); ++i)
813 {
814 const auto& comp = components[i];
815 if (comp.contains("type") && comp["type"].is_string())
816 {
817 std::string compType = JsonHelper::GetString(comp, "type", "Unknown");
818 metadata.components.push_back(compType);
819 }
820 }
821 }
822 // Schema v1: Check for components at top level
823 else if (j.contains("components") && j["components"].is_array())
824 {
825 const auto& components = j["components"];
826 metadata.componentCount = (int)components.size();
827
828 // Extract component types
829 for (size_t i = 0; i < components.size(); ++i)
830 {
831 const auto& comp = components[i];
832 if (comp.contains("type") && comp["type"].is_string())
833 {
834 std::string compType = JsonHelper::GetString(comp, "type", "Unknown");
835 metadata.components.push_back(compType);
836 }
837 }
838 }
839 }
840
842 {
843 metadata.name = JsonHelper::GetString(j, "name", "Unnamed Behavior Tree");
844 metadata.description = "Behavior Tree AI Definition";
845
846 // Schema v2: Check for nodes in "data" wrapper
847 if (j.contains("data"))
848 {
849 const json& data = j["data"];
850 if (data.contains("nodes") && data["nodes"].is_array())
851 {
852 const auto& nodes = data["nodes"];
853 metadata.nodeCount = (int)nodes.size();
854
855 // Extract node types
856 for (size_t i = 0; i < nodes.size(); ++i)
857 {
858 const auto& node = nodes[i];
859 if (node.contains("type") && node["type"].is_string())
860 {
861 std::string nodeType = JsonHelper::GetString(node, "type", "Unknown");
862 if (node.contains("name") && node["name"].is_string())
863 {
864 std::string nodeName = JsonHelper::GetString(node, "name", "");
865 metadata.nodes.push_back(nodeName + " (" + nodeType + ")");
866 }
867 else
868 {
869 metadata.nodes.push_back(nodeType);
870 }
871 }
872 }
873 }
874
875 if (data.contains("rootNodeId"))
876 {
877 int rootId = data["rootNodeId"].get<int>();
878 metadata.description += " - Root Node ID: " + std::to_string(rootId);
879 }
880 }
881 // Schema v1: Check for nodes at top level
882 else if (j.contains("nodes") && j["nodes"].is_array())
883 {
884 const auto& nodes = j["nodes"];
885 metadata.nodeCount = (int)nodes.size();
886
887 // Extract node types
888 for (size_t i = 0; i < nodes.size(); ++i)
889 {
890 const auto& node = nodes[i];
891 if (node.contains("type") && node["type"].is_string())
892 {
893 std::string nodeType = JsonHelper::GetString(node, "type", "Unknown");
894 if (node.contains("name") && node["name"].is_string())
895 {
896 std::string nodeName = JsonHelper::GetString(node, "name", "");
897 metadata.nodes.push_back(nodeName + " (" + nodeType + ")");
898 }
899 else
900 {
901 metadata.nodes.push_back(nodeType);
902 }
903 }
904 }
905
906 if (j.contains("rootNodeId"))
907 {
908 int rootId = j["rootNodeId"].get<int>();
909 metadata.description += " - Root Node ID: " + std::to_string(rootId);
910 }
911 }
912 }
913
915 {
916 metadata.name = JsonHelper::GetString(j, "name", "Unnamed HFSM");
917 metadata.description = "Hierarchical Finite State Machine";
918
919 // Count states
920 if (j.contains("states") && j["states"].is_array())
921 {
922 const auto& states = j["states"];
923 metadata.nodeCount = (int)states.size();
924
925 // Extract state names
926 for (size_t i = 0; i < states.size(); ++i)
927 {
928 const auto& state = states[i];
929 if (state.contains("name") && state["name"].is_string())
930 {
931 std::string stateName = JsonHelper::GetString(state, "name", "");
932 std::string stateType = JsonHelper::GetString(state, "type", "State");
933 metadata.nodes.push_back(stateName + " (" + stateType + ")");
934 }
935 }
936 }
937
938 // Add initial state info
939 if (j.contains("initialState"))
940 {
941 std::string initialState = JsonHelper::GetString(j, "initialState", "");
942 if (!initialState.empty())
943 {
944 metadata.description += " - Initial State: " + initialState;
945 }
946 }
947 }
948
949 bool BlueprintEditor::IsAssetValid(const std::string& filepath) const
950 {
951 try
952 {
953 json j;
954 return JsonHelper::LoadJsonFromFile(filepath, j);
955 }
956 catch (const std::exception&)
957 {
958 return false;
959 }
960 }
961
962 // ========================================================================
963 // B) Runtime Entity Management Implementation
964 // ========================================================================
965
967 {
968 // Add to runtime entities list if not already present
969 auto it = std::find(m_RuntimeEntities.begin(), m_RuntimeEntities.end(), entityId);
970 if (it == m_RuntimeEntities.end())
971 {
972 m_RuntimeEntities.push_back(entityId);
973 std::cout << "BlueprintEditor: Entity " << entityId << " created (total: "
974 << m_RuntimeEntities.size() << ")" << std::endl;
975 }
976 }
977
979 {
980 // Remove from runtime entities list
981 auto it = std::find(m_RuntimeEntities.begin(), m_RuntimeEntities.end(), entityId);
982 if (it != m_RuntimeEntities.end())
983 {
984 m_RuntimeEntities.erase(it);
985 std::cout << "BlueprintEditor: Entity " << entityId << " destroyed (total: "
986 << m_RuntimeEntities.size() << ")" << std::endl;
987
988 // If this was the selected entity, clear selection
989 if (m_SelectedEntity == entityId)
990 {
991 m_SelectedEntity = 0; // INVALID_ENTITY_ID
992 }
993 }
994 }
995
996 // ========================================================================
997 // C) Entity Selection Implementation
998 // ========================================================================
999
1001 {
1002 if (m_SelectedEntity != entityId)
1003 {
1004 m_SelectedEntity = entityId;
1005 std::cout << "BlueprintEditor: Selected entity " << entityId << std::endl;
1006
1007 // All panels will automatically read this selection on next Render()
1008 // No explicit notification needed - reactive update pattern
1009 }
1010 }
1011
1012 // ========================================================================
1013 // Asset Selection Implementation
1014 // ========================================================================
1015
1017 {
1019 {
1021
1022 // ===== PERFORMANCE: Invalidate cache when asset selection changes =====
1023 // This prevents stale metadata from being displayed if the same asset is
1024 // re-selected after being modified on disk
1026
1027 std::cout << "BlueprintEditor: Selected asset " << assetPath << std::endl;
1028 }
1029 }
1030
1032 {
1033 // ===== Performance Optimization: Clear Asset Metadata Cache =====
1034 // Called when asset selection changes or when asset files are refreshed
1035 // This prevents stale data while maintaining cache for non-selected assets
1036 m_AssetMetadataCache.clear();
1037 }
1038
1039 // ========================================================================
1040 // Graph Loading in Node Graph Editor
1041 // ========================================================================
1042
1044 {
1045 SYSTEM_LOG << "BlueprintEditor: Opening graph " << assetPath << " in editor\n";
1046
1047 // Detect asset type
1048 std::string assetType = DetectAssetType(assetPath);
1049
1050 // Route all graph types through TabManager
1051 if (assetType == "VisualScript" || assetType == "BehaviorTree" ||
1052 assetType == "HFSM" || assetType == "TaskGraph" ||
1053 assetType == "Generic")
1054 {
1055 std::string tabID = TabManager::Get().OpenFileInTab(assetPath);
1056 if (tabID.empty())
1057 {
1058 SYSTEM_LOG << "BlueprintEditor: TabManager failed to open: " << assetPath << "\n";
1059 m_LastError = "Failed to open graph file: " + assetPath;
1060 }
1061 return;
1062 }
1063
1064 // Fallback: legacy BehaviorTree directly via NodeGraphManager
1065 if (assetType != "BehaviorTree" && assetType != "HFSM")
1066 {
1067 SYSTEM_LOG << "BlueprintEditor: Cannot open asset type '" << assetType << "'\n";
1068 m_LastError = "Asset type '" + assetType + "' cannot be opened in editor";
1069 return;
1070 }
1071
1072 // Use NodeGraphManager to load the graph (legacy path)
1073 int graphId = NodeGraphManager::Get().LoadGraph(assetPath);
1074 if (graphId < 0)
1075 {
1076 SYSTEM_LOG << "BlueprintEditor: Failed to load graph from " << assetPath << "\n";
1077 m_LastError = "Failed to load graph file: " + assetPath;
1078 return;
1079 }
1080 SYSTEM_LOG << "BlueprintEditor: Graph loaded with ID " << graphId << "\n";
1081 }
1082
1083 // ========================================================================
1084 // Phase 5: Template Management Implementation
1085 // ========================================================================
1086
1087 bool BlueprintEditor::SaveCurrentAsTemplate(const std::string& name,
1088 const std::string& description,
1089 const std::string& category)
1090 {
1091 if (!HasBlueprint())
1092 {
1093 m_LastError = "No blueprint loaded to save as template";
1094 return false;
1095 }
1096
1097 // Convert current blueprint to JSON
1099
1100 // Create template from current blueprint
1103 name,
1104 description,
1105 category,
1106 "User"
1107 );
1108
1109 // Save template
1110 if (!TemplateManager::Get().SaveTemplate(tpl))
1111 {
1112 m_LastError = "Failed to save template: " + TemplateManager::Get().GetLastError();
1113 return false;
1114 }
1115
1116 std::cout << "Template saved: " << name << " (" << tpl.id << ")" << std::endl;
1117 return true;
1118 }
1119
1121 {
1123
1124 if (!TemplateManager::Get().ApplyTemplateToBlueprint(templateId, blueprintJson))
1125 {
1126 m_LastError = "Failed to apply template: " + TemplateManager::Get().GetLastError();
1127 return false;
1128 }
1129
1130 // Load the blueprint from JSON
1132 m_CurrentFilepath = ""; // Clear filepath since this is a new blueprint from template
1133 m_HasUnsavedChanges = true;
1134
1135 std::cout << "Template applied: " << templateId << std::endl;
1136 return true;
1137 }
1138
1140 {
1142 {
1143 m_LastError = "Failed to delete template: " + TemplateManager::Get().GetLastError();
1144 return false;
1145 }
1146
1147 std::cout << "Template deleted: " << templateId << std::endl;
1148 return true;
1149 }
1150
1152 {
1154 std::cout << "Templates reloaded" << std::endl;
1155 }
1156
1157 // ========================================================================
1158 // Phase 6: Undo/Redo System Implementation
1159 // ========================================================================
1160
1162 {
1163 if (m_CommandStack)
1164 {
1166 m_HasUnsavedChanges = true;
1167 }
1168 }
1169
1171 {
1172 if (m_CommandStack)
1173 {
1175 m_HasUnsavedChanges = true;
1176 }
1177 }
1178
1180 {
1182 }
1183
1185 {
1187 }
1188
1190 {
1191 if (m_CommandStack)
1192 {
1194 }
1195 return "";
1196 }
1197
1199 {
1200 if (m_CommandStack)
1201 {
1203 }
1204 return "";
1205 }
1206
1211
1212 // ========================================================================
1213 // Plugin System Implementation
1214 // ========================================================================
1215
1217 {
1218 std::cout << "BlueprintEditor: Initializing plugins..." << std::endl;
1219
1220 // Register all plugins
1221 RegisterPlugin(std::make_unique<BehaviorTreeEditorPlugin>());
1222 RegisterPlugin(std::make_unique<HFSMEditorPlugin>());
1223 RegisterPlugin(std::make_unique<EntityPrefabEditorPlugin>());
1224 RegisterPlugin(std::make_unique<AnimationGraphEditorPlugin>());
1225 RegisterPlugin(std::make_unique<ScriptedEventEditorPlugin>());
1226 RegisterPlugin(std::make_unique<LevelDefinitionEditorPlugin>());
1227 RegisterPlugin(std::make_unique<UIMenuEditorPlugin>());
1228
1229 std::cout << "BlueprintEditor: " << m_Plugins.size() << " plugins registered" << std::endl;
1230 }
1231
1232 void BlueprintEditor::RegisterPlugin(std::unique_ptr<BlueprintEditorPlugin> plugin)
1233 {
1234 std::string type = plugin->GetBlueprintType();
1235 m_Plugins[type] = std::move(plugin);
1236 std::cout << "BlueprintEditor: Registered plugin: " << type << std::endl;
1237 }
1238
1240 {
1241 auto it = m_Plugins.find(type);
1242 if (it != m_Plugins.end())
1243 {
1244 return it->second.get();
1245 }
1246 return nullptr;
1247 }
1248
1250 {
1251 // V2 format: read blueprintType directly
1252 if (blueprint.contains("blueprintType"))
1253 {
1254 std::string type = blueprint["blueprintType"].get<std::string>();
1255 return GetPlugin(type);
1256 }
1257
1258 // V1 format: use heuristic detection
1259 for (auto& pluginPair : m_Plugins)
1260 {
1261 auto& type = pluginPair.first;
1262 auto& plugin = pluginPair.second;
1263 if (plugin->CanHandle(blueprint))
1264 {
1265 return plugin.get();
1266 }
1267 }
1268
1269 return nullptr;
1270 }
1271
1272 // ========================================================================
1273 // Migration System Implementation
1274 // ========================================================================
1275
1276 std::vector<std::string> BlueprintEditor::ScanBlueprintFiles(const std::string& directory)
1277 {
1278 std::vector<std::string> blueprintFiles;
1279
1280 try
1281 {
1282 if (!fs::exists(directory) || !fs::is_directory(directory))
1283 {
1284 std::cerr << "BlueprintEditor: Directory not found: " << directory << std::endl;
1285 return blueprintFiles;
1286 }
1287
1288 for (const auto& entry : fs::recursive_directory_iterator(directory))
1289 {
1290 if (fs::is_regular_file(entry.path()) && entry.path().extension() == ".json")
1291 {
1292 blueprintFiles.push_back(entry.path().string());
1293 }
1294 }
1295 }
1296 catch (const std::exception& e)
1297 {
1298 std::cerr << "BlueprintEditor: Error scanning directory: " << e.what() << std::endl;
1299 }
1300
1301 return blueprintFiles;
1302 }
1303
1305 {
1306 std::cout << "BlueprintEditor: Starting migration..." << std::endl;
1307
1310 int successCount = 0;
1311 int failCount = 0;
1312 int skippedCount = 0;
1313
1314 std::vector<std::string> files = ScanBlueprintFiles(m_AssetRootPath);
1315
1316 for (const auto& path : files)
1317 {
1318 try
1319 {
1322 {
1323 std::cerr << "Failed to load: " << path << std::endl;
1324 failCount++;
1325 continue;
1326 }
1327
1329 bool needsSGMigration = sgMigrator.NeedsMigration(
1331
1333 {
1334 std::cout << "Skipping (already current format): " << path << std::endl;
1335 skippedCount++;
1336 continue;
1337 }
1338
1339 // Create backup before any write.
1340 std::string backupPath = path + ".pre_v5.backup";
1341 try
1342 {
1343 fs::copy_file(path, backupPath, fs::copy_options::overwrite_existing);
1344 }
1345 catch (const std::exception& e)
1346 {
1347 std::cerr << "Failed to create backup for " << path << ": " << e.what() << std::endl;
1348 failCount++;
1349 continue;
1350 }
1351
1352 // Step 1: v1 -> v2 if needed.
1353 if (needsV2Migration)
1354 {
1355 blueprint = v1Migrator.MigrateToV2(blueprint);
1356 std::cout << "[Migration] v1->v2: " << path << std::endl;
1357 }
1358
1359 // Step 2: legacy v2 -> Phase 8 flat-dictionary format if needed.
1360 if (sgMigrator.NeedsMigration(blueprint))
1361 {
1362 blueprint = sgMigrator.Migrate(blueprint);
1363 std::cout << "[Migration] v2->v5 (subgraph): " << path << std::endl;
1364 }
1365
1366 // Save final result.
1367 std::ofstream file(path);
1368 if (!file.is_open())
1369 {
1370 std::cerr << "Failed to open file for writing: " << path << std::endl;
1371 failCount++;
1372 continue;
1373 }
1374
1375 file << blueprint.dump(2);
1376 file.close();
1377
1378 std::cout << "Migrated: " << path << std::endl;
1379 successCount++;
1380 }
1381 catch (const std::exception& e)
1382 {
1383 std::cerr << "Migration failed for " << path << ": " << e.what() << std::endl;
1384 failCount++;
1385 }
1386 }
1387
1388 std::cout << "Migration complete: " << successCount << " success, "
1389 << skippedCount << " skipped, " << failCount << " failed" << std::endl;
1390
1391 // Refresh assets after migration
1392 RefreshAssets();
1393 }
1394
1395 // ========================================================================
1396 // Configuration System Implementation
1397 // ========================================================================
1398
1400 {
1401 try
1402 {
1403 std::ifstream file(configPath);
1404 if (!file.is_open())
1405 {
1406 std::cout << "[BlueprintEditor] Config file not found: " << configPath
1407 << ", creating default config" << std::endl;
1408
1409 // Create default config
1410 m_Config = json::object();
1411 m_Config["version"] = "1.0";
1412 m_Config["editor_mode"] = "standalone";
1413
1414 m_Config["window"] = json::object();
1415 m_Config["window"]["width"] = 1920;
1416 m_Config["window"]["height"] = 1080;
1417 m_Config["window"]["maximized"] = true;
1418
1419 m_Config["panels"] = json::object();
1420 m_Config["panels"]["asset_browser"] = json::object();
1421 m_Config["panels"]["asset_browser"]["visible"] = true;
1422 m_Config["panels"]["asset_browser"]["width"] = 400;
1423 m_Config["panels"]["node_graph"] = json::object();
1424 m_Config["panels"]["node_graph"]["visible"] = true;
1425 m_Config["panels"]["inspector"] = json::object();
1426 m_Config["panels"]["inspector"]["visible"] = true;
1427 m_Config["panels"]["inspector"]["width"] = 400;
1428
1429 m_Config["layout"] = json::object();
1430 m_Config["layout"]["mode"] = "fixed";
1431 m_Config["layout"]["asset_browser_width"] = 400.0f;
1432 m_Config["layout"]["inspector_width"] = 400.0f;
1433 m_Config["layout"]["min_panel_width"] = 200.0f;
1434 m_Config["layout"]["splitter_size"] = 8.0f;
1435
1437 return true;
1438 }
1439
1440 file >> m_Config;
1441 file.close();
1442
1443 std::cout << "[BlueprintEditor] Loaded config from: " << configPath << std::endl;
1444 return true;
1445 }
1446 catch (const std::exception& e)
1447 {
1448 std::cerr << "[BlueprintEditor] Error loading config: " << e.what() << std::endl;
1449 return false;
1450 }
1451 }
1452
1454 {
1455 try
1456 {
1457 std::ofstream file(configPath);
1458 if (!file.is_open())
1459 {
1460 std::cerr << "[BlueprintEditor] Failed to open config file for writing: "
1461 << configPath << std::endl;
1462 return false;
1463 }
1464
1465 file << m_Config.dump(2); // Pretty print with 2-space indent
1466 file.close();
1467
1468 std::cout << "[BlueprintEditor] Saved config to: " << configPath << std::endl;
1469 return true;
1470 }
1471 catch (const std::exception& e)
1472 {
1473 std::cerr << "[BlueprintEditor] Error saving config: " << e.what() << std::endl;
1474 return false;
1475 }
1476 }
1477
1478} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Editor mode and capabilities management.
Phase 8 — Migrates legacy blueprint data to the flat-dictionary subgraph format.
Central manager for the multi-graph tab system.
AssetID LoadTaskGraph(const std::string &path, std::vector< std::string > &outErrors)
Loads a TaskGraphTemplate from path and caches it.
static AssetManager & Get()
Returns the singleton AssetManager instance.
BlueprintEditorPlugin - Base interface for blueprint type plugins Each plugin handles a specific blue...
BlueprintEditor Singleton Backend Manages all business logic, state, and data for the Blueprint Edito...
std::vector< AssetMetadata > SearchAssets(const std::string &query) const
void RegisterPlugin(std::unique_ptr< class BlueprintEditorPlugin > plugin)
void ParseEntityBlueprint(const json &j, AssetMetadata &metadata)
std::vector< std::string > ScanBlueprintFiles(const std::string &directory)
static BlueprintEditor & Instance()
std::map< std::string, std::unique_ptr< class BlueprintEditorPlugin > > m_Plugins
bool LoadBlueprint(const std::string &filepath)
std::string DetectAssetType(const std::string &filepath)
std::shared_ptr< AssetNode > ScanDirectory(const std::string &path)
std::vector< AssetMetadata > GetAssetsByType(const std::string &type) const
Blueprint::CommandStack * m_CommandStack
std::vector< uint64_t > m_RuntimeEntities
void NewBlueprint(const std::string &name, const std::string &description="")
void NotifyEntityDestroyed(uint64_t entityId)
std::string GetNextRedoDescription() const
void SetAssetRootPath(const std::string &path)
std::string GetLastCommandDescription() const
void NotifyEntityCreated(uint64_t entityId)
void ParseHFSM(const json &j, AssetMetadata &metadata)
bool DeleteTemplate(const std::string &templateId)
AssetMetadata GetAssetMetadata(const std::string &filepath)
bool IsAssetValid(const std::string &filepath) const
bool LoadConfig(const std::string &configPath="blueprint_editor_config.json")
void SelectAsset(const std::string &assetPath)
void SetSelectedEntity(uint64_t entityId)
bool SaveConfig(const std::string &configPath="blueprint_editor_config.json")
Blueprint::CommandStack * GetCommandStack()
bool SaveBlueprintAs(const std::string &filepath)
class BlueprintEditorPlugin * GetPlugin(const std::string &type)
void OpenGraphInEditor(const std::string &assetPath)
bool ApplyTemplate(const std::string &templateId)
std::shared_ptr< AssetNode > m_AssetTreeRoot
void ParseAssetMetadata(const std::string &filepath, AssetMetadata &metadata)
bool SaveCurrentAsTemplate(const std::string &name, const std::string &description, const std::string &category)
class BlueprintEditorPlugin * DetectPlugin(const json &blueprint)
std::vector< AssetMetadata > GetAllAssets() const
void CollectAllAssets(const std::shared_ptr< AssetNode > &node, std::vector< AssetMetadata > &assets) const
void ParseBehaviorTree(const json &j, AssetMetadata &metadata)
Blueprint::EntityBlueprint m_CurrentBlueprint
void Update(float deltaTime)
std::map< std::string, AssetMetadata > m_AssetMetadataCache
BlueprintMigrator - Converts v1 blueprints to v2 format Handles automatic position calculation and st...
CommandStack - Manages undo/redo command history Maintains two stacks for undo and redo operations.
std::string GetNextRedoDescription() const
std::string GetLastCommandDescription() const
static EditorContext & Get()
static EntityInspectorManager & Get()
static EnumCatalogManager & Get()
int LoadGraph(const std::string &filepath)
static NodeGraphManager & Get()
Converts legacy blueprint JSON to the Phase 8 subgraph flat-dict format.
static TabManager & Get()
Returns the global singleton instance.
std::string OpenFileInTab(const std::string &filePath)
Opens a file in a new tab.
void Initialize(const std::string &templatesPath="Blueprints/Templates")
std::string GetLastError() const
static TemplateManager & Get()
BlueprintTemplate CreateTemplateFromBlueprint(const json &blueprint, const std::string &name, const std::string &description, const std::string &category, const std::string &author="User")
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
< Provides AssetID and INVALID_ASSET_ID
nlohmann::json json
void WorldBridge_UnregisterTaskCallback()
Unregisters the TaskExecutionBridge so that TaskSystem no longer publishes live state to the editor p...
uint32_t AssetID
Opaque asset identifier: 32-bit FNV-1a hash of the asset file path.
static const AssetID INVALID_ASSET_ID
Sentinel value indicating an invalid / unloaded asset.
std::vector< std::string > components
std::vector< std::string > nodes
BlueprintTemplate - Template metadata and data Stores a complete blueprint that can be reused as a te...
static EntityBlueprint LoadFromFile(const std::string &filepath)
bool SaveToFile(const std::string &filepath) const
static EntityBlueprint FromJson(const json &j)
#define SYSTEM_LOG