| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Xml;
- using System.Xml.Serialization;
- using Microsoft.Xna.Framework.Graphics;
- using MonoGame.Extended.Tilemaps.Parsers;
- using MonoGame.Extended.Tilemaps.Tiled.Converters;
- namespace MonoGame.Extended.Tilemaps.Tiled;
- /// <summary>
- /// Parser for Tiled TMX (Tile Map XML) files.
- /// </summary>
- public class TiledTmxParser : ITilemapParser
- {
- private readonly string _baseDirectory;
- /// <summary>
- /// Initializes a new instance of the <see cref="TiledTmxParser"/> class.
- /// </summary>
- /// <param name="baseDirectory">
- /// Optional base directory for resolving relative file paths. If provided, file paths in
- /// <see cref="ParseFromFile"/> will be resolved relative to this directory.
- /// If <see langword="null"/>, paths are resolved from the file's own location.
- /// </param>
- public TiledTmxParser(string baseDirectory = null)
- {
- _baseDirectory = baseDirectory;
- }
- /// <inheritdoc/>
- public IReadOnlyList<string> SupportedExtensions => new[] { ".tmx" };
- /// <summary>
- /// Parses a Tiled TMX file from disk.
- /// </summary>
- /// <param name="path">The path to the TMX file.</param>
- /// <param name="graphicsDevice">The graphics device for loading textures.</param>
- /// <returns>The parsed tilemap.</returns>
- /// <exception cref="ArgumentNullException">Thrown when path or graphicsDevice is null.</exception>
- /// <exception cref="FileNotFoundException">Thrown when the file does not exist.</exception>
- /// <exception cref="TilemapParseException">Thrown when parsing fails.</exception>
- public Tilemap ParseFromFile(string path, GraphicsDevice graphicsDevice)
- {
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
- if (graphicsDevice == null)
- {
- throw new ArgumentNullException(nameof(graphicsDevice));
- }
- // Resolve full path using base directory if provided
- string fullPath = _baseDirectory != null
- ? Path.Combine(_baseDirectory, path)
- : path;
- if (!File.Exists(fullPath))
- {
- throw new FileNotFoundException($"Tilemap file not found: {fullPath}", fullPath);
- }
- try
- {
- // Get the directory of the TMX file for resolving relative paths
- string baseDirectory = Path.GetDirectoryName(Path.GetFullPath(fullPath));
- // Load and deserialize TMX file
- TiledMapXml mapXml;
- using (Stream stream = File.OpenRead(fullPath))
- {
- mapXml = DeserializeMap(stream);
- }
- // Load external tilesets (TSX files)
- LoadExternalTilesets(mapXml, baseDirectory);
- // Load textures for tilesets
- LoadTilesetTextures(mapXml, baseDirectory, graphicsDevice);
- // Load textures for image layers
- LoadImageLayerTextures(mapXml, baseDirectory, graphicsDevice);
- // Convert to public API
- Tilemap tilemap = TilemapConverter.Convert(mapXml);
- return tilemap;
- }
- catch (TilemapParseException)
- {
- throw;
- }
- catch (Exception ex)
- {
- throw new TilemapParseException($"Failed to parse TMX file: {fullPath}", ex);
- }
- }
- /// <summary>
- /// Parses a Tiled TMX file from a stream.
- /// </summary>
- /// <param name="stream">The stream containing TMX data.</param>
- /// <param name="graphicsDevice">The graphics device for loading textures.</param>
- /// <param name="basePath">
- /// Optional base path for resolving relative file references. If not provided,
- /// uses the base directory from the constructor, or the current directory if neither is set.
- /// </param>
- /// <returns>The parsed tilemap.</returns>
- /// <exception cref="ArgumentNullException">Thrown when stream or graphicsDevice is null.</exception>
- /// <exception cref="TilemapParseException">Thrown when parsing fails.</exception>
- public Tilemap ParseFromStream(Stream stream, GraphicsDevice graphicsDevice, string basePath = null)
- {
- if (stream == null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
- if (graphicsDevice == null)
- {
- throw new ArgumentNullException(nameof(graphicsDevice));
- }
- try
- {
- // Use provided basePath, fall back to constructor base directory, then current directory
- string baseDirectory = basePath ?? _baseDirectory ?? Directory.GetCurrentDirectory();
- // Deserialize TMX from stream
- TiledMapXml mapXml = DeserializeMap(stream);
- // Load external tilesets
- LoadExternalTilesets(mapXml, baseDirectory);
- // Load textures
- LoadTilesetTextures(mapXml, baseDirectory, graphicsDevice);
- LoadImageLayerTextures(mapXml, baseDirectory, graphicsDevice);
- // Convert to public API
- Tilemap tilemap = TilemapConverter.Convert(mapXml);
- return tilemap;
- }
- catch (TilemapParseException)
- {
- throw;
- }
- catch (Exception ex)
- {
- throw new TilemapParseException("Failed to parse TMX from stream", ex);
- }
- }
- private TiledMapXml DeserializeMap(Stream stream)
- {
- try
- {
- XmlSerializer serializer = new XmlSerializer(typeof(TiledMapXml));
- // Configure XML reader to ignore DTD (Document Type Definition)
- // TMX files often include DTD declarations which can cause security warnings
- XmlReaderSettings settings = new XmlReaderSettings
- {
- DtdProcessing = DtdProcessing.Ignore,
- XmlResolver = null
- };
- using (XmlReader reader = XmlReader.Create(stream, settings))
- {
- return (TiledMapXml)serializer.Deserialize(reader);
- }
- }
- catch (Exception ex)
- {
- throw new TilemapParseException("Failed to deserialize TMX XML", ex);
- }
- }
- private void LoadExternalTilesets(TiledMapXml mapXml, string baseDirectory)
- {
- if (mapXml.Tilesets == null)
- {
- return;
- }
- foreach (TiledTilesetRefXml tilesetRef in mapXml.Tilesets)
- {
- // Skip if not an external tileset
- if (string.IsNullOrEmpty(tilesetRef.Source))
- {
- continue;
- }
- // Resolve TSX file path
- string tsxPath = Path.Combine(baseDirectory, tilesetRef.Source);
- if (!File.Exists(tsxPath))
- {
- throw new TilemapParseException($"External tileset file not found: {tsxPath}");
- }
- // Load and deserialize TSX file
- TiledTilesetXml tilesetXml;
- using (Stream stream = File.OpenRead(tsxPath))
- {
- try
- {
- XmlSerializer serializer = new XmlSerializer(typeof(TiledTilesetXml));
- // Configure XML reader to ignore DTD
- XmlReaderSettings settings = new XmlReaderSettings
- {
- DtdProcessing = DtdProcessing.Ignore,
- XmlResolver = null
- };
- using (XmlReader reader = XmlReader.Create(stream, settings))
- {
- tilesetXml = (TiledTilesetXml)serializer.Deserialize(reader);
- }
- }
- catch (Exception ex)
- {
- throw new TilemapParseException($"Failed to parse TSX file: {tsxPath}", ex);
- }
- }
- // Store the tileset data (firstgid comes from the map, rest from TSX)
- tilesetXml.FirstGlobalId = tilesetRef.FirstGlobalId;
- tilesetRef.TilesetData = tilesetXml;
- }
- }
- private void LoadTilesetTextures(TiledMapXml mapXml, string baseDirectory, GraphicsDevice graphicsDevice)
- {
- if (mapXml.Tilesets == null)
- {
- return;
- }
- foreach (TiledTilesetRefXml tilesetRef in mapXml.Tilesets)
- {
- // Get the actual tileset data (from external TSX or inline)
- // TilesetData is non-null for external tilesets, null for inline
- TiledTilesetXml tilesetXml = tilesetRef.TilesetData ?? tilesetRef;
- // Load main tileset image
- if (tilesetXml.Image != null && !string.IsNullOrEmpty(tilesetXml.Image.Source))
- {
- string imagePath = Path.Combine(baseDirectory, tilesetXml.Image.Source);
- tilesetXml.Image.Texture = LoadTexture(imagePath, graphicsDevice);
- }
- // Load individual tile images (for image collection tilesets)
- if (tilesetXml.Tiles != null)
- {
- foreach (TiledTileXml tile in tilesetXml.Tiles)
- {
- if (tile.Image != null && !string.IsNullOrEmpty(tile.Image.Source))
- {
- string imagePath = Path.Combine(baseDirectory, tile.Image.Source);
- tile.Image.Texture = LoadTexture(imagePath, graphicsDevice);
- }
- }
- }
- }
- }
- private void LoadImageLayerTextures(TiledMapXml mapXml, string baseDirectory, GraphicsDevice graphicsDevice)
- {
- if (mapXml.Layers == null)
- {
- return;
- }
- foreach (TiledLayerXml layer in mapXml.Layers)
- {
- LoadImageLayerTexturesRecursive(layer, baseDirectory, graphicsDevice);
- }
- }
- private void LoadImageLayerTexturesRecursive(TiledLayerXml layer, string baseDirectory, GraphicsDevice graphicsDevice)
- {
- // Handle image layers
- if (layer is TiledImageLayerXml imageLayer)
- {
- if (imageLayer.Image != null && !string.IsNullOrEmpty(imageLayer.Image.Source))
- {
- string imagePath = Path.Combine(baseDirectory, imageLayer.Image.Source);
- imageLayer.Image.Texture = LoadTexture(imagePath, graphicsDevice);
- }
- }
- // Recursively handle group layers
- if (layer is TiledGroupLayerXml groupLayer && groupLayer.Layers != null)
- {
- foreach (TiledLayerXml childLayer in groupLayer.Layers)
- {
- LoadImageLayerTexturesRecursive(childLayer, baseDirectory, graphicsDevice);
- }
- }
- }
- private Texture2D LoadTexture(string filePath, GraphicsDevice graphicsDevice)
- {
- if (!File.Exists(filePath))
- {
- throw new TilemapParseException($"Texture file not found: {filePath}");
- }
- try
- {
- using (var stream = File.OpenRead(filePath))
- {
- return Texture2D.FromStream(graphicsDevice, stream);
- }
- }
- catch (Exception ex)
- {
- throw new TilemapParseException($"Failed to load texture: {filePath}", ex);
- }
- }
- }
|