| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.IO.Compression;
- using MonoGame.Extended.Tilemaps.Parsers;
- namespace MonoGame.Extended.Tilemaps.Tiled;
- /// <summary>
- /// Decodes tile data from various Tiled encodings (CSV, XML, Base64, gzip, zlib).
- /// </summary>
- 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;
- /// <summary>
- /// Decodes tile data from various formats.
- /// </summary>
- /// <param name="data">The tile layer data XML.</param>
- /// <param name="width">The layer width in tiles.</param>
- /// <param name="height">The layer height in tiles.</param>
- /// <returns>A 2D array of tiles.</returns>
- 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<TiledDataTileXml> 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);
- }
- }
- }
|