summaryrefslogtreecommitdiff
path: root/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled
diff options
context:
space:
mode:
authorchai <215380520@qq.com>2024-06-03 10:15:45 +0800
committerchai <215380520@qq.com>2024-06-03 10:15:45 +0800
commitacea7b2e728787a0d83bbf83c8c1f042d2c32e7e (patch)
tree0bfec05c1ca2d71be2c337bcd110a0421f19318b /Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled
parent88febcb02bf127d961c6471d9e846c0e1315f5c3 (diff)
+ plugins project
Diffstat (limited to 'Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled')
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs108
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs324
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs145
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs227
11 files changed, 1036 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs
new file mode 100644
index 0000000..28ded17
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public static class ContentWriterExtensions
+ {
+ // ReSharper disable once SuggestBaseTypeForParameter
+ public static void WriteTiledMapProperties(this ContentWriter writer, IReadOnlyCollection<TiledMapPropertyContent> value)
+ {
+ if (value == null)
+ {
+ writer.Write(0);
+ return;
+ }
+ writer.Write(value.Count);
+ foreach (var property in value)
+ {
+ writer.Write(property.Name);
+ writer.Write(property.Value ?? string.Empty);
+ WriteTiledMapProperties(writer, property.Properties);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs
new file mode 100644
index 0000000..b376252
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs
@@ -0,0 +1,22 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled;
+
+public class TiledContentItem<T>: ContentItem<T>
+{
+ public TiledContentItem(T data) : base(data)
+ {
+ }
+
+ public void BuildExternalReference<T>(ContentProcessorContext context, TiledMapImageContent image)
+ {
+ var parameters = new OpaqueDataDictionary
+ {
+ { "ColorKeyColor", image.TransparentColor },
+ { "ColorKeyEnabled", true }
+ };
+ BuildExternalReference<Texture2DContent>(context, image.Source, parameters);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs
new file mode 100644
index 0000000..b0b7b5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs
@@ -0,0 +1,12 @@
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public class TiledMapContentItem : TiledContentItem<TiledMapContent>
+ {
+ public TiledMapContentItem(TiledMapContent data)
+ : base(data)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs
new file mode 100644
index 0000000..70e2bee
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Serialization;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tmx", DefaultProcessor = "TiledMapProcessor", DisplayName = "Tiled Map Importer - MonoGame.Extended")]
+ public class TiledMapImporter : ContentImporter<TiledMapContentItem>
+ {
+ public override TiledMapContentItem Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var map = DeserializeTiledMapContent(filePath, context);
+
+ if (map.Width > ushort.MaxValue || map.Height > ushort.MaxValue)
+ throw new InvalidContentException($"The map '{filePath} is much too large. The maximum supported width and height for a Tiled map is {ushort.MaxValue}.");
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return new TiledMapContentItem(map);
+
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ throw;
+ }
+ }
+
+ private static TiledMapContent DeserializeTiledMapContent(string mapFilePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(mapFilePath))
+ {
+ var mapSerializer = new XmlSerializer(typeof(TiledMapContent));
+ var map = (TiledMapContent)mapSerializer.Deserialize(reader);
+
+ map.FilePath = mapFilePath;
+
+ for (var i = 0; i < map.Tilesets.Count; i++)
+ {
+ var tileset = map.Tilesets[i];
+
+ string getTilesetSource(string source)
+ => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(mapFilePath), source));
+
+ if (!string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ tileset.Source = getTilesetSource(tileset.Source);
+ ContentLogger.Log($"Adding dependency for {tileset.Source}");
+ // We depend on the tileset. If the tileset changes, the map also needs to rebuild.
+ context.AddDependency(tileset.Source);
+ }
+ else
+ {
+ tileset.Image.Source = getTilesetSource(tileset.Image.Source);
+ ContentLogger.Log($"Adding dependency for {tileset.Image.Source}");
+ context.AddDependency(tileset.Image.Source);
+ }
+ }
+
+ ImportLayers(context, map.Layers, Path.GetDirectoryName(mapFilePath));
+
+ map.Name = mapFilePath;
+ return map;
+ }
+ }
+
+ private static void ImportLayers(ContentImporterContext context, List<TiledMapLayerContent> layers, string path)
+ {
+ for (var i = 0; i < layers.Count; i++)
+ {
+ if (layers[i] is TiledMapImageLayerContent imageLayer)
+ {
+ imageLayer.Image.Source = Path.Combine(path, imageLayer.Image.Source);
+ ContentLogger.Log($"Adding dependency for '{imageLayer.Image.Source}'");
+
+ // Tell the pipeline that we depend on this image and need to rebuild the map if the image changes.
+ // (Maybe the image is a different size)
+ context.AddDependency(imageLayer.Image.Source);
+ }
+ if (layers[i] is TiledMapObjectLayerContent objectLayer)
+ foreach (var obj in objectLayer.Objects)
+ if (!String.IsNullOrWhiteSpace(obj.TemplateSource))
+ {
+ obj.TemplateSource = Path.Combine(path, obj.TemplateSource);
+ ContentLogger.Log($"Adding dependency for '{obj.TemplateSource}'");
+ // Tell the pipeline that we depend on this template and need to rebuild the map if the template changes.
+ // (Templates are loaded into objects on process, so all objects which depend on the template file
+ // need the change to the template)
+ context.AddDependency(obj.TemplateSource);
+ }
+ if (layers[i] is TiledMapGroupLayerContent groupLayer)
+ // Yay recursion!
+ ImportLayers(context, groupLayer.Layers, path);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs
new file mode 100644
index 0000000..eeddcaa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs
@@ -0,0 +1,54 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tx", DefaultProcessor = "TiledMapObjectTemplateProcessor", DisplayName = "Tiled Map Object Template Importer - MonoGame.Extended")]
+ public class TiledMapObjectTemplateImporter : ContentImporter<TiledMapObjectTemplateContent>
+ {
+ public override TiledMapObjectTemplateContent Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var template = DeserializeTileMapObjectTemplateContent(filePath, context);
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return template;
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ return null;
+ }
+ }
+
+ private static TiledMapObjectTemplateContent DeserializeTileMapObjectTemplateContent(string filePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(filePath))
+ {
+ var templateSerializer = new XmlSerializer(typeof(TiledMapObjectTemplateContent));
+ var template = (TiledMapObjectTemplateContent)templateSerializer.Deserialize(reader);
+
+ if (!string.IsNullOrWhiteSpace(template.Tileset?.Source))
+ {
+ template.Tileset.Source = Path.Combine(Path.GetDirectoryName(filePath), template.Tileset.Source);
+ ContentLogger.Log($"Adding dependency '{template.Tileset.Source}'");
+ // We depend on this tileset.
+ context.AddDependency(template.Tileset.Source);
+ }
+
+ return template;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs
new file mode 100644
index 0000000..9668a72
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled;
+using MonoGame.Extended.Tiled.Serialization;
+using MonoGame.Framework.Utilities.Deflate;
+using CompressionMode = System.IO.Compression.CompressionMode;
+using GZipStream = System.IO.Compression.GZipStream;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public static class TiledMapContentHelper
+ {
+ public static void Process(TiledMapObjectContent obj, ContentProcessorContext context)
+ {
+ if (!string.IsNullOrWhiteSpace(obj.TemplateSource))
+ {
+ var externalReference = new ExternalReference<TiledMapObjectLayerContent>(obj.TemplateSource);
+ var template = context.BuildAndLoadAsset<TiledMapObjectLayerContent, TiledMapObjectTemplateContent>(externalReference, "");
+
+ // Nothing says a template can't reference another template.
+ // Yay recusion!
+ Process(template.Object, context);
+
+ if (!obj._globalIdentifier.HasValue && template.Object._globalIdentifier.HasValue)
+ obj.GlobalIdentifier = template.Object.GlobalIdentifier;
+
+ if (!obj._height.HasValue && template.Object._height.HasValue)
+ obj.Height = template.Object.Height;
+
+ if (!obj._identifier.HasValue && template.Object._identifier.HasValue)
+ obj.Identifier = template.Object.Identifier;
+
+ if (!obj._rotation.HasValue && template.Object._rotation.HasValue)
+ obj.Rotation = template.Object.Rotation;
+
+ if (!obj._visible.HasValue && template.Object._visible.HasValue)
+ obj.Visible = template.Object.Visible;
+
+ if (!obj._width.HasValue && template.Object._width.HasValue)
+ obj.Width = template.Object.Width;
+
+ if (!obj._x.HasValue && template.Object._x.HasValue)
+ obj.X = template.Object.X;
+
+ if (!obj._y.HasValue && template.Object._y.HasValue)
+ obj.Y = template.Object.Y;
+
+ if (obj.Ellipse == null && template.Object.Ellipse != null)
+ obj.Ellipse = template.Object.Ellipse;
+
+ if (string.IsNullOrWhiteSpace(obj.Name) && !string.IsNullOrWhiteSpace(template.Object.Name))
+ obj.Name = template.Object.Name;
+
+ if (obj.Polygon == null && template.Object.Polygon != null)
+ obj.Polygon = template.Object.Polygon;
+
+ if (obj.Polyline == null && template.Object.Polyline != null)
+ obj.Polyline = template.Object.Polyline;
+
+ foreach (var tProperty in template.Object.Properties)
+ {
+ if (!obj.Properties.Exists(p => p.Name == tProperty.Name))
+ obj.Properties.Add(tProperty);
+ }
+
+ if (string.IsNullOrWhiteSpace(obj.Type) && !string.IsNullOrWhiteSpace(template.Object.Type))
+ obj.Type = template.Object.Type;
+
+ if (string.IsNullOrWhiteSpace(obj.Class) && !string.IsNullOrWhiteSpace(template.Object.Class))
+ obj.Class = template.Object.Class;
+ }
+ }
+ }
+
+
+ [ContentProcessor(DisplayName = "Tiled Map Processor - MonoGame.Extended")]
+ public class TiledMapProcessor : ContentProcessor<TiledMapContentItem, TiledMapContentItem>
+ {
+ public override TiledMapContentItem Process(TiledMapContentItem contentItem, ContentProcessorContext context)
+ {
+ try
+ {
+ ContentLogger.Logger = context.Logger;
+ var map = contentItem.Data;
+
+ if (map.Orientation == TiledMapOrientationContent.Hexagonal || map.Orientation == TiledMapOrientationContent.Staggered)
+ throw new NotSupportedException($"{map.Orientation} Tiled Maps are currently not implemented!");
+
+ foreach (var tileset in map.Tilesets)
+ {
+ if (string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ // Load the Texture2DContent for the tileset as it will be saved into the map content file.
+ contentItem.BuildExternalReference<Texture2DContent>(context, tileset.Image);
+ }
+ else
+ {
+ // Link to the tileset for the content loader to load at runtime.
+ //var externalReference = new ExternalReference<TiledMapTilesetContent>(tileset.Source);
+ //tileset.Content = context.BuildAsset<TiledMapTilesetContent, TiledMapTilesetContent>(externalReference, "");
+ contentItem.BuildExternalReference<TiledMapTilesetContent>(context, tileset.Source);
+ }
+ }
+
+ ProcessLayers(contentItem, map, context, map.Layers);
+
+ return contentItem;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogImportantMessage(ex.Message);
+ throw;
+ }
+ }
+
+ private static void ProcessLayers(TiledMapContentItem contentItem, TiledMapContent map, ContentProcessorContext context, List<TiledMapLayerContent> layers)
+ {
+ foreach (var layer in layers)
+ {
+ switch (layer)
+ {
+ case TiledMapImageLayerContent imageLayer:
+ ContentLogger.Log($"Processing image layer '{imageLayer.Name}'");
+ contentItem.BuildExternalReference<Texture2DContent>(context, imageLayer.Image);
+ ContentLogger.Log($"Processed image layer '{imageLayer.Name}'");
+ break;
+
+ case TiledMapTileLayerContent tileLayer when tileLayer.Data.Chunks.Count > 0:
+ throw new NotSupportedException($"{map.FilePath} contains data chunks. These are currently not supported.");
+
+ case TiledMapTileLayerContent tileLayer:
+ var data = tileLayer.Data;
+ var encodingType = data.Encoding ?? "xml";
+ var compressionType = data.Compression ?? "xml";
+
+ ContentLogger.Log($"Processing tile layer '{tileLayer.Name}': Encoding: '{encodingType}', Compression: '{compressionType}'");
+ var tileData = DecodeTileLayerData(encodingType, tileLayer);
+ var tiles = CreateTiles(map.RenderOrder, map.Width, map.Height, tileData);
+ tileLayer.Tiles = tiles;
+ ContentLogger.Log($"Processed tile layer '{tileLayer}': {tiles.Length} tiles");
+ break;
+
+ case TiledMapObjectLayerContent objectLayer:
+ ContentLogger.Log($"Processing object layer '{objectLayer.Name}'");
+
+ foreach (var obj in objectLayer.Objects)
+ TiledMapContentHelper.Process(obj, context);
+
+ ContentLogger.Log($"Processed object layer '{objectLayer.Name}'");
+ break;
+
+ case TiledMapGroupLayerContent groupLayer:
+ ProcessLayers(contentItem, map, context, groupLayer.Layers);
+ break;
+ }
+ }
+ }
+
+ private static List<TiledMapTileContent> DecodeTileLayerData(string encodingType, TiledMapTileLayerContent tileLayer)
+ {
+ List<TiledMapTileContent> tiles;
+
+ switch (encodingType)
+ {
+ case "xml":
+ tiles = tileLayer.Data.Tiles;
+ break;
+ case "csv":
+ tiles = DecodeCommaSeperatedValuesData(tileLayer.Data);
+ break;
+ case "base64":
+ tiles = DecodeBase64Data(tileLayer.Data, tileLayer.Width, tileLayer.Height);
+ break;
+ default:
+ throw new NotSupportedException($"The tile layer encoding '{encodingType}' is not supported.");
+ }
+
+ return tiles;
+ }
+
+ private static TiledMapTile[] CreateTiles(TiledMapTileDrawOrderContent renderOrder, int mapWidth, int mapHeight, List<TiledMapTileContent> tileData)
+ {
+ TiledMapTile[] tiles;
+
+ switch (renderOrder)
+ {
+ case TiledMapTileDrawOrderContent.LeftDown:
+ tiles = CreateTilesInLeftDownOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.LeftUp:
+ tiles = CreateTilesInLeftUpOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.RightDown:
+ tiles = CreateTilesInRightDownOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.RightUp:
+ tiles = CreateTilesInRightUpOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ default:
+ throw new NotSupportedException($"{renderOrder} is not supported.");
+ }
+
+ return tiles.ToArray();
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInLeftDownOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = 0; y < mapHeight; y++)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInLeftUpOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = mapHeight - 1; y >= 0; y--)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInRightDownOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = 0; y < mapHeight; y++)
+ {
+ for (var x = 0; x < mapWidth; x++)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInRightUpOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = mapHeight - 1; y >= 0; y--)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static List<TiledMapTileContent> DecodeBase64Data(TiledMapTileLayerDataContent data, int width, int height)
+ {
+ var tileList = new List<TiledMapTileContent>();
+ var encodedData = data.Value.Trim();
+ var decodedData = Convert.FromBase64String(encodedData);
+
+ using (var stream = OpenStream(decodedData, data.Compression))
+ {
+ using (var reader = new BinaryReader(stream))
+ {
+ data.Tiles = new List<TiledMapTileContent>();
+
+ for (var y = 0; y < width; y++)
+ {
+ for (var x = 0; x < height; x++)
+ {
+ var gid = reader.ReadUInt32();
+ tileList.Add(new TiledMapTileContent
+ {
+ GlobalIdentifier = gid
+ });
+ }
+ }
+ }
+ }
+
+ return tileList;
+ }
+
+ private static List<TiledMapTileContent> DecodeCommaSeperatedValuesData(TiledMapTileLayerDataContent data)
+ {
+ return data.Value
+ .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(uint.Parse)
+ .Select(x => new TiledMapTileContent { GlobalIdentifier = x })
+ .ToList();
+ }
+
+ private static Stream OpenStream(byte[] decodedData, string compressionMode)
+ {
+ var memoryStream = new MemoryStream(decodedData, false);
+
+ return compressionMode switch
+ {
+ "gzip" => new GZipStream(memoryStream, CompressionMode.Decompress),
+ "zlib" => new ZlibStream(memoryStream, Framework.Utilities.Deflate.CompressionMode.Decompress),
+ _ => memoryStream
+ };
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs
new file mode 100644
index 0000000..d4b2221
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public class TiledMapTilesetContentItem : TiledContentItem<TiledMapTilesetContent>
+ {
+ public TiledMapTilesetContentItem(TiledMapTilesetContent data)
+ : base(data)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs
new file mode 100644
index 0000000..c848bde
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs
@@ -0,0 +1,60 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tsx", DefaultProcessor = "TiledMapTilesetProcessor", DisplayName = "Tiled Map Tileset Importer - MonoGame.Extended")]
+ public class TiledMapTilesetImporter : ContentImporter<TiledMapTilesetContentItem>
+ {
+ public override TiledMapTilesetContentItem Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var tileset = DeserializeTiledMapTilesetContent(filePath, context);
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return new TiledMapTilesetContentItem(tileset);
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ throw;
+ }
+ }
+
+ private TiledMapTilesetContent DeserializeTiledMapTilesetContent(string filePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(filePath))
+ {
+ var tilesetSerializer = new XmlSerializer(typeof(TiledMapTilesetContent));
+ var tileset = (TiledMapTilesetContent)tilesetSerializer.Deserialize(reader);
+
+ if (tileset.Image is not null)
+ tileset.Image.Source = context.AddDependencyWithLogging(filePath, tileset.Image.Source);
+
+ foreach (var tile in tileset.Tiles)
+ {
+ foreach (var obj in tile.Objects)
+ {
+ if (!string.IsNullOrWhiteSpace(obj.TemplateSource))
+ obj.TemplateSource = context.AddDependencyWithLogging(filePath, obj.TemplateSource);
+ }
+ if (tile.Image is not null)
+ tile.Image.Source = context.AddDependencyWithLogging(filePath, tile.Image.Source);
+ }
+
+ return tileset;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs
new file mode 100644
index 0000000..5682602
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs
@@ -0,0 +1,44 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using System;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentProcessor(DisplayName = "Tiled Map Tileset Processor - MonoGame.Extended")]
+ public class TiledMapTilesetProcessor : ContentProcessor<TiledMapTilesetContentItem, TiledMapTilesetContentItem>
+ {
+ public override TiledMapTilesetContentItem Process(TiledMapTilesetContentItem contentItem, ContentProcessorContext context)
+ {
+ try
+ {
+ var tileset = contentItem.Data;
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Processing tileset '{tileset.Name}'");
+
+ // Build the Texture2D asset and load it as it will be saved as part of this tileset file.
+ if (tileset.Image is not null)
+ contentItem.BuildExternalReference<Texture2DContent>(context, tileset.Image);
+
+ foreach (var tile in tileset.Tiles)
+ {
+ foreach (var obj in tile.Objects)
+ {
+ TiledMapContentHelper.Process(obj, context);
+ }
+ if (tile.Image is not null)
+ contentItem.BuildExternalReference<Texture2DContent>(context, tile.Image);
+ }
+
+ ContentLogger.Log($"Processed tileset '{tileset.Name}'");
+
+ return contentItem;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogImportantMessage(ex.Message);
+ throw ex;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs
new file mode 100644
index 0000000..48690ad
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs
@@ -0,0 +1,145 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled;
+using System;
+using System.Globalization;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentTypeWriter]
+ public class TiledMapTilesetWriter : ContentTypeWriter<TiledMapTilesetContentItem>
+ {
+ public override string GetRuntimeReader(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapTilesetReader, MonoGame.Extended.Tiled";
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapTileset, MonoGame.Extended.Tiled";
+
+ protected override void Write(ContentWriter writer, TiledMapTilesetContentItem contentItem)
+ {
+ try
+ {
+ WriteTileset(writer, contentItem.Data, contentItem);
+ }
+ catch (Exception ex)
+ {
+ ContentLogger.Logger.LogImportantMessage(ex.StackTrace);
+ throw;
+ }
+ }
+
+ public static void WriteTileset(ContentWriter writer, TiledMapTilesetContent tileset, IExternalReferenceRepository externalReferenceRepository)
+ {
+ var externalReference = externalReferenceRepository.GetExternalReference<Texture2DContent>(tileset.Image?.Source);
+ writer.WriteExternalReference(externalReference);
+ writer.Write(tileset.Class ?? tileset.Type ?? string.Empty);
+ writer.Write(tileset.TileWidth);
+ writer.Write(tileset.TileHeight);
+ writer.Write(tileset.TileCount);
+ writer.Write(tileset.Spacing);
+ writer.Write(tileset.Margin);
+ writer.Write(tileset.Columns);
+ writer.Write(tileset.Tiles.Count);
+
+ foreach (var tilesetTile in tileset.Tiles)
+ WriteTilesetTile(writer, tilesetTile, externalReferenceRepository);
+
+ writer.WriteTiledMapProperties(tileset.Properties);
+ }
+
+ private static void WriteTilesetTile(ContentWriter writer, TiledMapTilesetTileContent tilesetTile,
+ IExternalReferenceRepository externalReferenceRepository)
+ {
+ var externalReference = externalReferenceRepository.GetExternalReference<Texture2DContent>(tilesetTile.Image?.Source);
+ writer.WriteExternalReference(externalReference);
+
+ writer.Write(tilesetTile.LocalIdentifier);
+ writer.Write(tilesetTile.Type);
+ writer.Write(tilesetTile.Frames.Count);
+ writer.Write(tilesetTile.Objects.Count);
+
+ foreach (var @object in tilesetTile.Objects)
+ WriteObject(writer, @object);
+
+ foreach (var frame in tilesetTile.Frames)
+ {
+ writer.Write(frame.TileIdentifier);
+ writer.Write(frame.Duration);
+ }
+
+ writer.WriteTiledMapProperties(tilesetTile.Properties);
+ }
+
+ private static void WriteObject(ContentWriter writer, TiledMapObjectContent @object)
+ {
+ var type = GetObjectType(@object);
+
+ writer.Write((byte)type);
+
+ writer.Write(@object.Identifier);
+ writer.Write(@object.Name ?? string.Empty);
+ writer.Write(@object.Class ?? @object.Type ?? string.Empty);
+ writer.Write(@object.X);
+ writer.Write(@object.Y);
+ writer.Write(@object.Width);
+ writer.Write(@object.Height);
+ writer.Write(@object.Rotation);
+ writer.Write(@object.Visible);
+
+ writer.WriteTiledMapProperties(@object.Properties);
+
+ switch (type)
+ {
+ case TiledMapObjectType.Rectangle:
+ case TiledMapObjectType.Ellipse:
+ break;
+ case TiledMapObjectType.Tile:
+ writer.Write(@object.GlobalIdentifier);
+ break;
+ case TiledMapObjectType.Polygon:
+ WritePolyPoints(writer, @object.Polygon.Points);
+ break;
+ case TiledMapObjectType.Polyline:
+ WritePolyPoints(writer, @object.Polyline.Points);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WritePolyPoints(ContentWriter writer, string @string)
+ {
+ var stringPoints = @string.Split(' ');
+
+ writer.Write(stringPoints.Length);
+
+ foreach (var stringPoint in stringPoints)
+ {
+ var xy = stringPoint.Split(',');
+ var x = float.Parse(xy[0], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(x);
+ var y = float.Parse(xy[1], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(y);
+ }
+ }
+
+ public static TiledMapObjectType GetObjectType(TiledMapObjectContent content)
+ {
+ if (content.GlobalIdentifier > 0)
+ return TiledMapObjectType.Tile;
+
+ if (content.Ellipse != null)
+ return TiledMapObjectType.Ellipse;
+
+ if (content.Polygon != null)
+ return TiledMapObjectType.Polygon;
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement
+ if (content.Polyline != null)
+ return TiledMapObjectType.Polyline;
+
+ return TiledMapObjectType.Rectangle;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs
new file mode 100644
index 0000000..126debb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentTypeWriter]
+ public class TiledMapWriter : ContentTypeWriter<TiledMapContentItem>
+ {
+ private TiledMapContentItem _contentItem;
+
+ protected override void Write(ContentWriter writer, TiledMapContentItem contentItem)
+ {
+ _contentItem = contentItem;
+
+ var map = contentItem.Data;
+
+ try
+ {
+ WriteMetaData(writer, map);
+ WriteTilesets(writer, map.Tilesets);
+ WriteLayers(writer, map.Layers);
+ }
+ catch (Exception ex)
+ {
+ ContentLogger.Logger.LogImportantMessage(ex.StackTrace);
+ throw;
+ }
+ }
+
+ private static void WriteMetaData(ContentWriter writer, TiledMapContent map)
+ {
+ writer.Write(map.Class ?? map.Type ?? string.Empty);
+ writer.Write(map.Width);
+ writer.Write(map.Height);
+ writer.Write(map.TileWidth);
+ writer.Write(map.TileHeight);
+ writer.Write(ColorHelper.FromHex(map.BackgroundColor));
+ writer.Write((byte)map.RenderOrder);
+ writer.Write((byte)map.Orientation);
+ writer.WriteTiledMapProperties(map.Properties);
+ }
+
+ private void WriteTilesets(ContentWriter writer, IReadOnlyCollection<TiledMapTilesetContent> tilesets)
+ {
+ writer.Write(tilesets.Count);
+
+ foreach (var tileset in tilesets)
+ WriteTileset(writer, tileset);
+ }
+
+ private void WriteTileset(ContentWriter writer, TiledMapTilesetContent tileset)
+ {
+ writer.Write(tileset.FirstGlobalIdentifier);
+
+ if (!string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ writer.Write(true);
+ writer.WriteExternalReference(_contentItem.GetExternalReference<TiledMapTilesetContent>(tileset.Source));
+ }
+ else
+ {
+ writer.Write(false);
+ TiledMapTilesetWriter.WriteTileset(writer, tileset, _contentItem);
+ }
+ }
+
+ private void WriteLayers(ContentWriter writer, IReadOnlyCollection<TiledMapLayerContent> layers)
+ {
+ writer.Write(layers.Count);
+
+ foreach (var layer in layers)
+ WriteLayer(writer, layer);
+ }
+
+ private void WriteLayer(ContentWriter writer, TiledMapLayerContent layer)
+ {
+ writer.Write((byte)layer.LayerType);
+
+ writer.Write(layer.Name ?? string.Empty);
+ writer.Write(layer.Class ?? layer.Type ?? string.Empty);
+ writer.Write(layer.Visible);
+ writer.Write(layer.Opacity);
+ writer.Write(layer.OffsetX);
+ writer.Write(layer.OffsetY);
+ writer.Write(layer.ParallaxX);
+ writer.Write(layer.ParallaxY);
+
+ writer.WriteTiledMapProperties(layer.Properties);
+
+ switch (layer.LayerType)
+ {
+ case TiledMapLayerType.ImageLayer:
+ WriteImageLayer(writer, (TiledMapImageLayerContent)layer);
+ break;
+ case TiledMapLayerType.TileLayer:
+ WriteTileLayer(writer, (TiledMapTileLayerContent)layer);
+ break;
+ case TiledMapLayerType.ObjectLayer:
+ WriteObjectLayer(writer, (TiledMapObjectLayerContent)layer);
+ break;
+ case TiledMapLayerType.GroupLayer:
+ WriteLayers(writer, ((TiledMapGroupLayerContent)layer).Layers);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(layer.LayerType));
+ }
+ }
+
+ private void WriteImageLayer(ContentWriter writer, TiledMapImageLayerContent imageLayer)
+ {
+ var externalReference = _contentItem.GetExternalReference<Texture2DContent>(imageLayer.Image.Source);
+ writer.WriteExternalReference(externalReference);
+ writer.Write(new Vector2(imageLayer.X, imageLayer.Y));
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WriteTileLayer(ContentWriter writer, TiledMapTileLayerContent tileLayer)
+ {
+ writer.Write(tileLayer.Width);
+ writer.Write(tileLayer.Height);
+
+ writer.Write(tileLayer.Tiles.Length);
+
+ foreach (var tile in tileLayer.Tiles)
+ {
+ writer.Write(tile.GlobalTileIdentifierWithFlags);
+ writer.Write(tile.X);
+ writer.Write(tile.Y);
+ }
+ }
+
+ private static void WriteObjectLayer(ContentWriter writer, TiledMapObjectLayerContent layer)
+ {
+ writer.Write(ColorHelper.FromHex(layer.Color));
+ writer.Write((byte)layer.DrawOrder);
+
+ writer.Write(layer.Objects.Count);
+
+ foreach (var @object in layer.Objects)
+ WriteObject(writer, @object);
+ }
+
+
+ private static void WriteObject(ContentWriter writer, TiledMapObjectContent @object)
+ {
+ var type = GetObjectType(@object);
+
+ writer.Write((byte)type);
+
+ writer.Write(@object.Identifier);
+ writer.Write(@object.Name ?? string.Empty);
+ writer.Write(@object.Class ?? @object.Type ?? string.Empty);
+ writer.Write(@object.X);
+ writer.Write(@object.Y);
+ writer.Write(@object.Width);
+ writer.Write(@object.Height);
+ writer.Write(@object.Rotation);
+ writer.Write(@object.Visible);
+
+ writer.WriteTiledMapProperties(@object.Properties);
+
+ switch (type)
+ {
+ case TiledMapObjectType.Rectangle:
+ case TiledMapObjectType.Ellipse:
+ break;
+ case TiledMapObjectType.Tile:
+ writer.Write(@object.GlobalIdentifier);
+ break;
+ case TiledMapObjectType.Polygon:
+ WritePolyPoints(writer, @object.Polygon.Points);
+ break;
+ case TiledMapObjectType.Polyline:
+ WritePolyPoints(writer, @object.Polyline.Points);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WritePolyPoints(ContentWriter writer, string @string)
+ {
+ var stringPoints = @string.Split(' ');
+
+ writer.Write(stringPoints.Length);
+
+ foreach (var stringPoint in stringPoints)
+ {
+ var xy = stringPoint.Split(',');
+ var x = float.Parse(xy[0], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(x);
+ var y = float.Parse(xy[1], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(y);
+ }
+ }
+
+ public static TiledMapObjectType GetObjectType(TiledMapObjectContent content)
+ {
+ if (content.GlobalIdentifier > 0)
+ return TiledMapObjectType.Tile;
+
+ if (content.Ellipse != null)
+ return TiledMapObjectType.Ellipse;
+
+ if (content.Polygon != null)
+ return TiledMapObjectType.Polygon;
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement
+ if (content.Polyline != null)
+ return TiledMapObjectType.Polyline;
+
+ return TiledMapObjectType.Rectangle;
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMap, MonoGame.Extended.Tiled";
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapReader, MonoGame.Extended.Tiled";
+ }
+}