Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
ComponentDefinition.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
8ComponentDefinition implementation: Dynamic component parameter storage and JSON parsing
9for entity components. Supports type-safe parameter access and conversions.
10*/
11
12#include "ComponentDefinition.h"
13#include "ParameterSchema.h"
14#include "third_party/nlohmann/json.hpp"
15#include "system/system_utils.h"
16#include <algorithm>
17#include <cctype>
18#include <sstream>
19
20using nlohmann::json;
21
22// Helper function for clamping color values to valid range
23static inline uint8_t ClampColorValue(int value)
24{
25 return static_cast<uint8_t>(value < 0 ? 0 : (value > 255 ? 255 : value));
26}
27
28// Helper function to convert ParameterType enum to string for logging
30{
31 switch (type) {
32 case ComponentParameter::Type::Bool: return "Bool";
33 case ComponentParameter::Type::Int: return "Int";
34 case ComponentParameter::Type::Float: return "Float";
35 case ComponentParameter::Type::String: return "String";
36 case ComponentParameter::Type::Vector2: return "Vector2";
37 case ComponentParameter::Type::Vector3: return "Vector3";
38 case ComponentParameter::Type::Color: return "Color";
39 case ComponentParameter::Type::Array: return "Array";
40 case ComponentParameter::Type::EntityRef: return "EntityRef";
41 default: return "Unknown";
42 }
43}
44
45// ============================================================================
46// ComponentParameter Factory Methods
47// ============================================================================
48
50{
53 param.boolValue = value;
54 return param;
55}
56
58{
61 param.intValue = value;
62 return param;
63}
64
66{
69 param.floatValue = value;
70 return param;
71}
72
74{
77 param.stringValue = value;
78 return param;
79}
80
82{
85 param.vectorValue = Vector(x, y, 0.0f);
86 return param;
87}
88
90{
93 param.vectorValue = Vector(x, y, z);
94 return param;
95}
96
98{
101 param.colorValue = { r, g, b, a };
102 return param;
103}
104
106{
109 param.entityRefValue = entityId;
110 return param;
111}
112
114{
117 // Store array in shared_ptr for efficient copying
118 param.arrayValue = std::make_shared<nlohmann::json>(arrayData);
119 return param;
120}
121
122// ============================================================================
123// ComponentParameter Type Conversion Methods
124// ============================================================================
125
127{
128 switch (type)
129 {
130 case Type::Bool:
131 return boolValue;
132 case Type::Int:
133 return intValue != 0;
134 case Type::Float:
135 return floatValue != 0.0f;
136 case Type::String:
137 {
138 std::string lower = stringValue;
139 std::transform(lower.begin(), lower.end(), lower.begin(),
140 [](unsigned char c) { return std::tolower(c); });
141 return lower == "true" || lower == "1" || lower == "yes";
142 }
143 case Type::EntityRef:
145 default:
146 return false;
147 }
148}
149
151{
152 switch (type)
153 {
154 case Type::Int:
155 return intValue;
156 case Type::Bool:
157 return boolValue ? 1 : 0;
158 case Type::Float:
159 return static_cast<int>(floatValue);
160 case Type::String:
161 try {
162 return std::stoi(stringValue);
163 }
164 catch (...) {
165 return 0;
166 }
167 case Type::EntityRef:
168 return static_cast<int>(entityRefValue);
169 default:
170 return 0;
171 }
172}
173
175{
176 switch (type)
177 {
178 case Type::Float:
179 return floatValue;
180 case Type::Int:
181 return static_cast<float>(intValue);
182 case Type::Bool:
183 return boolValue ? 1.0f : 0.0f;
184 case Type::String:
185 try {
186 return std::stof(stringValue);
187 }
188 catch (...) {
189 return 0.0f;
190 }
191 default:
192 return 0.0f;
193 }
194}
195
197{
198 switch (type)
199 {
200 case Type::String:
201 return stringValue;
202 case Type::Int:
203 return std::to_string(intValue);
204 case Type::Float:
205 return std::to_string(floatValue);
206 case Type::Bool:
207 return boolValue ? "true" : "false";
208 case Type::Vector2:
209 case Type::Vector3:
210 {
211 std::ostringstream oss;
212 oss << "(" << vectorValue.x << ", " << vectorValue.y;
213 if (type == Type::Vector3)
214 oss << ", " << vectorValue.z;
215 oss << ")";
216 return oss.str();
217 }
218 case Type::Color:
219 {
220 std::ostringstream oss;
221 oss << "rgba(" << (int)colorValue.r << ", " << (int)colorValue.g
222 << ", " << (int)colorValue.b << ", " << (int)colorValue.a << ")";
223 return oss.str();
224 }
225 case Type::Array:
226 {
227 // Convert array to JSON string representation
228 if (arrayValue)
229 return arrayValue->dump();
230 else
231 return "[]";
232 }
233 case Type::EntityRef:
234 return std::to_string(entityRefValue);
235 default:
236 return "";
237 }
238}
239
241{
242 switch (type)
243 {
244 case Type::Vector2:
245 case Type::Vector3:
246 return vectorValue;
247 case Type::String:
248 {
249 // Parse string like "(x, y)" or "(x, y, z)"
250 std::string s = stringValue;
251 s.erase(std::remove(s.begin(), s.end(), '('), s.end());
252 s.erase(std::remove(s.begin(), s.end(), ')'), s.end());
253
254 std::istringstream iss(s);
255 std::string token;
257 int idx = 0;
258
259 while (std::getline(iss, token, ',') && idx < 3)
260 {
261 // Trim whitespace
262 token.erase(0, token.find_first_not_of(" \t\n\r"));
263 token.erase(token.find_last_not_of(" \t\n\r") + 1);
264
265 try {
266 float val = std::stof(token);
267 if (idx == 0) result.x = val;
268 else if (idx == 1) result.y = val;
269 else if (idx == 2) result.z = val;
270 idx++;
271 }
272 catch (...) {
273 break;
274 }
275 }
276 return result;
277 }
278 default:
279 return Vector(0.0f, 0.0f, 0.0f);
280 }
281}
282
284{
285 switch (type)
286 {
287 case Type::Color:
288 return colorValue;
289 case Type::String:
290 {
291 // Parse hex color like "#RRGGBB" or "#RRGGBBAA"
292 std::string s = stringValue;
293 if (!s.empty() && s[0] == '#' && (s.length() == 7 || s.length() == 9))
294 {
295 try {
296 unsigned long val = std::stoul(s.substr(1), nullptr, 16);
297 if (s.length() == 7) {
298 return SDL_Color{
299 static_cast<uint8_t>((val >> 16) & 0xFF),
300 static_cast<uint8_t>((val >> 8) & 0xFF),
301 static_cast<uint8_t>(val & 0xFF),
302 255
303 };
304 }
305 else {
306 return SDL_Color{
307 static_cast<uint8_t>((val >> 24) & 0xFF),
308 static_cast<uint8_t>((val >> 16) & 0xFF),
309 static_cast<uint8_t>((val >> 8) & 0xFF),
310 static_cast<uint8_t>(val & 0xFF)
311 };
312 }
313 }
314 catch (...) {
315 SYSTEM_LOG << "Failed to parse color from hex string: " << s << std::endl;
316 }
317 }
318 // Parse rgba like "rgba(r, g, b, a)" or "rgb(r, g, b)"
319 else if (s.find("rgb") == 0)
320 {
321 std::string nums = s.substr(s.find('(') + 1);
322 nums = nums.substr(0, nums.find(')'));
323
324 std::istringstream iss(nums);
325 std::string token;
326 int values[4] = { 0, 0, 0, 255 };
327 int idx = 0;
328
329 while (std::getline(iss, token, ',') && idx < 4)
330 {
331 // Trim whitespace
332 token.erase(0, token.find_first_not_of(" \t\n\r"));
333 token.erase(token.find_last_not_of(" \t\n\r") + 1);
334
335 try {
336 values[idx] = ClampColorValue(std::stoi(token));
337 idx++;
338 }
339 catch (...) {
340 break;
341 }
342 }
343
344 return SDL_Color{
345 static_cast<uint8_t>(values[0]),
346 static_cast<uint8_t>(values[1]),
347 static_cast<uint8_t>(values[2]),
348 static_cast<uint8_t>(values[3])
349 };
350 }
351 return SDL_Color{ 255, 255, 255, 255 };
352 }
353 default:
354 return SDL_Color{ 255, 255, 255, 255 };
355 }
356}
357
359{
360 switch (type)
361 {
362 case Type::EntityRef:
363 return entityRefValue;
364 case Type::Int:
365 return static_cast<EntityID>(intValue);
366 case Type::String:
367 try {
368 return static_cast<EntityID>(std::stoull(stringValue));
369 }
370 catch (...) {
371 return INVALID_ENTITY_ID;
372 }
373 default:
374 return INVALID_ENTITY_ID;
375 }
376}
377
379{
380 // Return empty array if not an array type or arrayValue is null
381 static const nlohmann::json emptyArray = nlohmann::json::array();
382
383 if (type == Type::Array && arrayValue)
384 {
385 return *arrayValue;
386 }
387
388 return emptyArray;
389}
390
391// ============================================================================
392// ComponentDefinition JSON Parsing
393// ============================================================================
394
395// Parse a single parameter with schema-aware type detection
397 const std::string& componentType,
398 const std::string& paramName,
400{
402
403 // Step 1: Try to get schema for this component
405
406 // Step 2: If schema exists, look up the parameter type
407 if (schema != nullptr)
408 {
409 auto paramIt = schema->parameters.find(paramName);
410 if (paramIt != schema->parameters.end())
411 {
413
414 // Force conversion based on schema type
415 switch (schemaEntry.expectedType) {
417 if (jsonValue.is_boolean()) {
419 } else {
420 SYSTEM_LOG << " [WARN] Schema expects Bool for '" << paramName
421 << "', got " << jsonValue.type_name() << std::endl;
423 }
424 break;
425
427 if (jsonValue.is_number()) {
428 // Convert any number to int
429 param = ComponentParameter::FromInt(static_cast<int>(jsonValue.get<double>()));
430 } else {
431 SYSTEM_LOG << " [WARN] Schema expects Int for '" << paramName
432 << "', got " << jsonValue.type_name() << std::endl;
434 }
435 break;
436
438 if (jsonValue.is_number()) {
439 // CRITICAL FIX: Always convert to float
440 param = ComponentParameter::FromFloat(static_cast<float>(jsonValue.get<double>()));
441 } else {
442 SYSTEM_LOG << " [WARN] Schema expects Float for '" << paramName
443 << "', got " << jsonValue.type_name() << std::endl;
445 }
446 break;
447
449 if (jsonValue.is_string()) {
450 param = ComponentParameter::FromString(jsonValue.get<std::string>());
451 } else {
453 }
454 break;
455
457 if (jsonValue.is_object() && jsonValue.contains("x") && jsonValue.contains("y")) {
458 float x = static_cast<float>(jsonValue["x"].get<double>());
459 float y = static_cast<float>(jsonValue["y"].get<double>());
461 } else if (jsonValue.is_array() && jsonValue.size() == 2) {
462 // Support array format: [x, y]
463 float x = jsonValue[0].get<float>();
464 float y = jsonValue[1].get<float>();
466 } else {
467 SYSTEM_LOG << " [WARN] Schema expects Vector2 for '" << paramName
468 << "', got " << jsonValue.type_name() << std::endl;
470 }
471 break;
472
474 if (jsonValue.is_object() && jsonValue.contains("x") && jsonValue.contains("y") && jsonValue.contains("z")) {
475 float x = static_cast<float>(jsonValue["x"].get<double>());
476 float y = static_cast<float>(jsonValue["y"].get<double>());
477 float z = static_cast<float>(jsonValue["z"].get<double>());
479 } else if (jsonValue.is_array() && jsonValue.size() == 3) {
480 // Support array format: [x, y, z]
481 float x = jsonValue[0].get<float>();
482 float y = jsonValue[1].get<float>();
483 float z = jsonValue[2].get<float>();
485 } else {
486 SYSTEM_LOG << " [WARN] Schema expects Vector3 for '" << paramName
487 << "', got " << jsonValue.type_name() << std::endl;
488 param = ComponentParameter::FromVector3(0.0f, 0.0f, 0.0f);
489 }
490 break;
491
493 if (jsonValue.is_string()) {
494 std::string colorStr = jsonValue.get<std::string>();
495 // Parse color using existing AsColor logic
498 param.colorValue = param.AsColor();
499 } else if (jsonValue.is_object() && jsonValue.contains("r") && jsonValue.contains("g") && jsonValue.contains("b")) {
500 // Support object format: {"r": 255, "g": 128, "b": 64, "a": 255}
501 uint8_t r = jsonValue["r"].is_number() ? ClampColorValue(jsonValue["r"].get<int>()) : 255;
502 uint8_t g = jsonValue["g"].is_number() ? ClampColorValue(jsonValue["g"].get<int>()) : 255;
503 uint8_t b = jsonValue["b"].is_number() ? ClampColorValue(jsonValue["b"].get<int>()) : 255;
504 uint8_t a = jsonValue.contains("a") && jsonValue["a"].is_number() ?
505 ClampColorValue(jsonValue["a"].get<int>()) : 255;
507 } else if (jsonValue.is_array() && jsonValue.size() >= 3 && jsonValue.size() <= 4) {
508 // Support array format: [r, g, b] or [r, g, b, a]
512 uint8_t a = jsonValue.size() == 4 ? ClampColorValue(jsonValue[3].get<int>()) : 255;
514 } else {
515 SYSTEM_LOG << " [WARN] Schema expects Color for '" << paramName
516 << "', got " << jsonValue.type_name() << std::endl;
519 param.colorValue = param.AsColor();
520 }
521 break;
522
524 if (jsonValue.is_string()) {
525 try {
526 EntityID entityId = std::stoull(jsonValue.get<std::string>());
528 }
529 catch (...) {
530 SYSTEM_LOG << " [WARN] Failed to parse EntityRef from string for '" << paramName << "'" << std::endl;
532 }
533 } else if (jsonValue.is_number()) {
534 // Use double conversion to avoid MSVC linkage issues with int64_t types
535 param = ComponentParameter::FromEntityRef(static_cast<EntityID>(jsonValue.get<double>()));
536 } else {
537 SYSTEM_LOG << " [WARN] Schema expects EntityRef for '" << paramName
538 << "', got " << jsonValue.type_name() << std::endl;
540 }
541 break;
542
544 // Array type: store as-is (do NOT coerce into Vector unless schema expects Vector2/Vector3)
545 if (jsonValue.is_array()) {
547 } else {
548 SYSTEM_LOG << " [WARN] Schema expects Array for '" << paramName
549 << "', got " << jsonValue.type_name() << std::endl;
550 param = ComponentParameter::FromArray(nlohmann::json::array());
551 }
552 break;
553
554 default:
555 SYSTEM_LOG << " [ERROR] Unknown schema type for '" << paramName << "'" << std::endl;
556 break;
557 }
558
559 #ifdef DEBUG_PARAMETER_PARSING
560 SYSTEM_LOG << " [SCHEMA-AWARE] " << paramName << " -> "
561 << ParameterTypeToString(schemaEntry.expectedType)
562 << " (from schema)" << std::endl;
563 #endif
564
565 return param;
566 }
567 }
568
569 // FALLBACK: No schema found, use improved heuristics
570 #ifdef DEBUG_PARAMETER_PARSING
571 SYSTEM_LOG << " [WARN] No schema for '" << componentType << "." << paramName << "', using heuristics" << std::endl;
572 #endif
573
574 if (jsonValue.is_boolean())
575 {
577 }
578 else if (jsonValue.is_number())
579 {
580 // IMPROVED HEURISTIC: Prefer Float over Int for all numbers
581 // This matches most common use cases (speed, mass, positions, etc.)
582 double numValue = jsonValue.get<double>();
583 param = ComponentParameter::FromFloat(static_cast<float>(numValue));
584
585 #ifdef DEBUG_PARAMETER_PARSING
586 SYSTEM_LOG << " -> Inferred as Float (" << numValue << ")" << std::endl;
587 #endif
588 }
589 else if (jsonValue.is_string())
590 {
591 std::string strValue = jsonValue.get<std::string>();
592
593 // Check if it's a color (hex or rgba format)
594 if ((!strValue.empty() && strValue[0] == '#' && (strValue.length() == 7 || strValue.length() == 9)) ||
595 strValue.find("rgb") == 0)
596 {
599 param.colorValue = param.AsColor();
600 }
601 // Check if key hints at entity reference
602 else if (paramName.find("entity") != std::string::npos ||
603 paramName.find("Entity") != std::string::npos ||
604 paramName.find("ref") != std::string::npos ||
605 paramName.find("Ref") != std::string::npos)
606 {
607 try {
608 EntityID entityId = std::stoull(strValue);
610 }
611 catch (...) {
613 }
614 }
615 else
616 {
618 }
619 }
620 else if (jsonValue.is_object())
621 {
622 // Try Vector2/Vector3
623 if (jsonValue.contains("x") && jsonValue.contains("y") && jsonValue.contains("z"))
624 {
625 float x = static_cast<float>(jsonValue["x"].get<double>());
626 float y = static_cast<float>(jsonValue["y"].get<double>());
627 float z = static_cast<float>(jsonValue["z"].get<double>());
629 }
630 else if (jsonValue.contains("x") && jsonValue.contains("y"))
631 {
632 float x = static_cast<float>(jsonValue["x"].get<double>());
633 float y = static_cast<float>(jsonValue["y"].get<double>());
635 }
636 // Check for color object
637 else if (jsonValue.contains("r") && jsonValue.contains("g") && jsonValue.contains("b"))
638 {
639 uint8_t r = jsonValue["r"].is_number() ? ClampColorValue(jsonValue["r"].get<int>()) : 255;
640 uint8_t g = jsonValue["g"].is_number() ? ClampColorValue(jsonValue["g"].get<int>()) : 255;
641 uint8_t b = jsonValue["b"].is_number() ? ClampColorValue(jsonValue["b"].get<int>()) : 255;
642 uint8_t a = jsonValue.contains("a") && jsonValue["a"].is_number() ?
643 ClampColorValue(jsonValue["a"].get<int>()) : 255;
644
646 }
647 else
648 {
649 // Store as JSON string
651 }
652 }
653 else if (jsonValue.is_array())
654 {
655 // Handle arrays based on size to avoid ambiguity
656 // IMPORTANT: Only coerce to Vector2/Vector3/Color if schema is not available
657 // Otherwise, preserve array structure for proper Array type parameters
658 if (jsonValue.size() == 2 && jsonValue[0].is_number())
659 {
660 // Could be Vector2 - but check if paramName suggests array (e.g., "waypoints", "path")
661 if (paramName.find("waypoint") != std::string::npos ||
662 paramName.find("path") != std::string::npos ||
663 paramName.find("points") != std::string::npos ||
664 paramName.find("Path") != std::string::npos)
665 {
666 // Preserve as array
668 }
669 else
670 {
671 // Treat as Vector2
672 float x = jsonValue[0].get<float>();
673 float y = jsonValue[1].get<float>();
675 }
676 }
677 else if (jsonValue.size() == 3 && jsonValue[0].is_number())
678 {
679 // Ambiguous: could be Vector3 or RGB color
680 // Use heuristic: if values are in typical color range (0-255), treat as color
681 double val0 = jsonValue[0].get<double>();
682 double val1 = jsonValue[1].get<double>();
683 double val2 = jsonValue[2].get<double>();
684
685 // If all values are in [0, 255] range and are integers, likely a color
686 bool looksLikeColor = (val0 >= 0 && val0 <= 255 && val0 == (int)val0) &&
687 (val1 >= 0 && val1 <= 255 && val1 == (int)val1) &&
688 (val2 >= 0 && val2 <= 255 && val2 == (int)val2);
689
690 if (looksLikeColor)
691 {
693 ClampColorValue((int)val0),
694 ClampColorValue((int)val1),
695 ClampColorValue((int)val2),
696 255
697 );
698 }
699 else
700 {
701 // Treat as Vector3
702 float x = jsonValue[0].get<float>();
703 float y = jsonValue[1].get<float>();
704 float z = jsonValue[2].get<float>();
706 }
707 }
708 else if (jsonValue.size() == 4 && jsonValue[0].is_number())
709 {
710 // 4 elements: likely RGBA color
716 }
717 else
718 {
719 // Non-standard size or non-numeric: preserve as Array type
721 }
722 }
723 else
724 {
725 // Unknown type, store as string
727 }
728
729 return param;
730}
731
733{
735
736 try
737 {
738 // Extract component type
739 if (jsonObj.contains("type") && jsonObj["type"].is_string())
740 {
741 def.componentType = jsonObj["type"].get<std::string>();
742 }
743 else
744 {
745 SYSTEM_LOG << "Warning: Component definition missing 'type' field" << std::endl;
746 return def;
747 }
748
749 // Check if there's a "properties" object - if so, parse its contents instead of top-level fields
750 const json& fieldsToIterate = (jsonObj.contains("properties") && jsonObj["properties"].is_object())
751 ? jsonObj["properties"]
752 : jsonObj;
753
754 // Parse all fields as parameters using schema-aware parsing
755 for (auto it = fieldsToIterate.begin(); it != fieldsToIterate.end(); ++it)
756 {
757 const std::string& key = it.key();
758
759 // Skip the type field (normally at top level, but safeguard against it in properties)
760 if (key == "type")
761 continue;
762
763 const auto& value = it.value();
764
765 // Use schema-aware parsing
767
768 def.parameters[key] = param;
769 }
770 }
771 catch (const std::exception& e)
772 {
773 SYSTEM_LOG << "Error parsing component JSON: " << e.what() << std::endl;
774 }
775
776 return def;
777}
778
779// ============================================================================
780// ComponentDefinition Parameter Access
781// ============================================================================
782
783bool ComponentDefinition::HasParameter(const std::string& name) const
784{
785 return parameters.find(name) != parameters.end();
786}
787
788const ComponentParameter* ComponentDefinition::GetParameter(const std::string& name) const
789{
790 auto it = parameters.find(name);
791 if (it != parameters.end())
792 return &(it->second);
793 return nullptr;
794}
nlohmann::json json
std::string ParameterTypeToString(ComponentParameter::Type type)
static uint8_t ClampColorValue(int value)
ComponentParameter ParseParameterWithSchema(const std::string &componentType, const std::string &paramName, const nlohmann::json &jsonValue)
ComponentParameter ParseParameterWithSchema(const std::string &componentType, const std::string &paramName, const nlohmann::json &jsonValue)
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
std::uint64_t EntityID
Definition ECS_Entity.h:21
const EntityID INVALID_ENTITY_ID
Definition ECS_Entity.h:23
static ParameterSchemaRegistry & GetInstance()
const ComponentSchema * GetComponentSchema(const std::string &componentType) const
float z
Definition vector.h:27
float y
Definition vector.h:27
float x
Definition vector.h:27
nlohmann::json json
const ComponentParameter * GetParameter(const std::string &name) const
std::map< std::string, ComponentParameter > parameters
bool HasParameter(const std::string &name) const
static ComponentDefinition FromJSON(const nlohmann::json &jsonObj)
std::shared_ptr< nlohmann::json > arrayValue
EntityID AsEntityRef() const
static ComponentParameter FromBool(bool value)
static ComponentParameter FromFloat(float value)
static ComponentParameter FromVector2(float x, float y)
static ComponentParameter FromEntityRef(EntityID entityId)
static ComponentParameter FromVector3(float x, float y, float z)
static ComponentParameter FromColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a=255)
const nlohmann::json & AsArray() const
std::string AsString() const
static ComponentParameter FromInt(int value)
static ComponentParameter FromArray(const nlohmann::json &arrayData)
SDL_Color AsColor() const
static ComponentParameter FromString(const std::string &value)
std::map< std::string, ParameterSchemaEntry > parameters
#define SYSTEM_LOG