Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
BehaviorTreeDependencyScanner.cpp
Go to the documentation of this file.
1/*
2Olympe Engine V2 - 2025
3Nicolas Chereau
4nchereau@gmail.com
5
6This file is part of Olympe Engine V2.
7
8BehaviorTreeDependencyScanner implementation: Scans prefabs and levels to extract
9behavior tree dependencies for automatic loading.
10*/
11
13#include "../PrefabScanner.h"
14#include "../prefabfactory.h"
15#include "../third_party/nlohmann/json.hpp"
16#include <iostream>
17#include <algorithm>
18#include <cctype>
19
20// Generate consistent hash from string path using FNV-1a algorithm
22{
23 // FNV-1a hash - simple but consistent hash function
24 uint32_t hash = 2166136261u; // FNV-1a offset basis
25
26 for (char c : treePath)
27 {
28 hash ^= static_cast<uint32_t>(c);
29 hash *= 16777619u; // FNV-1a prime
30 }
31
32 return hash;
33}
34
35// Validate path to prevent directory traversal attacks
36static bool IsValidBehaviorTreePath(const std::string& treePath)
37{
38 // Maximum path length to prevent buffer issues downstream
39 const size_t MAX_PATH_LENGTH = 512;
40
41 if (treePath.empty() || treePath.length() > MAX_PATH_LENGTH)
42 return false;
43
44 // Check for directory traversal attempts
45 if (treePath.find("..") != std::string::npos)
46 return false;
47
48 // Must start with valid prefix (prevent absolute paths)
49 if (treePath[0] == '/' || treePath[0] == '\\')
50 return false;
51
52 // Check for path prefix - should start with "Blueprints/"
53 if (treePath.find("Blueprints/") != 0)
54 return false;
55
56 // Basic character whitelist for paths (alphanumeric, /, _, -, .)
57 for (char c : treePath)
58 {
59 if (!std::isalnum(c) && c != '/' && c != '_' && c != '-' && c != '.')
60 return false;
61 }
62
63 return true;
64}
65
66std::vector<BehaviorTreeDependencyScanner::BTDependency>
68{
69 std::vector<BTDependency> dependencies;
70
71 // Iterate through all components in the prefab
72 for (const auto& compDef : prefab.components)
73 {
74 if (compDef.componentType == "BehaviorTreeRuntime_data" ||
75 compDef.componentType == "BehaviorTreeRuntime")
76 {
77 // Look for treePath property
78 const auto* treePathParam = compDef.GetParameter("AITreePath");
80 {
81 std::string treePath = treePathParam->AsString();
82
83 // Validate path for security
84 if (!IsValidBehaviorTreePath(treePath))
85 {
86 std::cerr << "[BTDepScanner] WARNING: Invalid or unsafe BT path rejected: "
87 << treePath << " (in prefab: " << prefab.prefabName << ")\n";
88 continue;
89 }
90
91 uint32_t treeId = GenerateTreeIdFromPath(treePath);
92 dependencies.emplace_back(treePath, treeId);
93
94 std::cout << "[BTDepScanner] Found BT dependency: " << treePath
95 << " (ID=" << treeId << ")\n";
96 }
97 }
98 }
99
100 return dependencies;
101}
102
103std::vector<BehaviorTreeDependencyScanner::BTDependency>
105{
106 std::vector<BTDependency> allDependencies;
107 std::set<std::string> uniquePaths; // Prevent duplicates
108
109 // Limit number of prefabs to prevent resource exhaustion
110 const size_t MAX_PREFABS = 5000;
111 if (prefabNames.size() > MAX_PREFABS)
112 {
113 std::cerr << "[BTDepScanner] ERROR: Too many prefabs to scan (" << prefabNames.size()
114 << " > " << MAX_PREFABS << "). Possible DoS attempt.\n";
115 return allDependencies;
116 }
117
118 std::cout << "[BTDepScanner] Scanning " << prefabNames.size() << " prefabs for BT dependencies...\n";
119
120 for (const auto& prefabName : prefabNames)
121 {
123 if (!prefab)
124 {
125 std::cerr << "[BTDepScanner] Warning: Prefab not found: " << prefabName << "\n";
126 continue;
127 }
128
129 auto deps = ScanPrefab(*prefab);
130 for (const auto& dep : deps)
131 {
132 // Only add unique paths
133 if (uniquePaths.find(dep.treePath) == uniquePaths.end())
134 {
135 uniquePaths.insert(dep.treePath);
136 allDependencies.push_back(dep);
137 }
138 }
139 }
140
141 std::cout << "[BTDepScanner] Found " << allDependencies.size() << " unique BT dependencies\n";
142 return allDependencies;
143}
144
145// Validate prefab name to prevent directory traversal
146static bool IsValidPrefabName(const std::string& prefabName)
147{
148 const size_t MAX_PREFAB_NAME_LENGTH = 256;
149
150 if (prefabName.empty() || prefabName.length() > MAX_PREFAB_NAME_LENGTH)
151 return false;
152
153 // Check for path traversal attempts
154 if (prefabName.find("..") != std::string::npos)
155 return false;
156
157 // Should not contain path separators (prefabs are just names, not paths)
158 if (prefabName.find('/') != std::string::npos || prefabName.find('\\') != std::string::npos)
159 return false;
160
161 // Basic character whitelist (alphanumeric, underscore, hyphen)
162 for (char c : prefabName)
163 {
164 if (!std::isalnum(c) && c != '_' && c != '-')
165 return false;
166 }
167
168 return true;
169}
170
172{
173 std::set<std::string> prefabNames;
174
175 std::cout << "[BTDepScanner] Extracting prefab types from level...\n";
176
177 // Parse layers in the level
178 if (!levelJson.contains("layers") || !levelJson["layers"].is_array())
179 {
180 std::cerr << "[BTDepScanner] Warning: No 'layers' array in level JSON\n";
181 return prefabNames;
182 }
183
184 // Limit number of layers to prevent resource exhaustion
185 const size_t MAX_LAYERS = 1000;
186 const auto& layers = levelJson["layers"];
187 if (layers.size() > MAX_LAYERS)
188 {
189 std::cerr << "[BTDepScanner] ERROR: Too many layers in level JSON (" << layers.size()
190 << " > " << MAX_LAYERS << "). Possible DoS attempt.\n";
191 return prefabNames;
192 }
193
194 for (const auto& layer : layers)
195 {
196 // Look for object layers (where entities are defined)
197 std::string layerType = layer.value("type", "");
198
199 if (layerType == "objectgroup")
200 {
201 if (layer.contains("objects") && layer["objects"].is_array())
202 {
203 const auto& objects = layer["objects"];
204
205 // Limit number of objects per layer to prevent resource exhaustion
206 const size_t MAX_OBJECTS_PER_LAYER = 10000;
207 if (objects.size() > MAX_OBJECTS_PER_LAYER)
208 {
209 std::cerr << "[BTDepScanner] WARNING: Too many objects in layer (" << objects.size()
210 << " > " << MAX_OBJECTS_PER_LAYER << "). Skipping layer to prevent DoS.\n";
211 continue;
212 }
213
214 for (const auto& obj : objects)
215 {
216 // Extract prefab type (Tiled 1.8 and earlier use "type")
217 if (obj.contains("type") && obj["type"].is_string())
218 {
219 std::string prefabType = obj["type"].get<std::string>();
220
221 // Validate prefab name for security
222 if (IsValidPrefabName(prefabType))
223 {
224 prefabNames.insert(prefabType);
225 }
226 else if (!prefabType.empty())
227 {
228 std::cerr << "[BTDepScanner] WARNING: Invalid prefab name rejected: "
229 << prefabType << "\n";
230 }
231 }
232
233 // Tiled 1.9+ uses "class" instead of "type"
234 if (obj.contains("class") && obj["class"].is_string())
235 {
236 std::string prefabClass = obj["class"].get<std::string>();
237
238 // Validate prefab name for security
240 {
241 prefabNames.insert(prefabClass);
242 }
243 else if (!prefabClass.empty())
244 {
245 std::cerr << "[BTDepScanner] WARNING: Invalid prefab name rejected: "
246 << prefabClass << "\n";
247 }
248 }
249 }
250 }
251 }
252 }
253
254 std::cout << "[BTDepScanner] Extracted " << prefabNames.size() << " unique prefab types\n";
255 return prefabNames;
256}
static bool IsValidPrefabName(const std::string &prefabName)
static bool IsValidBehaviorTreePath(const std::string &treePath)
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
static uint32_t GenerateTreeIdFromPath(const std::string &treePath)
static std::vector< BTDependency > ScanPrefab(const PrefabBlueprint &prefab)
static std::vector< BTDependency > ScanPrefabs(const std::vector< std::string > &prefabNames)
static std::set< std::string > ExtractPrefabsFromLevel(const nlohmann::json &levelJson)
const PrefabRegistry & GetPrefabRegistry() const
Get the cached prefab registry.
static PrefabFactory & Get()
Get singleton instance.
const PrefabBlueprint * Find(const std::string &name) const
nlohmann::json json