TiledDataDecoder.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.IO.Compression;
  5. using MonoGame.Extended.Tilemaps.Parsers;
  6. namespace MonoGame.Extended.Tilemaps.Tiled;
  7. /// <summary>
  8. /// Decodes tile data from various Tiled encodings (CSV, XML, Base64, gzip, zlib).
  9. /// </summary>
  10. internal static class TiledDataDecoder
  11. {
  12. // Flip flag constants from Tiled specification
  13. private const uint FLIPPED_HORIZONTALLY_FLAG = 0x80000000;
  14. private const uint FLIPPED_VERTICALLY_FLAG = 0x40000000;
  15. private const uint FLIPPED_DIAGONALLY_FLAG = 0x20000000;
  16. private const uint FLIP_MASK = 0xE0000000;
  17. /// <summary>
  18. /// Decodes tile data from various formats.
  19. /// </summary>
  20. /// <param name="data">The tile layer data XML.</param>
  21. /// <param name="width">The layer width in tiles.</param>
  22. /// <param name="height">The layer height in tiles.</param>
  23. /// <returns>A 2D array of tiles.</returns>
  24. public static TilemapTile[,] DecodeTileData(TiledTileLayerDataXml data, int width, int height)
  25. {
  26. if (data == null)
  27. {
  28. return new TilemapTile[width, height];
  29. }
  30. // Handle infinite maps (chunked data)
  31. if (data.Chunks != null && data.Chunks.Count > 0)
  32. {
  33. return DecodeChunkedData(data, width, height);
  34. }
  35. // Handle regular data
  36. return data.Encoding switch
  37. {
  38. null => DecodeXmlData(data.Tiles, width, height),
  39. "csv" => DecodeCsvData(data.Value, width, height),
  40. "base64" => DecodeBase64Data(data.Value, data.Compression, width, height),
  41. _ => throw new TilemapParseException($"Unsupported tile data encoding: '{data.Encoding}'")
  42. };
  43. }
  44. private static TilemapTile[,] DecodeXmlData(List<TiledDataTileXml> tiles, int width, int height)
  45. {
  46. TilemapTile[,] result = new TilemapTile[width, height];
  47. if (tiles == null || tiles.Count == 0)
  48. {
  49. return result;
  50. }
  51. for (int i = 0; i < tiles.Count && i < width * height; i++)
  52. {
  53. (int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(tiles[i].Gid);
  54. int x = i % width;
  55. int y = i / width;
  56. result[x, y] = new TilemapTile(gid, flags);
  57. }
  58. return result;
  59. }
  60. private static TilemapTile[,] DecodeCsvData(string csv, int width, int height)
  61. {
  62. TilemapTile[,] result = new TilemapTile[width, height];
  63. if (string.IsNullOrWhiteSpace(csv))
  64. {
  65. return result;
  66. }
  67. string[] values = csv.Split(new[] { ',', '\n', '\r', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
  68. for (int i = 0; i < values.Length && i < width * height; i++)
  69. {
  70. if (!uint.TryParse(values[i].Trim(), out uint rawGid))
  71. {
  72. throw new TilemapParseException($"Invalid tile GID in CSV data: '{values[i]}'");
  73. }
  74. (int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(rawGid);
  75. int x = i % width;
  76. int y = i / width;
  77. result[x, y] = new TilemapTile(gid, flags);
  78. }
  79. return result;
  80. }
  81. private static TilemapTile[,] DecodeBase64Data(string base64, string compression, int width, int height)
  82. {
  83. if (string.IsNullOrWhiteSpace(base64))
  84. {
  85. return new TilemapTile[width, height];
  86. }
  87. byte[] bytes = Convert.FromBase64String(base64.Trim());
  88. // Decompress if needed
  89. if (!string.IsNullOrEmpty(compression))
  90. {
  91. bytes = compression.ToLowerInvariant() switch
  92. {
  93. "gzip" => DecompressGzip(bytes),
  94. "zlib" => DecompressZlib(bytes),
  95. _ => throw new TilemapParseException($"Unsupported compression format: '{compression}'")
  96. };
  97. }
  98. // Parse uint32 array (little-endian)
  99. TilemapTile[,] result = new TilemapTile[width, height];
  100. int tileCount = width * height;
  101. if (bytes.Length < tileCount * 4)
  102. {
  103. throw new TilemapParseException($"Insufficient tile data: expected {tileCount * 4} bytes, got {bytes.Length}");
  104. }
  105. for (int i = 0; i < tileCount; i++)
  106. {
  107. uint rawGid = BitConverter.ToUInt32(bytes, i * 4);
  108. (int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(rawGid);
  109. int x = i % width;
  110. int y = i / width;
  111. result[x, y] = new TilemapTile(gid, flags);
  112. }
  113. return result;
  114. }
  115. private static TilemapTile[,] DecodeChunkedData(TiledTileLayerDataXml data, int width, int height)
  116. {
  117. TilemapTile[,] result = new TilemapTile[width, height];
  118. foreach (TiledChunkXml chunk in data.Chunks)
  119. {
  120. // Decode chunk data (always CSV for chunks based on Tiled spec)
  121. TilemapTile[,] chunkTiles = DecodeCsvData(chunk.Value, chunk.Width, chunk.Height);
  122. // Copy chunk tiles into result at correct position
  123. for (int cy = 0; cy < chunk.Height; cy++)
  124. {
  125. for (int cx = 0; cx < chunk.Width; cx++)
  126. {
  127. int worldX = chunk.X + cx;
  128. int worldY = chunk.Y + cy;
  129. // Skip tiles outside map bounds
  130. if (worldX < 0 || worldX >= width || worldY < 0 || worldY >= height)
  131. {
  132. continue;
  133. }
  134. result[worldX, worldY] = chunkTiles[cx, cy];
  135. }
  136. }
  137. }
  138. return result;
  139. }
  140. private static (int gid, TilemapTileFlipFlags flags) ExtractFlipFlags(uint rawGid)
  141. {
  142. // Extract GID (clear flip flags)
  143. int gid = (int)(rawGid & ~FLIP_MASK);
  144. // Extract flip flags
  145. TilemapTileFlipFlags flags = TilemapTileFlipFlags.None;
  146. if ((rawGid & FLIPPED_HORIZONTALLY_FLAG) != 0)
  147. {
  148. flags |= TilemapTileFlipFlags.FlipHorizontally;
  149. }
  150. if ((rawGid & FLIPPED_VERTICALLY_FLAG) != 0)
  151. {
  152. flags |= TilemapTileFlipFlags.FlipVertically;
  153. }
  154. if ((rawGid & FLIPPED_DIAGONALLY_FLAG) != 0)
  155. {
  156. flags |= TilemapTileFlipFlags.FlipDiagonally;
  157. }
  158. return (gid, flags);
  159. }
  160. private static byte[] DecompressGzip(byte[] data)
  161. {
  162. try
  163. {
  164. using var input = new MemoryStream(data);
  165. using var gzip = new GZipStream(input, CompressionMode.Decompress);
  166. using var output = new MemoryStream();
  167. gzip.CopyTo(output);
  168. return output.ToArray();
  169. }
  170. catch (Exception ex)
  171. {
  172. throw new TilemapParseException("Failed to decompress gzip data", ex);
  173. }
  174. }
  175. private static byte[] DecompressZlib(byte[] data)
  176. {
  177. try
  178. {
  179. // Zlib format = 2-byte header + DEFLATE stream + 4-byte Adler-32 checksum
  180. // Skip the 2-byte zlib header and 4-byte checksum at the end
  181. if (data.Length < 6)
  182. {
  183. throw new TilemapParseException($"Invalid zlib data: too short ({data.Length} bytes)");
  184. }
  185. using var input = new MemoryStream(data, 2, data.Length - 6);
  186. using var deflate = new DeflateStream(input, CompressionMode.Decompress);
  187. using var output = new MemoryStream();
  188. deflate.CopyTo(output);
  189. return output.ToArray();
  190. }
  191. catch (Exception ex)
  192. {
  193. throw new TilemapParseException("Failed to decompress zlib data", ex);
  194. }
  195. }
  196. }