Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
TiledLevelLoader.cpp
Go to the documentation of this file.
1/*
2 * TiledLevelLoader.cpp - Main loader implementation
3 */
4
5#include "../include/TiledLevelLoader.h"
6#include "../include/TiledDecoder.h"
7#include "../include/TilesetCache.h"
8#include "../include/TilesetParser.h"
9#include "../../system/system_utils.h"
10#include <fstream>
11#include <sstream>
12#include "../../third_party/nlohmann/json.hpp"
13#include "../third_party/tinyxml2/tinyxml2.h"
14
15namespace Olympe {
16namespace Tiled {
17
21
25
26 bool TiledLevelLoader::LoadFromFile(const std::string& filepath, TiledMap& outMap)
27 {
28 lastError_.clear();
29
30 SYSTEM_LOG << "TiledLevelLoader: Loading map from " << filepath << std::endl;
31
32 // Detect format by extension
33 size_t dotPos = filepath.find_last_of('.');
34 if (dotPos == std::string::npos) {
35 lastError_ = "No file extension in " + filepath;
36 SYSTEM_LOG << "TiledLevelLoader: " << lastError_ << std::endl;
37 return false;
38 }
39
40 std::string ext = filepath.substr(dotPos);
41 std::string mapDir = GetDirectory(filepath);
42
43 bool success = false;
44
45 if (ext == ".tmx") {
46 // Parse TMX (XML format)
47 tinyxml2::XMLDocument doc;
48 if (doc.LoadFile(filepath.c_str()) != tinyxml2::XML_SUCCESS) {
49 lastError_ = "Failed to load TMX file: " + filepath;
50 SYSTEM_LOG << "TiledLevelLoader: " << lastError_ << std::endl;
51 return false;
52 }
53
54 success = ParseMapXML(&doc, outMap);
55 }
56 else if (ext == ".tmj" || ext == ".json") {
57 // Parse TMJ (JSON format)
58 std::string content;
59 if (!ReadFile(filepath, content)) {
60 lastError_ = "Failed to read file: " + filepath;
61 return false;
62 }
63
64 json j;
65 try {
66 j = json::parse(content);
67 } catch (const std::exception& e) {
68 lastError_ = std::string("JSON parse error: ") + e.what();
69 SYSTEM_LOG << "TiledLevelLoader: " << lastError_ << std::endl;
70 return false;
71 }
72
73 success = ParseMap(j, outMap);
74 }
75 else {
76 lastError_ = "Unknown map format: " + ext + " (expected .tmx, .tmj, or .json)";
77 SYSTEM_LOG << "TiledLevelLoader: " << lastError_ << std::endl;
78 return false;
79 }
80
81 if (!success) {
82 SYSTEM_LOG << "TiledLevelLoader: Failed to parse map" << std::endl;
83 return false;
84 }
85
86 // Load external tilesets
87 for (auto& tileset : outMap.tilesets) {
88 if (!tileset.source.empty()) {
89 std::string tilesetPath = ResolvePath(mapDir, tileset.source);
90 if (!LoadExternalTileset(tilesetPath, tileset)) {
91 lastError_ = "Failed to load external tileset: " + tilesetPath;
92 SYSTEM_LOG << "TiledLevelLoader: " << lastError_ << std::endl;
93 return false;
94 }
95 }
96 }
97
98 // Calculate lastgid for all tilesets
99 outMap.CalculateAllLastGids();
100
101 // Log tileset information
102 for (const auto& tileset : outMap.tilesets) {
103 SYSTEM_LOG << "TiledLevelLoader: Tileset '" << tileset.name
104 << "' - firstgid=" << tileset.firstgid
105 << ", lastgid=" << tileset.lastgid
106 << ", tilecount=" << tileset.tilecount << std::endl;
107 }
108
109 SYSTEM_LOG << "TiledLevelLoader: Successfully loaded map" << std::endl;
110 return true;
111 }
112
114 {
115 map.version = GetInt(j, "version", 1);
116 map.tiledversion = GetString(j, "tiledversion");
117 map.type = GetString(j, "type", "map");
118
119 // Parse orientation
120 std::string orientStr = GetString(j, "orientation", "orthogonal");
121 if (orientStr == "orthogonal") map.orientation = MapOrientation::Orthogonal;
122 else if (orientStr == "isometric") map.orientation = MapOrientation::Isometric;
123 else if (orientStr == "staggered") map.orientation = MapOrientation::Staggered;
124 else if (orientStr == "hexagonal") map.orientation = MapOrientation::Hexagonal;
125
126 // Parse render order
127 std::string renderStr = GetString(j, "renderorder", "right-down");
128 if (renderStr == "right-down") map.renderorder = RenderOrder::RightDown;
129 else if (renderStr == "right-up") map.renderorder = RenderOrder::RightUp;
130 else if (renderStr == "left-down") map.renderorder = RenderOrder::LeftDown;
131 else if (renderStr == "left-up") map.renderorder = RenderOrder::LeftUp;
132
133 map.compressionlevel = GetInt(j, "compressionlevel", -1);
134 map.width = GetInt(j, "width");
135 map.height = GetInt(j, "height");
136 map.tilewidth = GetInt(j, "tilewidth");
137 map.tileheight = GetInt(j, "tileheight");
138 map.infinite = GetBool(j, "infinite", false);
139 map.backgroundcolor = GetString(j, "backgroundcolor");
140 map.nextlayerid = GetInt(j, "nextlayerid", 1);
141 map.nextobjectid = GetInt(j, "nextobjectid", 1);
142
143 // Parse tilesets
144 if (HasKey(j, "tilesets") && j["tilesets"].is_array()) {
145 for (size_t i = 0; i < j["tilesets"].size(); ++i) {
146 const json& tsJson = j["tilesets"][i];
147 TiledTileset tileset;
148 if (ParseTileset(tsJson, tileset, "")) {
149 map.tilesets.push_back(tileset);
150 }
151 }
152 }
153
154 // Parse layers
155 if (HasKey(j, "layers") && j["layers"].is_array()) {
156 for (size_t i = 0; i < j["layers"].size(); ++i) {
157 const json& layerJson = j["layers"][i];
158 std::shared_ptr<TiledLayer> layer;
159 if (ParseLayer(layerJson, layer)) {
160 map.layers.push_back(layer);
161 }
162 }
163 }
164
165 // Parse properties
166 if (HasKey(j, "properties")) {
167 ParseProperties(j["properties"], map.properties);
168 }
169
170 return true;
171 }
172
173 bool TiledLevelLoader::ParseLayer(const json& j, std::shared_ptr<TiledLayer>& layer)
174 {
175 layer = std::make_shared<TiledLayer>();
176
177 layer->id = GetInt(j, "id");
178 layer->name = GetString(j, "name");
179 layer->visible = GetBool(j, "visible", true);
180 layer->opacity = GetFloat(j, "opacity", 1.0f);
181 layer->offsetx = GetFloat(j, "offsetx", 0.0f);
182 layer->offsety = GetFloat(j, "offsety", 0.0f);
183 layer->parallaxx = GetFloat(j, "parallaxx", 1.0f);
184 layer->parallaxy = GetFloat(j, "parallaxy", 1.0f);
185
186 if (HasKey(j, "tintcolor")) {
187 layer->tintcolor = ParseColor(GetString(j, "tintcolor"));
188 }
189
190 std::string typeStr = GetString(j, "type", "tilelayer");
191
192 if (typeStr == "tilelayer") {
193 layer->type = LayerType::TileLayer;
194 if (!ParseTileLayer(j, *layer)) return false;
195 }
196 else if (typeStr == "objectgroup") {
197 layer->type = LayerType::ObjectGroup;
198 if (!ParseObjectLayer(j, *layer)) return false;
199 }
200 else if (typeStr == "imagelayer") {
201 layer->type = LayerType::ImageLayer;
202 if (!ParseImageLayer(j, *layer)) return false;
203 }
204 else if (typeStr == "group") {
205 layer->type = LayerType::Group;
206 if (!ParseGroupLayer(j, *layer)) return false;
207 }
208
209 // Parse properties
210 if (HasKey(j, "properties")) {
211 ParseProperties(j["properties"], layer->properties);
212 }
213
214 return true;
215 }
216
218 {
219 layer.width = GetInt(j, "width");
220 layer.height = GetInt(j, "height");
221 layer.startx = GetInt(j, "startx", 0); // Parse startx offset
222 layer.starty = GetInt(j, "starty", 0); // Parse starty offset
223 layer.encoding = GetString(j, "encoding", "csv");
224 layer.compression = GetString(j, "compression", "");
225
226 // Parse chunks (for infinite maps)
227 if (HasKey(j, "chunks") && j["chunks"].is_array()) {
228 for (size_t i = 0; i < j["chunks"].size(); ++i) {
229 const json& chunkJson = j["chunks"][i];
231 // PASS LAYER ENCODING/COMPRESSION TO CHUNK
232 if (ParseChunk(chunkJson, chunk, layer.encoding, layer.compression)) {
233 layer.chunks.push_back(chunk);
234 }
235 }
236 }
237 // Parse data (for finite maps)
238 else if (HasKey(j, "data")) {
239 if (!ParseTileData(j, layer)) {
240 return false;
241 }
242 }
243
244 return true;
245 }
246
248 {
249 if (HasKey(j, "objects") && j["objects"].is_array()) {
250 for (size_t i = 0; i < j["objects"].size(); ++i) {
251 const json& objJson = j["objects"][i];
253 if (ParseObject(objJson, object)) {
254 layer.objects.push_back(object);
255 }
256 }
257 }
258
259 return true;
260 }
261
263 {
264 layer.image = GetString(j, "image");
265 layer.repeatx = GetBool(j, "repeatx", false);
266 layer.repeaty = GetBool(j, "repeaty", false);
267 return true;
268 }
269
271 {
272 if (HasKey(j, "layers") && j["layers"].is_array()) {
273 for (size_t i = 0; i < j["layers"].size(); ++i) {
274 const json& childJson = j["layers"][i];
275 std::shared_ptr<TiledLayer> childLayer;
277 layer.layers.push_back(childLayer);
278 }
279 }
280 }
281
282 return true;
283 }
284
286 {
287 object.id = GetInt(j, "id");
288 object.name = GetString(j, "name");
289 object.type = GetString(j, "type");
290 object.x = GetFloat(j, "x");
291 object.y = GetFloat(j, "y");
292 object.width = GetFloat(j, "width");
293 object.height = GetFloat(j, "height");
294 object.rotation = GetFloat(j, "rotation");
295 object.gid = GetInt(j, "gid");
296 object.visible = GetBool(j, "visible", true);
297
298 // Determine object type
299 if (HasKey(j, "point") && GetBool(j, "point", false)) {
300 object.objectType = ObjectType::Point;
301 }
302 else if (HasKey(j, "ellipse") && GetBool(j, "ellipse", false)) {
303 object.objectType = ObjectType::Ellipse;
304 }
305 else if (HasKey(j, "polygon")) {
306 object.objectType = ObjectType::Polygon;
307 if (j["polygon"].is_array()) {
308 for (size_t i = 0; i < j["polygon"].size(); ++i) {
309 const json& ptJson = j["polygon"][i];
310 Point pt;
311 pt.x = GetFloat(ptJson, "x");
312 pt.y = GetFloat(ptJson, "y");
313 object.polygon.push_back(pt);
314 }
315 }
316 }
317 else if (HasKey(j, "polyline")) {
318 object.objectType = ObjectType::Polyline;
319 if (j["polyline"].is_array()) {
320 for (size_t i = 0; i < j["polyline"].size(); ++i) {
321 const json& ptJson = j["polyline"][i];
322 Point pt;
323 pt.x = GetFloat(ptJson, "x");
324 pt.y = GetFloat(ptJson, "y");
325 object.polyline.push_back(pt);
326 }
327 }
328 }
329 else if (HasKey(j, "text")) {
330 object.objectType = ObjectType::Text;
331 object.text = GetString(j["text"], "text", "");
332 }
333 else {
334 object.objectType = ObjectType::Rectangle;
335 }
336
337 // Parse properties
338 if (HasKey(j, "properties")) {
339 ParseProperties(j["properties"], object.properties);
340 }
341
342 return true;
343 }
344
345 bool TiledLevelLoader::ParseTileset(const json& j, TiledTileset& tileset, const std::string& mapDir)
346 {
347 tileset.firstgid = GetInt(j, "firstgid");
348 tileset.source = GetString(j, "source");
349
350 // If external tileset, just store the source path
351 if (!tileset.source.empty()) {
352 return true;
353 }
354
355 // Parse embedded tileset
356 tileset.name = GetString(j, "name");
357 tileset.tilewidth = GetInt(j, "tilewidth");
358 tileset.tileheight = GetInt(j, "tileheight");
359 tileset.tilecount = GetInt(j, "tilecount");
360 tileset.columns = GetInt(j, "columns");
361 tileset.spacing = GetInt(j, "spacing");
362 tileset.margin = GetInt(j, "margin");
363
364 // ====================================================================
365 // CRITICAL: Parse tileoffset for embedded tilesets
366 // This was previously only parsed for external tilesets in TilesetParser
367 // Now properly handled for embedded tilesets as well
368 // ====================================================================
369 if (HasKey(j, "tileoffset"))
370 {
371 const auto& offset = j["tileoffset"];
372 tileset.tileoffsetX = GetInt(offset, "x");
373 tileset.tileoffsetY = GetInt(offset, "y");
374 SYSTEM_LOG << "TiledLevelLoader: Parsed embedded tileset tileoffset ("
375 << tileset.tileoffsetX << ", " << tileset.tileoffsetY
376 << ") for tileset '" << tileset.name << "'" << std::endl;
377 }
378 else
379 {
380 // Explicit defaults when no tileoffset property present
381 tileset.tileoffsetX = 0;
382 tileset.tileoffsetY = 0;
383 }
384
385 tileset.image = GetString(j, "image");
386 tileset.imagewidth = GetInt(j, "imagewidth");
387 tileset.imageheight = GetInt(j, "imageheight");
388 tileset.transparentcolor = GetString(j, "transparentcolor");
389
390 // Parse tiles
391 if (HasKey(j, "tiles") && j["tiles"].is_array()) {
392 for (size_t i = 0; i < j["tiles"].size(); ++i) {
393 const json& tileJson = j["tiles"][i];
395 tile.id = GetInt(tileJson, "id");
396 tile.type = GetString(tileJson, "type");
397 tile.image = GetString(tileJson, "image");
398 tile.imagewidth = GetInt(tileJson, "imagewidth");
399 tile.imageheight = GetInt(tileJson, "imageheight");
400
401 if (HasKey(tileJson, "properties")) {
402 ParseProperties(tileJson["properties"], tile.properties);
403 }
404
405 tileset.tiles.push_back(tile);
406 }
407 }
408
409 // Parse properties
410 if (HasKey(j, "properties")) {
411 ParseProperties(j["properties"], tileset.properties);
412 }
413
414 return true;
415 }
416
417 bool TiledLevelLoader::LoadExternalTileset(const std::string& filepath, TiledTileset& tileset)
418 {
419 SYSTEM_LOG << "TiledLevelLoader: Loading external tileset from " << filepath << std::endl;
420
421 // Try to get from cache (cache will load from file if not already cached)
423 if (cachedTileset) {
424 // Copy data from cached tileset (preserve firstgid and source)
425 int firstgid = tileset.firstgid;
426 std::string source = tileset.source;
427 tileset = *cachedTileset;
428 tileset.firstgid = firstgid;
429 tileset.source = source;
430
431 // Log successful loading with tileoffset info
432 SYSTEM_LOG << "TiledLevelLoader: External tileset loaded successfully"
433 << " - firstgid=" << firstgid
434 << ", tileoffset=(" << tileset.tileoffsetX << ", " << tileset.tileoffsetY << ")"
435 << std::endl;
436 return true;
437 }
438
439 // If we get here, cache returned nullptr meaning parsing failed
440 SYSTEM_LOG << "TiledLevelLoader: Failed to load external tileset from " << filepath
441 << " - File may not exist, be corrupted, or have invalid format" << std::endl;
442 lastError_ = "Failed to load or parse external tileset: " + filepath;
443 return false;
444 }
445
447 const std::string& layerEncoding,
448 const std::string& layerCompression)
449 {
450 try {
451 chunk.x = GetInt(j, "x");
452 chunk.y = GetInt(j, "y");
453 chunk.width = GetInt(j, "width");
454 chunk.height = GetInt(j, "height");
455
456 if (HasKey(j, "data")) {
457 if (j["data"].is_string()) {
458 std::string dataStr = j["data"].get<std::string>();
460
461 // Validate decoded data
462 if (chunk.data.empty() && !dataStr.empty()) {
463 SYSTEM_LOG << "TiledLevelLoader: ERROR - Failed to decode chunk data at ("
464 << chunk.x << ", " << chunk.y << ")"
465 << " (encoding=" << layerEncoding
466 << ", compression=" << layerCompression << ")" << std::endl;
467 return false;
468 }
469 }
470 else if (j["data"].is_array()) {
471 // CSV array
472 for (size_t i = 0; i < j["data"].size(); ++i) {
473 const json& val = j["data"][i];
474 if (val.is_number()) {
475 chunk.data.push_back(static_cast<uint32_t>(val.get<int>()));
476 }
477 }
478 }
479
480 // ====================================================================
481 // CRITICAL VALIDATION: Check chunk data size matches dimensions
482 // ====================================================================
483 int expectedSize = chunk.width * chunk.height;
484 int actualSize = static_cast<int>(chunk.data.size());
485
486 if (actualSize != expectedSize) {
487 SYSTEM_LOG << "TiledLevelLoader: ERROR - Chunk data size mismatch at ("
488 << chunk.x << ", " << chunk.y << ")"
489 << "\n Expected: " << expectedSize << " tiles (" << chunk.width << " x " << chunk.height << ")"
490 << "\n Actual: " << actualSize << " tiles"
491 << "\n Encoding: " << layerEncoding
492 << "\n Compression: " << (layerCompression.empty() ? "none" : layerCompression)
493 << std::endl;
494 return false;
495 }
496 }
497
498 return true;
499 }
500 catch (const std::exception& e) {
501 SYSTEM_LOG << "TiledLevelLoader: Failed to parse chunk at (" << chunk.x << ", " << chunk.y << "): " << e.what() << std::endl;
502 return false;
503 }
504 }
505
506 void TiledLevelLoader::ParseProperties(const json& j, std::map<std::string, TiledProperty>& properties)
507 {
508 if (!j.is_array()) return;
509
510 for (size_t i = 0; i < j.size(); ++i) {
511 const json& propJson = j[i];
514 properties[prop.name] = prop;
515 }
516 }
517
519 {
520 prop.name = GetString(j, "name");
521 std::string typeStr = GetString(j, "type", "string");
522
523 if (typeStr == "int") {
524 prop.type = PropertyType::Int;
525 prop.intValue = GetInt(j, "value");
526 }
527 else if (typeStr == "float") {
529 prop.floatValue = GetFloat(j, "value");
530 }
531 else if (typeStr == "bool") {
533 prop.boolValue = GetBool(j, "value");
534 }
535 else if (typeStr == "color") {
537 prop.stringValue = GetString(j, "value");
538 }
539 else if (typeStr == "file") {
541 prop.stringValue = GetString(j, "value");
542 }
543 else {
545 prop.stringValue = GetString(j, "value");
546 }
547 }
548
550 {
551 if (!HasKey(j, "data")) {
552 SYSTEM_LOG << "TiledLevelLoader: Layer '" << layer.name << "' has no 'data' field" << std::endl;
553 return false;
554 }
555
556 if (j["data"].is_string()) {
557 // Encoded data (base64)
558 std::string dataStr = j["data"].get<std::string>();
560
561 // Validate decoded data size
562 if (layer.data.empty() && !dataStr.empty()) {
563 SYSTEM_LOG << "TiledLevelLoader: ERROR - Failed to decode tile data for layer '"
564 << layer.name << "' (encoding=" << layer.encoding
565 << ", compression=" << layer.compression << ")" << std::endl;
566 lastError_ = "Failed to decode tile data for layer: " + layer.name;
567 return false;
568 }
569 }
570 else if (j["data"].is_array()) {
571 // CSV array
572 for (size_t i = 0; i < j["data"].size(); ++i) {
573 const json& val = j["data"][i];
574 if (val.is_number()) {
575 layer.data.push_back(static_cast<uint32_t>(val.get<int>()));
576 }
577 }
578 }
579
580 // ====================================================================
581 // CRITICAL VALIDATION: Check data size matches layer dimensions
582 // ====================================================================
583 int expectedSize = layer.width * layer.height;
584 int actualSize = static_cast<int>(layer.data.size());
585
586 if (actualSize != expectedSize) {
587 SYSTEM_LOG << "TiledLevelLoader: ERROR - Data size mismatch for layer '" << layer.name << "'"
588 << "\n Expected: " << expectedSize << " tiles (" << layer.width << " x " << layer.height << ")"
589 << "\n Actual: " << actualSize << " tiles"
590 << "\n Encoding: " << layer.encoding
591 << "\n Compression: " << (layer.compression.empty() ? "none" : layer.compression)
592 << std::endl;
593 lastError_ = "Data size mismatch for layer '" + layer.name + "': expected "
594 + std::to_string(expectedSize) + " but got " + std::to_string(actualSize);
595 return false;
596 }
597
598 SYSTEM_LOG << "TiledLevelLoader: Successfully parsed layer '" << layer.name
599 << "' with " << actualSize << " tiles" << std::endl;
600
601 return true;
602 }
603
604 // ============================================================================
605 // TMX XML Parsing Functions
606 // ============================================================================
607
609 {
610 tinyxml2::XMLDocument* doc = static_cast<tinyxml2::XMLDocument*>(docPtr);
611 tinyxml2::XMLElement* mapElement = doc->FirstChildElement("map");
612
613 if (!mapElement) {
614 lastError_ = "No <map> element in TMX file";
615 SYSTEM_LOG << "TiledLevelLoader: " << lastError_ << std::endl;
616 return false;
617 }
618
619 // Parse map attributes
620 map.version = mapElement->IntAttribute("version", 1);
621 map.tiledversion = mapElement->Attribute("tiledversion") ? mapElement->Attribute("tiledversion") : "";
622 map.type = "map";
623
624 // Parse orientation
625 const char* orientAttr = mapElement->Attribute("orientation");
626 if (orientAttr) {
627 std::string orientStr = orientAttr;
628 if (orientStr == "orthogonal") map.orientation = MapOrientation::Orthogonal;
629 else if (orientStr == "isometric") map.orientation = MapOrientation::Isometric;
630 else if (orientStr == "staggered") map.orientation = MapOrientation::Staggered;
631 else if (orientStr == "hexagonal") map.orientation = MapOrientation::Hexagonal;
632 }
633
634 // Parse render order
635 const char* renderAttr = mapElement->Attribute("renderorder");
636 if (renderAttr) {
637 std::string renderStr = renderAttr;
638 if (renderStr == "right-down") map.renderorder = RenderOrder::RightDown;
639 else if (renderStr == "right-up") map.renderorder = RenderOrder::RightUp;
640 else if (renderStr == "left-down") map.renderorder = RenderOrder::LeftDown;
641 else if (renderStr == "left-up") map.renderorder = RenderOrder::LeftUp;
642 }
643
644 map.compressionlevel = mapElement->IntAttribute("compressionlevel", -1);
645 map.width = mapElement->IntAttribute("width", 0);
646 map.height = mapElement->IntAttribute("height", 0);
647 map.tilewidth = mapElement->IntAttribute("tilewidth", 0);
648 map.tileheight = mapElement->IntAttribute("tileheight", 0);
649 map.infinite = mapElement->BoolAttribute("infinite", false);
650
651 const char* bgColorAttr = mapElement->Attribute("backgroundcolor");
652 if (bgColorAttr) {
653 map.backgroundcolor = bgColorAttr;
654 }
655
656 map.nextlayerid = mapElement->IntAttribute("nextlayerid", 1);
657 map.nextobjectid = mapElement->IntAttribute("nextobjectid", 1);
658
659 // Validation
660 if (map.width == 0 || map.height == 0 || map.tilewidth == 0 || map.tileheight == 0) {
661 lastError_ = "Invalid map dimensions in TMX";
662 SYSTEM_LOG << "TiledLevelLoader: " << lastError_ << std::endl;
663 return false;
664 }
665
666 // Parse tilesets
667 for (tinyxml2::XMLElement* tsElement = mapElement->FirstChildElement("tileset");
668 tsElement != nullptr;
669 tsElement = tsElement->NextSiblingElement("tileset"))
670 {
671 TiledTileset tileset;
672 if (ParseTilesetXML(tsElement, tileset, "")) {
673 map.tilesets.push_back(tileset);
674 }
675 }
676
677 // Parse layers
678 for (tinyxml2::XMLElement* layerElement = mapElement->FirstChildElement();
679 layerElement != nullptr;
680 layerElement = layerElement->NextSiblingElement())
681 {
682 std::string elementName = layerElement->Name();
683 if (elementName == "layer" || elementName == "objectgroup" ||
684 elementName == "imagelayer" || elementName == "group")
685 {
686 std::shared_ptr<TiledLayer> layer;
687 if (ParseLayerXML(layerElement, layer)) {
688 map.layers.push_back(layer);
689 }
690 }
691 }
692
693 // Parse properties
694 tinyxml2::XMLElement* propertiesElement = mapElement->FirstChildElement("properties");
695 if (propertiesElement) {
697 }
698
699 return true;
700 }
701
702 bool TiledLevelLoader::ParseTilesetXML(void* element, TiledTileset& tileset, const std::string& mapDir)
703 {
704 tinyxml2::XMLElement* tsElement = static_cast<tinyxml2::XMLElement*>(element);
705
706 tileset.firstgid = tsElement->IntAttribute("firstgid", 0);
707
708 // Check for external tileset
709 const char* sourceAttr = tsElement->Attribute("source");
710 if (sourceAttr) {
711 tileset.source = sourceAttr;
712 return true; // Will be loaded later
713 }
714
715 // Parse embedded tileset
716 tileset.name = tsElement->Attribute("name") ? tsElement->Attribute("name") : "";
717 tileset.tilewidth = tsElement->IntAttribute("tilewidth", 0);
718 tileset.tileheight = tsElement->IntAttribute("tileheight", 0);
719 tileset.tilecount = tsElement->IntAttribute("tilecount", 0);
720 tileset.columns = tsElement->IntAttribute("columns", 0);
721 tileset.spacing = tsElement->IntAttribute("spacing", 0);
722 tileset.margin = tsElement->IntAttribute("margin", 0);
723
724 // Parse tileoffset
725 tinyxml2::XMLElement* offsetElement = tsElement->FirstChildElement("tileoffset");
726 if (offsetElement) {
727 tileset.tileoffsetX = offsetElement->IntAttribute("x", 0);
728 tileset.tileoffsetY = offsetElement->IntAttribute("y", 0);
729 SYSTEM_LOG << "TiledLevelLoader (TMX): Parsed tileoffset ("
730 << tileset.tileoffsetX << ", " << tileset.tileoffsetY
731 << ") for tileset '" << tileset.name << "'" << std::endl;
732 }
733
734 // Parse image
735 tinyxml2::XMLElement* imageElement = tsElement->FirstChildElement("image");
736 if (imageElement) {
737 tileset.image = imageElement->Attribute("source") ? imageElement->Attribute("source") : "";
738 tileset.imagewidth = imageElement->IntAttribute("width", 0);
739 tileset.imageheight = imageElement->IntAttribute("height", 0);
740 const char* trans = imageElement->Attribute("trans");
741 if (trans) {
742 std::string transStr = trans;
743 tileset.transparentcolor = (transStr[0] == '#') ? transStr : ("#" + transStr);
744 }
745 }
746
747 // Parse tiles (for collection tilesets)
748 for (tinyxml2::XMLElement* tileElement = tsElement->FirstChildElement("tile");
749 tileElement != nullptr;
750 tileElement = tileElement->NextSiblingElement("tile"))
751 {
753 tile.id = tileElement->IntAttribute("id", 0);
754
755 const char* typeAttr = tileElement->Attribute("type");
756 if (typeAttr) {
757 tile.type = typeAttr;
758 }
759
760 // Parse tile image
761 tinyxml2::XMLElement* tileImageElement = tileElement->FirstChildElement("image");
762 if (tileImageElement) {
763 tile.image = tileImageElement->Attribute("source") ?
764 tileImageElement->Attribute("source") : "";
765 tile.imagewidth = tileImageElement->IntAttribute("width", 0);
766 tile.imageheight = tileImageElement->IntAttribute("height", 0);
767 }
768
769 // Parse tile properties
770 tinyxml2::XMLElement* tilePropsElement = tileElement->FirstChildElement("properties");
771 if (tilePropsElement) {
773 }
774
775 tileset.tiles.push_back(tile);
776 }
777
778 // Parse tileset properties
779 tinyxml2::XMLElement* propertiesElement = tsElement->FirstChildElement("properties");
780 if (propertiesElement) {
782 }
783
784 return true;
785 }
786
787 bool TiledLevelLoader::ParseLayerXML(void* element, std::shared_ptr<TiledLayer>& layer)
788 {
789 tinyxml2::XMLElement* layerElement = static_cast<tinyxml2::XMLElement*>(element);
790 layer = std::make_shared<TiledLayer>();
791
792 std::string elementName = layerElement->Name();
793
794 // Common attributes
795 layer->id = layerElement->IntAttribute("id", 0);
796 layer->name = layerElement->Attribute("name") ? layerElement->Attribute("name") : "";
797 layer->visible = layerElement->IntAttribute("visible", 1) != 0;
798 layer->opacity = layerElement->FloatAttribute("opacity", 1.0f);
799 layer->offsetx = layerElement->FloatAttribute("offsetx", 0.0f);
800 layer->offsety = layerElement->FloatAttribute("offsety", 0.0f);
801 layer->parallaxx = layerElement->FloatAttribute("parallaxx", 1.0f);
802 layer->parallaxy = layerElement->FloatAttribute("parallaxy", 1.0f);
803
804 const char* tintAttr = layerElement->Attribute("tintcolor");
805 if (tintAttr) {
806 layer->tintcolor = ParseColor(tintAttr);
807 }
808
809 // Parse by layer type
810 if (elementName == "layer") {
811 layer->type = LayerType::TileLayer;
812 if (!ParseTileLayerXML(layerElement, *layer)) return false;
813 }
814 else if (elementName == "objectgroup") {
815 layer->type = LayerType::ObjectGroup;
816 if (!ParseObjectLayerXML(layerElement, *layer)) return false;
817 }
818 else if (elementName == "imagelayer") {
819 layer->type = LayerType::ImageLayer;
820 if (!ParseImageLayerXML(layerElement, *layer)) return false;
821 }
822 else if (elementName == "group") {
823 layer->type = LayerType::Group;
824 if (!ParseGroupLayerXML(layerElement, *layer)) return false;
825 }
826
827 // Parse properties
828 tinyxml2::XMLElement* propertiesElement = layerElement->FirstChildElement("properties");
829 if (propertiesElement) {
830 ParsePropertiesXML(propertiesElement, layer->properties);
831 }
832
833 return true;
834 }
835
837 {
838 tinyxml2::XMLElement* layerElement = static_cast<tinyxml2::XMLElement*>(element);
839
840 layer.width = layerElement->IntAttribute("width", 0);
841 layer.height = layerElement->IntAttribute("height", 0);
842 layer.startx = layerElement->IntAttribute("startx", 0);
843 layer.starty = layerElement->IntAttribute("starty", 0);
844
845 // Parse data element
846 tinyxml2::XMLElement* dataElement = layerElement->FirstChildElement("data");
847 if (dataElement) {
848 if (!ParseTileDataXML(dataElement, layer)) {
849 return false;
850 }
851 }
852
853 // Parse chunks (for infinite maps)
854 if (dataElement) {
855 for (tinyxml2::XMLElement* chunkElement = dataElement->FirstChildElement("chunk");
856 chunkElement != nullptr;
857 chunkElement = chunkElement->NextSiblingElement("chunk"))
858 {
861 layer.chunks.push_back(chunk);
862 }
863 }
864 }
865
866 return true;
867 }
868
870 {
871 tinyxml2::XMLElement* layerElement = static_cast<tinyxml2::XMLElement*>(element);
872
873 // Parse objects
874 for (tinyxml2::XMLElement* objElement = layerElement->FirstChildElement("object");
875 objElement != nullptr;
876 objElement = objElement->NextSiblingElement("object"))
877 {
879 if (ParseObjectXML(objElement, object)) {
880 layer.objects.push_back(object);
881 }
882 }
883
884 return true;
885 }
886
888 {
889 tinyxml2::XMLElement* layerElement = static_cast<tinyxml2::XMLElement*>(element);
890
891 // Parse image element
892 tinyxml2::XMLElement* imageElement = layerElement->FirstChildElement("image");
893 if (imageElement) {
894 layer.image = imageElement->Attribute("source") ? imageElement->Attribute("source") : "";
895 }
896
897 layer.repeatx = layerElement->BoolAttribute("repeatx", false);
898 layer.repeaty = layerElement->BoolAttribute("repeaty", false);
899
900 return true;
901 }
902
904 {
905 tinyxml2::XMLElement* layerElement = static_cast<tinyxml2::XMLElement*>(element);
906
907 // Parse child layers
908 for (tinyxml2::XMLElement* childElement = layerElement->FirstChildElement();
909 childElement != nullptr;
910 childElement = childElement->NextSiblingElement())
911 {
912 std::string elementName = childElement->Name();
913 if (elementName == "layer" || elementName == "objectgroup" ||
914 elementName == "imagelayer" || elementName == "group")
915 {
916 std::shared_ptr<TiledLayer> childLayer;
918 layer.layers.push_back(childLayer);
919 }
920 }
921 }
922
923 return true;
924 }
925
927 {
928 tinyxml2::XMLElement* objElement = static_cast<tinyxml2::XMLElement*>(element);
929
930 object.id = objElement->IntAttribute("id", 0);
931 object.name = objElement->Attribute("name") ? objElement->Attribute("name") : "";
932 object.type = objElement->Attribute("type") ? objElement->Attribute("type") : "";
933 object.x = objElement->FloatAttribute("x", 0.0f);
934 object.y = objElement->FloatAttribute("y", 0.0f);
935 object.width = objElement->FloatAttribute("width", 0.0f);
936 object.height = objElement->FloatAttribute("height", 0.0f);
937 object.rotation = objElement->FloatAttribute("rotation", 0.0f);
938 object.gid = objElement->IntAttribute("gid", 0);
939 object.visible = objElement->IntAttribute("visible", 1) != 0;
940
941 // Determine object type
942 tinyxml2::XMLElement* pointElement = objElement->FirstChildElement("point");
943 tinyxml2::XMLElement* ellipseElement = objElement->FirstChildElement("ellipse");
944 tinyxml2::XMLElement* polygonElement = objElement->FirstChildElement("polygon");
945 tinyxml2::XMLElement* polylineElement = objElement->FirstChildElement("polyline");
946 tinyxml2::XMLElement* textElement = objElement->FirstChildElement("text");
947
948 if (pointElement) {
949 object.objectType = ObjectType::Point;
950 }
951 else if (ellipseElement) {
952 object.objectType = ObjectType::Ellipse;
953 }
954 else if (polygonElement) {
955 object.objectType = ObjectType::Polygon;
956 const char* points = polygonElement->Attribute("points");
957 if (points) {
958 // Parse points string "x1,y1 x2,y2 x3,y3"
959 std::string pointsStr = points;
960 size_t pos = 0;
961 while (pos < pointsStr.length()) {
962 size_t commaPos = pointsStr.find(',', pos);
963 size_t spacePos = pointsStr.find(' ', commaPos);
964 if (commaPos == std::string::npos) break;
965
966 float x = std::stof(pointsStr.substr(pos, commaPos - pos));
967 float y = std::stof(pointsStr.substr(commaPos + 1,
968 (spacePos == std::string::npos ? pointsStr.length() : spacePos) - commaPos - 1));
969 object.polygon.push_back(Point(x, y));
970
971 if (spacePos == std::string::npos) break;
972 pos = spacePos + 1;
973 }
974 }
975 }
976 else if (polylineElement) {
977 object.objectType = ObjectType::Polyline;
978 const char* points = polylineElement->Attribute("points");
979 if (points) {
980 // Parse points string "x1,y1 x2,y2 x3,y3"
981 std::string pointsStr = points;
982 size_t pos = 0;
983 while (pos < pointsStr.length()) {
984 size_t commaPos = pointsStr.find(',', pos);
985 size_t spacePos = pointsStr.find(' ', commaPos);
986 if (commaPos == std::string::npos) break;
987
988 float x = std::stof(pointsStr.substr(pos, commaPos - pos));
989 float y = std::stof(pointsStr.substr(commaPos + 1,
990 (spacePos == std::string::npos ? pointsStr.length() : spacePos) - commaPos - 1));
991 object.polyline.push_back(Point(x, y));
992
993 if (spacePos == std::string::npos) break;
994 pos = spacePos + 1;
995 }
996 }
997 }
998 else if (textElement) {
999 object.objectType = ObjectType::Text;
1000 const char* textContent = textElement->GetText();
1001 if (textContent) {
1002 object.text = textContent;
1003 }
1004 }
1005 else {
1006 object.objectType = ObjectType::Rectangle;
1007 }
1008
1009 // Parse properties
1010 tinyxml2::XMLElement* propertiesElement = objElement->FirstChildElement("properties");
1011 if (propertiesElement) {
1012 ParsePropertiesXML(propertiesElement, object.properties);
1013 }
1014
1015 return true;
1016 }
1017
1019 {
1020 tinyxml2::XMLElement* dataElement = static_cast<tinyxml2::XMLElement*>(element);
1021
1022 // Get encoding and compression
1023 const char* encodingAttr = dataElement->Attribute("encoding");
1024 const char* compressionAttr = dataElement->Attribute("compression");
1025
1026 layer.encoding = encodingAttr ? encodingAttr : "csv";
1028
1029 const char* dataText = dataElement->GetText();
1030 if (!dataText) {
1031 // Check for tile elements (uncompressed XML format)
1032 for (tinyxml2::XMLElement* tileElement = dataElement->FirstChildElement("tile");
1033 tileElement != nullptr;
1034 tileElement = tileElement->NextSiblingElement("tile"))
1035 {
1036 uint32_t gid = tileElement->IntAttribute("gid", 0);
1037 layer.data.push_back(gid);
1038 }
1039 }
1040 else {
1041 std::string dataStr = dataText;
1042
1043 // Trim whitespace
1044 size_t start = dataStr.find_first_not_of(" \t\n\r");
1045 size_t end = dataStr.find_last_not_of(" \t\n\r");
1046 if (start != std::string::npos && end != std::string::npos) {
1047 dataStr = dataStr.substr(start, end - start + 1);
1048 }
1049
1050 if (layer.encoding == "csv") {
1051 // Parse CSV
1052 size_t pos = 0;
1053 while (pos < dataStr.length()) {
1054 size_t commaPos = dataStr.find(',', pos);
1055 std::string token = dataStr.substr(pos,
1056 (commaPos == std::string::npos ? dataStr.length() : commaPos) - pos);
1057
1058 // Trim token
1059 size_t tokenStart = token.find_first_not_of(" \t\n\r");
1060 size_t tokenEnd = token.find_last_not_of(" \t\n\r");
1061 if (tokenStart != std::string::npos && tokenEnd != std::string::npos) {
1062 token = token.substr(tokenStart, tokenEnd - tokenStart + 1);
1063 }
1064
1065 if (!token.empty()) {
1066 try {
1067 uint32_t gid = static_cast<uint32_t>(std::stoul(token));
1068 layer.data.push_back(gid);
1069 } catch (const std::exception& e) {
1070 SYSTEM_LOG << "TiledLevelLoader: Failed to parse CSV token: " << token << " Error : " << e.what() << std::endl;
1071 }
1072 }
1073
1074 if (commaPos == std::string::npos) break;
1075 pos = commaPos + 1;
1076 }
1077 }
1078 else if (layer.encoding == "base64") {
1079 // Decode base64
1081 }
1082 }
1083
1084 // Validate data size
1085 int expectedSize = layer.width * layer.height;
1086 int actualSize = static_cast<int>(layer.data.size());
1087
1088 if (actualSize != expectedSize && !dataText) {
1089 SYSTEM_LOG << "TiledLevelLoader: ERROR - Data size mismatch for layer '" << layer.name << "'"
1090 << "\n Expected: " << expectedSize << " tiles (" << layer.width << " x " << layer.height << ")"
1091 << "\n Actual: " << actualSize << " tiles" << std::endl;
1092 lastError_ = "Data size mismatch for layer '" + layer.name + "'";
1093 return false;
1094 }
1095
1096 SYSTEM_LOG << "TiledLevelLoader: Successfully parsed TMX layer '" << layer.name
1097 << "' with " << actualSize << " tiles" << std::endl;
1098
1099 return true;
1100 }
1101
1103 const std::string& layerEncoding,
1104 const std::string& layerCompression)
1105 {
1106 tinyxml2::XMLElement* chunkElement = static_cast<tinyxml2::XMLElement*>(element);
1107
1108 chunk.x = chunkElement->IntAttribute("x", 0);
1109 chunk.y = chunkElement->IntAttribute("y", 0);
1110 chunk.width = chunkElement->IntAttribute("width", 0);
1111 chunk.height = chunkElement->IntAttribute("height", 0);
1112
1113 const char* dataText = chunkElement->GetText();
1114 if (dataText) {
1115 std::string dataStr = dataText;
1116
1117 // Trim whitespace
1118 size_t start = dataStr.find_first_not_of(" \t\n\r");
1119 size_t end = dataStr.find_last_not_of(" \t\n\r");
1120 if (start != std::string::npos && end != std::string::npos) {
1121 dataStr = dataStr.substr(start, end - start + 1);
1122 }
1123
1125
1126 // Validate chunk data size
1127 int expectedSize = chunk.width * chunk.height;
1128 int actualSize = static_cast<int>(chunk.data.size());
1129
1130 if (actualSize != expectedSize) {
1131 SYSTEM_LOG << "TiledLevelLoader: ERROR - Chunk data size mismatch at ("
1132 << chunk.x << ", " << chunk.y << ")"
1133 << "\n Expected: " << expectedSize << " tiles"
1134 << "\n Actual: " << actualSize << " tiles" << std::endl;
1135 return false;
1136 }
1137 }
1138
1139 return true;
1140 }
1141
1142 void TiledLevelLoader::ParsePropertiesXML(void* element, std::map<std::string, TiledProperty>& properties)
1143 {
1144 tinyxml2::XMLElement* propsElement = static_cast<tinyxml2::XMLElement*>(element);
1145
1146 for (tinyxml2::XMLElement* propElement = propsElement->FirstChildElement("property");
1147 propElement != nullptr;
1148 propElement = propElement->NextSiblingElement("property"))
1149 {
1152 properties[prop.name] = prop;
1153 }
1154 }
1155
1157 {
1158 tinyxml2::XMLElement* propElement = static_cast<tinyxml2::XMLElement*>(element);
1159
1160 prop.name = propElement->Attribute("name") ? propElement->Attribute("name") : "";
1161
1162 const char* typeAttr = propElement->Attribute("type");
1163 std::string typeStr = typeAttr ? typeAttr : "string";
1164
1165 if (typeStr == "int") {
1166 prop.type = PropertyType::Int;
1167 prop.intValue = propElement->IntAttribute("value", 0);
1168 }
1169 else if (typeStr == "float") {
1171 prop.floatValue = propElement->FloatAttribute("value", 0.0f);
1172 }
1173 else if (typeStr == "bool") {
1174 prop.type = PropertyType::Bool;
1175 prop.boolValue = propElement->BoolAttribute("value", false);
1176 }
1177 else if (typeStr == "color") {
1179 prop.stringValue = propElement->Attribute("value") ? propElement->Attribute("value") : "";
1180 }
1181 else if (typeStr == "file") {
1182 prop.type = PropertyType::File;
1183 prop.stringValue = propElement->Attribute("value") ? propElement->Attribute("value") : "";
1184 }
1185 else {
1187 prop.stringValue = propElement->Attribute("value") ? propElement->Attribute("value") : "";
1188 }
1189 }
1190
1191 // ============================================================================
1192 // End of TMX XML Parsing Functions
1193 // ============================================================================
1194
1195 std::string TiledLevelLoader::ResolvePath(const std::string& mapDir, const std::string& relativePath)
1196 {
1197 if (relativePath.empty()) return relativePath;
1198
1199 // If absolute path, return as-is
1200 if (relativePath[0] == '/' || (relativePath.length() > 1 && relativePath[1] == ':')) {
1201 return relativePath;
1202 }
1203
1204 // Combine with map directory
1205 if (mapDir.empty()) {
1206 return relativePath;
1207 }
1208
1209 return mapDir + "\\" + relativePath;
1210 }
1211
1212 std::string TiledLevelLoader::GetDirectory(const std::string& filepath)
1213 {
1214 size_t pos = filepath.find_last_of("/\\");
1215 if (pos == std::string::npos) {
1216 return "";
1217 }
1218 return filepath.substr(0, pos);
1219 }
1220
1221 bool TiledLevelLoader::ReadFile(const std::string& filepath, std::string& outContent)
1222 {
1223 std::ifstream file(filepath, std::ios::binary);
1224 if (!file.is_open()) {
1225 SYSTEM_LOG << "TiledLevelLoader: Failed to open file: " << filepath << std::endl;
1226 return false;
1227 }
1228
1229 std::stringstream buffer;
1230 buffer << file.rdbuf();
1231 outContent = buffer.str();
1232 return true;
1233 }
1234
1235} // namespace Tiled
1236} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
static std::vector< uint32_t > DecodeTileData(const std::string &data, const std::string &encoding, const std::string &compression)
bool ReadFile(const std::string &filepath, std::string &outContent)
bool ParseMapXML(void *doc, TiledMap &map)
bool ParseImageLayer(const json &j, TiledLayer &layer)
bool ParseTilesetXML(void *element, TiledTileset &tileset, const std::string &mapDir)
bool ParseTileLayerXML(void *element, TiledLayer &layer)
bool LoadExternalTileset(const std::string &filepath, TiledTileset &tileset)
bool ParseTileData(const json &j, TiledLayer &layer)
void ParseProperties(const json &j, std::map< std::string, TiledProperty > &properties)
bool ParseMap(const json &j, TiledMap &map)
bool ParseObject(const json &j, TiledObject &object)
bool ParseLayer(const json &j, std::shared_ptr< TiledLayer > &layer)
bool ParseObjectLayerXML(void *element, TiledLayer &layer)
bool ParseImageLayerXML(void *element, TiledLayer &layer)
void ParsePropertiesXML(void *element, std::map< std::string, TiledProperty > &properties)
bool ParseChunk(const json &j, TiledChunk &chunk, const std::string &layerEncoding, const std::string &layerCompression)
bool ParseTileset(const json &j, TiledTileset &tileset, const std::string &mapDir)
void ParseProperty(const json &j, TiledProperty &prop)
bool ParseChunkXML(void *element, TiledChunk &chunk, const std::string &layerEncoding, const std::string &layerCompression)
std::string GetDirectory(const std::string &filepath)
std::string ResolvePath(const std::string &mapDir, const std::string &relativePath)
bool ParseTileLayer(const json &j, TiledLayer &layer)
bool ParseObjectXML(void *element, TiledObject &object)
void ParsePropertyXML(void *element, TiledProperty &prop)
bool ParseLayerXML(void *element, std::shared_ptr< TiledLayer > &layer)
bool ParseObjectLayer(const json &j, TiledLayer &layer)
bool LoadFromFile(const std::string &filepath, TiledMap &outMap)
bool ParseGroupLayer(const json &j, TiledLayer &layer)
bool ParseGroupLayerXML(void *element, TiledLayer &layer)
bool ParseTileDataXML(void *element, TiledLayer &layer)
std::shared_ptr< TiledTileset > GetTileset(const std::string &filepath)
static TilesetCache & GetInstance()
nlohmann::json json
std::string GetString(const json &j, const std::string &key, const std::string &defaultValue="")
int GetInt(const json &j, const std::string &key, int defaultValue=0)
bool HasKey(const json &j, const std::string &key)
bool GetBool(const json &j, const std::string &key, bool defaultValue=false)
float GetFloat(const json &j, const std::string &key, float defaultValue=0.0f)
int ParseColor(const std::string &colorStr)
std::vector< TiledObject > objects
std::vector< uint32_t > data
std::vector< TiledChunk > chunks
std::vector< std::shared_ptr< TiledLayer > > layers
std::vector< TiledTile > tiles
std::map< std::string, TiledProperty > properties
#define SYSTEM_LOG