Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
SubgraphMigrator.cpp
Go to the documentation of this file.
1/**
2 * @file SubgraphMigrator.cpp
3 * @brief Phase 8 — SubgraphMigrator implementation.
4 *
5 * See SubgraphMigrator.h for the full API contract.
6 */
7
8#include "SubgraphMigrator.h"
9
10#include <algorithm>
11#include <iostream>
12#include <string>
13#include <vector>
14
16
17namespace Olympe
18{
19
20// ============================================================================
21// Detection helpers
22// ============================================================================
23
25{
26 // Must have a "data" section to be a recognised blueprint.
27 if (!blueprint.is_object() || !blueprint.contains("data"))
28 return false;
29
30 const json& data = blueprint["data"];
31 if (!data.is_object())
32 return false;
33
34 // Legacy BehaviorTree: data.nodes exists directly.
35 if (data.contains("nodes") && data["nodes"].is_array())
36 return true;
37
38 // Legacy HFSM: data.states exists directly.
39 if (data.contains("states") && data["states"].is_array())
40 return true;
41
42 return false;
43}
44
46{
47 if (!blueprint.is_object() || !blueprint.contains("data"))
48 return false;
49
50 const json& data = blueprint["data"];
51 return data.is_object() && data.contains("rootGraph");
52}
53
54// ============================================================================
55// Migration
56// ============================================================================
57
59{
61 {
62 std::cout << "[SubgraphMigrator] Blueprint is already in new format — skipping migration.\n";
63 return blueprint;
64 }
65
67 {
68 std::cout << "[SubgraphMigrator] Blueprint does not require migration.\n";
69 return blueprint;
70 }
71
72 std::cout << "[SubgraphMigrator] Migrating blueprint to flat-dictionary subgraph format...\n";
73
74 json result = blueprint;
75 MigrateDataSection(result);
76
77 // Bump schema_version to 5 to signal the new format.
78 result["schema_version"] = 5;
79
80 std::cout << "[SubgraphMigrator] Migration complete.\n";
81 return result;
82}
83
85{
86 const json& data = blueprint["data"];
87
88 // Build the rootGraph object from whatever is in data right now.
89 json rootGraph = json::object();
90
91 // --- BehaviorTree fields ---
92 if (data.contains("nodes"))
93 rootGraph["nodes"] = data["nodes"];
94 if (data.contains("links"))
95 rootGraph["links"] = data["links"];
96 if (data.contains("rootNodeId"))
97 rootGraph["rootNodeId"] = data["rootNodeId"];
98
99 // --- HFSM fields ---
100 if (data.contains("states"))
101 rootGraph["states"] = data["states"];
102 if (data.contains("transitions"))
103 rootGraph["transitions"] = data["transitions"];
104 if (data.contains("initialState"))
105 rootGraph["initialState"] = data["initialState"];
106
107 // Build a new clean data section containing only rootGraph and subgraphs.
108 json newData = json::object();
109 newData["rootGraph"] = rootGraph;
110 newData["subgraphs"] = json::object();
111
112 blueprint["data"] = newData;
113}
114
115// ============================================================================
116// Subgraph helpers
117// ============================================================================
118
120 const std::string& uuid,
121 const std::string& blueprintType)
122{
123 json sg;
124 sg["uuid"] = uuid;
125 sg["name"] = name;
126 sg["blueprintType"] = blueprintType;
127 sg["inputPins"] = json::array();
128 sg["outputPins"] = json::array();
129
130 if (blueprintType == "HFSM")
131 {
132 sg["states"] = json::array();
133 sg["transitions"] = json::array();
134 sg["initialState"] = "";
135 }
136 else
137 {
138 // Default: BehaviorTree layout
139 sg["nodes"] = json::array();
140 sg["links"] = json::array();
141 }
142
143 return sg;
144}
145
146// ============================================================================
147// Validation
148// ============================================================================
149
151 std::string& outError)
152{
153 if (!blueprint.is_object() || !blueprint.contains("data"))
154 {
155 outError = "Missing 'data' section.";
156 return false;
157 }
158
159 const json& data = blueprint["data"];
160 if (!data.contains("rootGraph") || !data.contains("subgraphs"))
161 {
162 outError = "Blueprint is not in new subgraph format (missing rootGraph or subgraphs).";
163 return false;
164 }
165
166 const json& subgraphs = data["subgraphs"];
167 if (!subgraphs.is_object())
168 {
169 outError = "data.subgraphs must be a JSON object.";
170 return false;
171 }
172
173 // 1. Verify every referenced UUID exists.
174 // Collect refs from rootGraph first.
175 std::vector<std::string> rootRefs = CollectSubgraphRefs(data["rootGraph"]);
176 for (const auto& ref : rootRefs)
177 {
178 if (!subgraphs.contains(ref))
179 {
180 outError = "rootGraph references unknown subgraph UUID: " + ref;
181 return false;
182 }
183 }
184
185 // Collect refs from each subgraph and verify.
186 for (auto it = subgraphs.begin(); it != subgraphs.end(); ++it)
187 {
188 std::vector<std::string> refs = CollectSubgraphRefs(it.value());
189 for (const auto& ref : refs)
190 {
191 if (!subgraphs.contains(ref))
192 {
193 outError = "Subgraph '" + it.key() +
194 "' references unknown subgraph UUID: " + ref;
195 return false;
196 }
197 }
198 }
199
200 // 2. Detect circular dependencies via DFS.
201 for (auto it = subgraphs.begin(); it != subgraphs.end(); ++it)
202 {
203 std::vector<std::string> visited;
204 std::vector<std::string> inStack;
205 if (HasCycle(it.key(), subgraphs, visited, inStack))
206 {
207 outError = "Circular subgraph dependency detected starting from: " + it.key();
208 return false;
209 }
210 }
211
212 return true;
213}
214
215// ============================================================================
216// Private helpers
217// ============================================================================
218
219bool SubgraphMigrator::HasCycle(const std::string& start,
220 const json& subgraphs,
221 std::vector<std::string>& visited,
222 std::vector<std::string>& inStack)
223{
224 // Already fully explored — no cycle through this node.
225 if (std::find(visited.begin(), visited.end(), start) != visited.end())
226 return false;
227
228 // In the current DFS stack — cycle found.
229 if (std::find(inStack.begin(), inStack.end(), start) != inStack.end())
230 return true;
231
232 inStack.push_back(start);
233
234 if (subgraphs.contains(start))
235 {
236 std::vector<std::string> refs = CollectSubgraphRefs(subgraphs[start]);
237 for (const auto& ref : refs)
238 {
240 return true;
241 }
242 }
243
244 inStack.erase(std::find(inStack.begin(), inStack.end(), start));
245 visited.push_back(start);
246 return false;
247}
248
250{
251 std::vector<std::string> refs;
252 if (!graphObj.is_object())
253 return refs;
254
255 // Look in "nodes" array (BehaviorTree format).
256 if (graphObj.contains("nodes") && graphObj["nodes"].is_array())
257 {
258 for (const auto& node : graphObj["nodes"])
259 {
260 if (node.is_object() && node.contains("subgraphUUID"))
261 {
262 const auto& uuid = node["subgraphUUID"];
263 if (uuid.is_string() && !uuid.get<std::string>().empty())
264 refs.push_back(uuid.get<std::string>());
265 }
266 }
267 }
268
269 // Look in "states" array (HFSM format).
270 if (graphObj.contains("states") && graphObj["states"].is_array())
271 {
272 for (const auto& state : graphObj["states"])
273 {
274 if (state.is_object() && state.contains("subgraphUUID"))
275 {
276 const auto& uuid = state["subgraphUUID"];
277 if (uuid.is_string() && !uuid.get<std::string>().empty())
278 refs.push_back(uuid.get<std::string>());
279 }
280 }
281 }
282
283 return refs;
284}
285
286} // namespace Olympe
nlohmann::json json
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Phase 8 — Migrates legacy blueprint data to the flat-dictionary subgraph format.
static bool HasCycle(const std::string &start, const nlohmann::json &subgraphs, std::vector< std::string > &visited, std::vector< std::string > &inStack)
void MigrateDataSection(nlohmann::json &blueprint) const
bool NeedsMigration(const nlohmann::json &blueprint) const
Returns true when the blueprint uses the legacy format (data.nodes exists at the top level of the dat...
static nlohmann::json MakeEmptySubgraph(const std::string &name, const std::string &uuid, const std::string &blueprintType)
Creates an empty subgraph definition JSON object.
bool IsNewFormat(const nlohmann::json &blueprint) const
Returns true when the blueprint is already in the Phase 8 format (data.rootGraph exists).
static bool ValidateSubgraphReferences(const nlohmann::json &blueprint, std::string &outError)
Validates that every subgraphUUID referenced by SubGraph nodes actually exists in data....
nlohmann::json Migrate(const nlohmann::json &blueprint) const
Migrates a legacy blueprint to the flat-dictionary format.
static std::vector< std::string > CollectSubgraphRefs(const nlohmann::json &graphObj)
< Provides AssetID and INVALID_ASSET_ID
nlohmann::json json
nlohmann::json json