using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using MonoGame.Extended.Tilemaps.Parsers; namespace MonoGame.Extended.Tilemaps.Tiled; /// /// Decodes tile data from various Tiled encodings (CSV, XML, Base64, gzip, zlib). /// internal static class TiledDataDecoder { // Flip flag constants from Tiled specification private const uint FLIPPED_HORIZONTALLY_FLAG = 0x80000000; private const uint FLIPPED_VERTICALLY_FLAG = 0x40000000; private const uint FLIPPED_DIAGONALLY_FLAG = 0x20000000; private const uint FLIP_MASK = 0xE0000000; /// /// Decodes tile data from various formats. /// /// The tile layer data XML. /// The layer width in tiles. /// The layer height in tiles. /// A 2D array of tiles. public static TilemapTile[,] DecodeTileData(TiledTileLayerDataXml data, int width, int height) { if (data == null) { return new TilemapTile[width, height]; } // Handle infinite maps (chunked data) if (data.Chunks != null && data.Chunks.Count > 0) { return DecodeChunkedData(data, width, height); } // Handle regular data return data.Encoding switch { null => DecodeXmlData(data.Tiles, width, height), "csv" => DecodeCsvData(data.Value, width, height), "base64" => DecodeBase64Data(data.Value, data.Compression, width, height), _ => throw new TilemapParseException($"Unsupported tile data encoding: '{data.Encoding}'") }; } private static TilemapTile[,] DecodeXmlData(List tiles, int width, int height) { TilemapTile[,] result = new TilemapTile[width, height]; if (tiles == null || tiles.Count == 0) { return result; } for (int i = 0; i < tiles.Count && i < width * height; i++) { (int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(tiles[i].Gid); int x = i % width; int y = i / width; result[x, y] = new TilemapTile(gid, flags); } return result; } private static TilemapTile[,] DecodeCsvData(string csv, int width, int height) { TilemapTile[,] result = new TilemapTile[width, height]; if (string.IsNullOrWhiteSpace(csv)) { return result; } string[] values = csv.Split(new[] { ',', '\n', '\r', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < values.Length && i < width * height; i++) { if (!uint.TryParse(values[i].Trim(), out uint rawGid)) { throw new TilemapParseException($"Invalid tile GID in CSV data: '{values[i]}'"); } (int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(rawGid); int x = i % width; int y = i / width; result[x, y] = new TilemapTile(gid, flags); } return result; } private static TilemapTile[,] DecodeBase64Data(string base64, string compression, int width, int height) { if (string.IsNullOrWhiteSpace(base64)) { return new TilemapTile[width, height]; } byte[] bytes = Convert.FromBase64String(base64.Trim()); // Decompress if needed if (!string.IsNullOrEmpty(compression)) { bytes = compression.ToLowerInvariant() switch { "gzip" => DecompressGzip(bytes), "zlib" => DecompressZlib(bytes), _ => throw new TilemapParseException($"Unsupported compression format: '{compression}'") }; } // Parse uint32 array (little-endian) TilemapTile[,] result = new TilemapTile[width, height]; int tileCount = width * height; if (bytes.Length < tileCount * 4) { throw new TilemapParseException($"Insufficient tile data: expected {tileCount * 4} bytes, got {bytes.Length}"); } for (int i = 0; i < tileCount; i++) { uint rawGid = BitConverter.ToUInt32(bytes, i * 4); (int gid, TilemapTileFlipFlags flags) = ExtractFlipFlags(rawGid); int x = i % width; int y = i / width; result[x, y] = new TilemapTile(gid, flags); } return result; } private static TilemapTile[,] DecodeChunkedData(TiledTileLayerDataXml data, int width, int height) { TilemapTile[,] result = new TilemapTile[width, height]; foreach (TiledChunkXml chunk in data.Chunks) { // Decode chunk data (always CSV for chunks based on Tiled spec) TilemapTile[,] chunkTiles = DecodeCsvData(chunk.Value, chunk.Width, chunk.Height); // Copy chunk tiles into result at correct position for (int cy = 0; cy < chunk.Height; cy++) { for (int cx = 0; cx < chunk.Width; cx++) { int worldX = chunk.X + cx; int worldY = chunk.Y + cy; // Skip tiles outside map bounds if (worldX < 0 || worldX >= width || worldY < 0 || worldY >= height) { continue; } result[worldX, worldY] = chunkTiles[cx, cy]; } } } return result; } private static (int gid, TilemapTileFlipFlags flags) ExtractFlipFlags(uint rawGid) { // Extract GID (clear flip flags) int gid = (int)(rawGid & ~FLIP_MASK); // Extract flip flags TilemapTileFlipFlags flags = TilemapTileFlipFlags.None; if ((rawGid & FLIPPED_HORIZONTALLY_FLAG) != 0) { flags |= TilemapTileFlipFlags.FlipHorizontally; } if ((rawGid & FLIPPED_VERTICALLY_FLAG) != 0) { flags |= TilemapTileFlipFlags.FlipVertically; } if ((rawGid & FLIPPED_DIAGONALLY_FLAG) != 0) { flags |= TilemapTileFlipFlags.FlipDiagonally; } return (gid, flags); } private static byte[] DecompressGzip(byte[] data) { try { using var input = new MemoryStream(data); using var gzip = new GZipStream(input, CompressionMode.Decompress); using var output = new MemoryStream(); gzip.CopyTo(output); return output.ToArray(); } catch (Exception ex) { throw new TilemapParseException("Failed to decompress gzip data", ex); } } private static byte[] DecompressZlib(byte[] data) { try { // Zlib format = 2-byte header + DEFLATE stream + 4-byte Adler-32 checksum // Skip the 2-byte zlib header and 4-byte checksum at the end if (data.Length < 6) { throw new TilemapParseException($"Invalid zlib data: too short ({data.Length} bytes)"); } using var input = new MemoryStream(data, 2, data.Length - 6); using var deflate = new DeflateStream(input, CompressionMode.Decompress); using var output = new MemoryStream(); deflate.CopyTo(output); return output.ToArray(); } catch (Exception ex) { throw new TilemapParseException("Failed to decompress zlib data", ex); } } }