Browse Source

Tilemap Importer

Nick Kastellanos 4 years ago
parent
commit
ef0929db0a

+ 7 - 0
Aether.Extras.WINDOWS.MG.sln

@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aether.Graphics.WINDOWS.MG"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnimationImporters.WINDOWS.MG", "Content.Pipeline\AnimationImporters\AnimationImporters.WINDOWS.MG.csproj", "{D9A47306-DEE0-4410-BC2C-BA8FFCE682A3}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TilemapImporter.WINDOWS.MG", "Content.Pipeline\TilemapImporters\TilemapImporter.WINDOWS.MG.csproj", "{9B0F9C6B-3C43-472D-B0C1-91E11A9FDE89}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aether.Animation.WINDOWS.MG", "Animation\Aether.Animation.WINDOWS.MG.csproj", "{F08D6D4C-60FB-4543-8D81-594080EB8051}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aether.Shaders", "Aether.Shaders", "{13D47E11-4A7C-49C8-942E-2543E9C0098A}"
@@ -63,6 +65,10 @@ Global
 		{D9A47306-DEE0-4410-BC2C-BA8FFCE682A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{D9A47306-DEE0-4410-BC2C-BA8FFCE682A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{D9A47306-DEE0-4410-BC2C-BA8FFCE682A3}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9B0F9C6B-3C43-472D-B0C1-91E11A9FDE89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9B0F9C6B-3C43-472D-B0C1-91E11A9FDE89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9B0F9C6B-3C43-472D-B0C1-91E11A9FDE89}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9B0F9C6B-3C43-472D-B0C1-91E11A9FDE89}.Release|Any CPU.Build.0 = Release|Any CPU
 		{F08D6D4C-60FB-4543-8D81-594080EB8051}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{F08D6D4C-60FB-4543-8D81-594080EB8051}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{F08D6D4C-60FB-4543-8D81-594080EB8051}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -112,6 +118,7 @@ Global
 		{48E4029A-115C-4DC2-AF3A-0AB94F36BFC0} = {A921886B-C6F7-4FF8-8668-EC20004C464A}
 		{400DC7B2-739D-4156-916D-2F2E1920310D} = {A921886B-C6F7-4FF8-8668-EC20004C464A}
 		{D9A47306-DEE0-4410-BC2C-BA8FFCE682A3} = {A921886B-C6F7-4FF8-8668-EC20004C464A}
+		{9B0F9C6B-3C43-472D-B0C1-91E11A9FDE89} = {A921886B-C6F7-4FF8-8668-EC20004C464A}
 		{FBBDE2BA-F9F3-4041-8584-2C912C235E26} = {13D47E11-4A7C-49C8-942E-2543E9C0098A}
 		{96105100-20DB-4187-9BCA-0A20AC9F1298} = {13D47E11-4A7C-49C8-942E-2543E9C0098A}
 		{E710FBEA-8C75-405D-B6B4-CFC82CB48FB5} = {A921886B-C6F7-4FF8-8668-EC20004C464A}

+ 9 - 0
Aether.Extras.WINDOWS.XNA.sln

@@ -9,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aether.Content.Pipeline", "
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnimationImporters.WINDOWS.XNA", "Content.Pipeline\AnimationImporters\AnimationImporters.WINDOWS.XNA.csproj", "{E22F02E7-6799-4C14-B9B3-B461D6E9AB6E}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TilemapImporter.WINDOWS.XNA", "Content.Pipeline\TilemapImporters\TilemapImporter.WINDOWS.XNA.csproj", "{B5FAF2D1-7164-4956-9474-0F4B19706DE8}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aether.Animation.WINDOWS.XNA", "Animation\Aether.Animation.WINDOWS.XNA.csproj", "{1BD2DBC0-D366-42F7-9369-F566CCD01C03}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aether.Shaders", "Aether.Shaders", "{EFC7A27C-C20B-4BE7-8B3A-2B109991D704}"
@@ -81,6 +83,10 @@ Global
 		{497B1130-EB63-4057-BC40-F60D6FFA50CA}.Debug|x86.Build.0 = Debug|x86
 		{497B1130-EB63-4057-BC40-F60D6FFA50CA}.Release|x86.ActiveCfg = Release|x86
 		{497B1130-EB63-4057-BC40-F60D6FFA50CA}.Release|x86.Build.0 = Release|x86
+		{B5FAF2D1-7164-4956-9474-0F4B19706DE8}.Debug|x86.ActiveCfg = Debug|x86
+		{B5FAF2D1-7164-4956-9474-0F4B19706DE8}.Debug|x86.Build.0 = Debug|x86
+		{B5FAF2D1-7164-4956-9474-0F4B19706DE8}.Release|x86.ActiveCfg = Release|x86
+		{B5FAF2D1-7164-4956-9474-0F4B19706DE8}.Release|x86.Build.0 = Release|x86
 		{0690782F-0000-0000-0000-000000000000}.Debug|x86.ActiveCfg = Debug|x86
 		{0690782F-0000-0000-0000-000000000000}.Debug|x86.Build.0 = Debug|x86
 		{0690782F-0000-0000-0000-000000000000}.Release|x86.ActiveCfg = Release|x86
@@ -89,6 +95,8 @@ Global
 		{746551BA-FB64-43B0-8479-0506B930D3E5}.Debug|x86.Build.0 = Debug|x86
 		{746551BA-FB64-43B0-8479-0506B930D3E5}.Release|x86.ActiveCfg = Release|x86
 		{746551BA-FB64-43B0-8479-0506B930D3E5}.Release|x86.Build.0 = Release|x86
+		
+		
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -98,6 +106,7 @@ Global
 		{E22F02E7-6799-4C14-B9B3-B461D6E9AB6E} = {F6B6E505-6037-49E4-9060-8B29A7B99BC1}
 		{90E6017D-198B-4470-BF9B-8B8791C295CC} = {F6B6E505-6037-49E4-9060-8B29A7B99BC1}
 		{497B1130-EB63-4057-BC40-F60D6FFA50CA} = {F6B6E505-6037-49E4-9060-8B29A7B99BC1}
+		{B5FAF2D1-7164-4956-9474-0F4B19706DE8} = {F6B6E505-6037-49E4-9060-8B29A7B99BC1}
 		{0690782F-0000-0000-0000-000000000000} = {F6B6E505-6037-49E4-9060-8B29A7B99BC1}
 		{89E0198E-7298-411A-B5C1-61F2754A3F80} = {EFC7A27C-C20B-4BE7-8B3A-2B109991D704}
 		{B82B862D-C728-4A10-8A56-65D688E022C8} = {EFC7A27C-C20B-4BE7-8B3A-2B109991D704}

+ 174 - 0
Content.Pipeline/TilemapImporters/Processors/TilePacker.cs

@@ -0,0 +1,174 @@
+#region License
+//   Copyright 2021 Kastellanos Nikolaos
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace tainicom.Aether.Content.Pipeline
+{
+	internal static class TilePacker
+	{
+		internal static IList<TileContent> ArrangeGlyphs(IList<TileContent> sourceTiles,
+            int tileWidth, int tileHeight,
+            bool requirePOT, bool requireSquare)
+		{
+            // copy tiles to destTiles
+            var destTiles = new List<TileContent>();
+            for (int i = 0; i < sourceTiles.Count; i++)
+            {
+                var srcTile = sourceTiles[i];
+                var dstTile = new TileContent(srcTile);
+                destTiles.Add(dstTile);
+            }
+            
+            for (int i = 0; i < destTiles.Count; i++)
+			{
+                var dstTile = destTiles[i];
+                dstTile.DstBounds.Width = tileWidth;
+                dstTile.DstBounds.Height = tileHeight;
+            }
+            
+			// Sort so the largest glyphs get arranged first.
+			destTiles.Sort(CompareTileSizes);
+
+			// Work out how big the output bitmap should be.
+			int outputWidth = EstimateOutputWidth(destTiles);
+            outputWidth = MakeValidTextureSize(outputWidth, requirePOT);
+            int outputHeight = 0;
+
+			// Choose positions for each glyph, one at a time.
+			for (int i = 0; i < destTiles.Count; i++)
+			{
+				PositionGlyph(destTiles, i, outputWidth);
+                outputHeight = Math.Max(outputHeight, destTiles[i].DstBounds.Y + destTiles[i].DstBounds.Height);
+            }
+
+			// Create the merged output bitmap.
+			outputHeight = MakeValidTextureSize(outputHeight, requirePOT);
+			if (requireSquare)
+            {
+				outputHeight = Math.Max(outputWidth, outputHeight);
+				outputWidth = outputHeight;
+			}
+
+            return destTiles;
+
+        }
+        
+		static void PositionGlyph(List<TileContent> glyphs, int index, int outputWidth)
+		{
+			int x = 0;
+			int y = 0;
+
+			while (true)
+			{
+				// Is this position free for us to use?
+				int intersects = FindIntersectingTile(glyphs, index, x, y);
+
+				if (intersects < 0)
+				{
+                    glyphs[index].DstBounds.X = x;
+                    glyphs[index].DstBounds.Y = y;
+                    return;
+				}
+
+				// Skip past the existing glyph that we collided with.
+                x = glyphs[intersects].DstBounds.X + glyphs[intersects].DstBounds.Width;
+
+                // If we ran out of room to move to the right, try the next line down instead.
+                //if (x + glyphs[index].SrcBounds.Width > outputWidth)
+                if (x + glyphs[index].DstBounds.Width > outputWidth)
+                {
+					x = 0;
+					y++;
+				}
+			}
+		}
+        
+		// Checks if a proposed glyph position collides with anything that we already arranged.
+		static int FindIntersectingTile(List<TileContent> glyphs, int index, int x, int y)
+		{
+            var bounds = glyphs[index].DstBounds;
+
+			for (int i = 0; i < index; i++)
+			{
+                var other = glyphs[i].DstBounds;
+
+                if (other.X >= x + bounds.Width)
+					continue;
+				if (other.X + other.Width <= x)
+					continue;
+				if (other.Y >= y + bounds.Height)
+					continue;
+				if (other.Y + other.Height <= y)
+					continue;
+
+				return i;
+			}
+
+			return -1;
+		}
+
+		static int CompareTileSizes(TileContent a, TileContent b)
+		{
+			var res = b.DstBounds.Height.CompareTo(a.DstBounds.Height);
+            if (res == 0)
+                res = b.DstBounds.Width.CompareTo(a.DstBounds.Width);
+  		    return res;
+		}
+
+		static int EstimateOutputWidth(IList<TileContent> sourceGlyphs)
+		{
+			int maxWidth = 0;
+			int totalSize = 0;
+
+			foreach (var sourceGlyph in sourceGlyphs)
+			{
+				maxWidth = Math.Max(maxWidth, sourceGlyph.DstBounds.Width);
+				totalSize += sourceGlyph.DstBounds.Width * sourceGlyph.DstBounds.Height;
+			}
+
+			int width = Math.Max((int)Math.Sqrt(totalSize), maxWidth);
+            return width;
+		}
+
+		// Rounds a value up to the next larger valid texture size.
+		static int MakeValidTextureSize(int value, bool requirePowerOfTwo)
+		{
+			// In case we want to compress the texture, make sure the size is a multiple of 4.
+			const int blockSize = 4;
+
+			if (requirePowerOfTwo)
+			{
+				// Round up to a power of two.
+				int powerOfTwo = blockSize;
+
+				while (powerOfTwo < value)
+					powerOfTwo <<= 1;
+
+				return powerOfTwo;
+			}
+			else
+			{
+				// Round up to the specified block size.
+				return (value + blockSize - 1) & ~(blockSize - 1);
+			}
+		}
+	}
+
+}
+

+ 149 - 0
Content.Pipeline/TilemapImporters/Processors/TilemapProcessor.cs

@@ -0,0 +1,149 @@
+#region License
+//   Copyright 2021 Kastellanos Nikolaos
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+#endregion
+
+using System;
+using System.ComponentModel;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using Microsoft.Xna.Framework.Content.Pipeline.Processors;
+
+namespace tainicom.Aether.Content.Pipeline
+{
+    [ContentProcessor(DisplayName = "Tilemap Processor - Aether")]
+    public class TilemapProcessor : TextureProcessor, IContentProcessor
+    {        
+        private bool _mipmapsPerSprite = true;
+
+#if WINDOWS
+        // override InputType
+        [Browsable(false)]
+#endif
+        Type IContentProcessor.InputType { get { return typeof(TilemapContent); } }
+
+#if WINDOWS
+        // override OutputType
+        [Browsable(false)]
+#endif
+        Type IContentProcessor.OutputType { get { return typeof(TilemapContent); } }
+        
+
+        [DefaultValue(true)]
+        public bool MipmapsPerSprite
+        {
+            get { return _mipmapsPerSprite; }
+            set { _mipmapsPerSprite = value; }
+        }
+
+        public TilemapProcessor()
+        {
+        }
+        
+        object IContentProcessor.Process(object input, ContentProcessorContext context)
+        {
+            return Process((TilemapContent)input, context);
+        }
+
+        public TilemapContent Process(TilemapContent input, ContentProcessorContext context)
+        {
+            if (MipmapsPerSprite && GenerateMipmaps)
+                foreach (var texture in input.DestinationTiles)
+                    texture.SrcTexture.GenerateMipmaps(false);
+
+            var output = input;
+            
+            if (GenerateMipmaps)
+            {
+                if (MipmapsPerSprite)
+                {
+                    var maxTileWidth = 1;
+                    var maxTileHeight = 1;
+                    foreach (var tile in input.DestinationTiles)
+                    {
+                        maxTileWidth = Math.Max(maxTileWidth, tile.DstBounds.Width);
+                        maxTileHeight = Math.Max(maxTileHeight, tile.DstBounds.Height);
+                    }
+
+                    for (int mipLevel = 1; ; mipLevel++)
+                    {
+                        int mipLevel2 = (int)Math.Pow(2, mipLevel);
+                        Rectangle size = new Rectangle(0, 0, output.TextureAtlas.Faces[0][0].Width, output.TextureAtlas.Faces[0][0].Height);
+                        size.Width /= mipLevel2;
+                        size.Height /= mipLevel2;
+
+                        if ((maxTileWidth / mipLevel2) < 1 && (maxTileHeight / mipLevel2) < 1)
+                            break;
+
+                        var mipmapBmp = new PixelBitmapContent<Color>(size.Width, size.Height);
+                        foreach (var tile in input.DestinationTiles)
+                        {
+                            if (mipLevel >= tile.SrcTexture.Faces[0].Count) continue;
+                            var srcBmp = tile.SrcTexture.Faces[0][mipLevel];
+                            var srcBounds = new Rectangle(0, 0, srcBmp.Width, srcBmp.Height);
+                            var dstBounds = tile.DstBounds;
+                            dstBounds.X = (int)Math.Ceiling((float)dstBounds.X / mipLevel2);
+                            dstBounds.Y = (int)Math.Ceiling((float)dstBounds.Y / mipLevel2);
+                            dstBounds.Width = (int)(dstBounds.Width / mipLevel2);
+                            dstBounds.Height = (int)(dstBounds.Height / mipLevel2);
+                            // snap image to bottom
+                            dstBounds.Width  = srcBounds.Width;
+                            dstBounds.Y     += (dstBounds.Height - srcBounds.Height);
+                            dstBounds.Height = srcBounds.Height;
+
+                            if (dstBounds.Width == 0 || dstBounds.Height == 0)
+                                continue;
+                                
+                            //if (dstBounds.Width > 0 && dstBounds.Height > 0)
+                                BitmapContent.Copy(srcBmp, srcBounds, mipmapBmp, dstBounds);
+                        }
+                        output.TextureAtlas.Mipmaps.Add(mipmapBmp);
+                    }
+
+                    var outputFace0 = output.TextureAtlas.Faces[0];
+                    while (outputFace0[outputFace0.Count - 1].Width > 1 || outputFace0[outputFace0.Count - 1].Height > 1)
+                    {
+                        var lastMipmap = outputFace0[outputFace0.Count - 1];
+                        var w = Math.Max(1, lastMipmap.Width/2);
+                        var h = Math.Max(1, lastMipmap.Height/2);
+                        var mipmapBmp = new PixelBitmapContent<Color>(w, h);
+                        //PixelBitmapContent<Color>.Copy(lastMipmap, mipmapBmp);
+                        output.TextureAtlas.Mipmaps.Add(mipmapBmp);
+                    }
+                }
+                else
+                {
+                    output.TextureAtlas.GenerateMipmaps(false);
+                }
+            }
+            
+            // Workaround MonoGame TextureProcessor bug.
+            // MonoGame TextureProcessor overwrites existing mipmaps.
+            if (GenerateMipmaps && MipmapsPerSprite)
+            {
+                GenerateMipmaps = false;
+                base.Process(output.TextureAtlas, context);
+                GenerateMipmaps = true;
+            }
+            else
+            {
+                base.Process(output.TextureAtlas, context);
+            }
+            
+            return output;
+        }
+        
+    }
+}

+ 28 - 0
Content.Pipeline/TilemapImporters/Properties/AssemblyInfo.cs

@@ -0,0 +1,28 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Aether.Content.Pipeline.TilemapImporter")]
+[assembly: AssemblyProduct("TilemapImporter")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyCopyright("Copyright © Kastellanos Nikolaos  2021")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 55 - 0
Content.Pipeline/TilemapImporters/Serialization/TilemapWriter.cs

@@ -0,0 +1,55 @@
+#region License
+//   Copyright 2021 Kastellanos Nikolaos
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+#endregion
+
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using Microsoft.Xna.Framework.Graphics;
+using System;
+
+namespace tainicom.Aether.Content.Pipeline.Serialization
+{
+    [ContentTypeWriter]
+    class TextureAtlasWriter : ContentTypeWriter<TilemapContent>
+    {
+        protected override void Write(ContentWriter output, TilemapContent atlas)
+        {
+            output.WriteRawObject((Texture2DContent)atlas.TextureAtlas);
+
+            output.WriteRawObject((Texture2DContent)atlas.TextureMap);
+
+            // write Sprites
+            output.Write(atlas.DestinationTiles.Count);
+            foreach(var name in atlas.Tiles.Keys)
+            {
+                var sprite = atlas.Tiles[name];
+                output.Write(name);
+                output.Write(sprite.DstBounds.X);
+                output.Write(sprite.DstBounds.Y);
+                output.Write(sprite.DstBounds.Width);
+                output.Write(sprite.DstBounds.Height);
+            }
+            
+            return;
+        }
+        
+        public override string GetRuntimeReader(TargetPlatform targetPlatform)
+        {
+            return "tainicom.Aether.Graphics.Content.TilemapReader, Aether.Tilemap";
+        }
+        
+    }
+}

+ 55 - 0
Content.Pipeline/TilemapImporters/Tilemap/TileContent.cs

@@ -0,0 +1,55 @@
+#region License
+//   Copyright 2021 Kastellanos Nikolaos
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+#endregion
+
+
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+
+namespace tainicom.Aether.Content.Pipeline
+{
+    public class TileContent
+    {
+        internal TilesetContent Tileset;
+        internal int Id;
+        public TextureContent SrcTexture;
+        public Rectangle SrcBounds;
+
+        public Rectangle DstBounds;
+
+        public TileContent()
+        {
+            this.Tileset = null;
+            this.Id = -1;
+            this.SrcTexture = null;
+            this.SrcBounds = Rectangle.Empty;
+            this.DstBounds = Rectangle.Empty;
+        }
+
+        public TileContent(TileContent other)
+        {
+            this.Tileset = other.Tileset;
+            this.Id = other.Id;
+            this.SrcTexture = other.SrcTexture;
+            this.SrcBounds = other.SrcBounds;
+            this.DstBounds = other.DstBounds;
+        }
+
+        public override string ToString()
+        {
+            return string.Format("{{SrcBounds:{0}, DstBounds{1}}}", SrcBounds, DstBounds);
+        }
+    }
+}

+ 117 - 0
Content.Pipeline/TilemapImporters/Tilemap/TilemapContent.cs

@@ -0,0 +1,117 @@
+#region License
+//   Copyright 2021 Kastellanos Nikolaos
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+
+namespace tainicom.Aether.Content.Pipeline
+{
+    public class TilemapContent : Texture2DContent
+    {
+        public Texture2DContent TextureAtlas { get { return this; } }
+        public Texture2DContent TextureMap;
+        public readonly Dictionary<string, TileContent> Tiles = new Dictionary<string, TileContent>();
+        
+        public TilesetContent Tileset;
+        internal readonly List<TileContent> DestinationTiles = new List<TileContent>();
+
+        internal int MapColumns, MapRows;
+        internal int TileWidth, TileHeight;
+        internal int Width, Height;
+
+        internal string Renderorder;
+        internal int Firstgid;
+        internal int LayerColumns, LayerRows;
+        internal int[] MapData;
+
+
+
+        internal static void PackTiles(TilemapContent output, int tileWidth, int tileHeight)
+        {
+            var dstTiles = TilePacker.ArrangeGlyphs(output.Tileset.SourceTiles, tileWidth, tileHeight, true, true);
+
+            foreach (var dstTile in dstTiles)
+            {
+                output.DestinationTiles.Add(dstTile);
+                var name = dstTile.SrcTexture.Name;
+                if (output.Tiles.ContainsKey(name))
+                    name = name + dstTile.Id;
+                output.Tiles.Add(name, dstTile);
+            }
+        }
+        
+        internal static void RenderAtlas(TilemapContent output)
+        {
+            Rectangle s = new Rectangle(0,0,1,1);
+            foreach (var dstTile in output.DestinationTiles)
+            {
+                s = Rectangle.Union(s,dstTile.DstBounds);
+            }
+
+            var outputBmp = new PixelBitmapContent<Color>(s.Width, s.Height);
+
+            foreach (var dstTile in output.DestinationTiles)
+            {
+                var srcBounds = dstTile.SrcBounds;
+                var dstBounds = new Rectangle(dstTile.DstBounds.X, dstTile.DstBounds.Y, srcBounds.Width, srcBounds.Height);
+                var offsetX = 0;
+                var offsetY = dstTile.DstBounds.Height - srcBounds.Height;
+                dstBounds.X += offsetX;
+                dstBounds.Y += offsetY;
+                var srcBmp = dstTile.SrcTexture.Faces[0][0];
+                BitmapContent.Copy(srcBmp, srcBounds, outputBmp, dstBounds);
+            }
+            var mipmapChain = new MipmapChain(outputBmp);
+            output.TextureAtlas.Mipmaps = mipmapChain;
+        }
+
+        internal static void RenderMap(TilemapContent output)
+        {
+            var mp = new byte[output.MapData.Length * 4];
+            for (int i = 0; i < output.MapData.Length; i++)
+            {
+                var id = output.MapData[i] - output.Firstgid;
+                TileContent tile = null;
+                foreach (var t in output.DestinationTiles)
+                {
+                    if (t.Id == id)
+                    {
+                        tile = t;
+                        break;
+                    }
+                }
+
+                byte x = (byte)(tile.DstBounds.X / output.TileWidth);
+                byte y = (byte)(tile.DstBounds.Y / output.TileHeight);
+
+                mp[i * 4 + 0] = x;
+                mp[i * 4 + 1] = y;
+                mp[i * 4 + 2] = 0;
+                mp[i * 4 + 3] = 255;
+            }
+
+            BitmapContent bm = new PixelBitmapContent<Color>(output.MapColumns, output.MapRows);
+            bm.SetPixelData(mp);
+            Texture2DContent mt = new Texture2DContent();
+            mt.Faces[0].Add(bm);
+            output.TextureMap = mt;
+        }
+
+    }
+}

+ 31 - 0
Content.Pipeline/TilemapImporters/Tilemap/TilesetContent.cs

@@ -0,0 +1,31 @@
+#region License
+//   Copyright 2021 Kastellanos Nikolaos
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+#endregion
+
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+
+namespace tainicom.Aether.Content.Pipeline
+{
+    public class TilesetContent : Texture2DContent
+    {
+        public int TileWidth;
+        public int tileHeight;
+
+        public readonly List<TileContent> SourceTiles = new List<TileContent>();
+        
+    }
+}

+ 50 - 0
Content.Pipeline/TilemapImporters/Tilemap/XMLExtensions.cs

@@ -0,0 +1,50 @@
+#region License
+//   Copyright 2021 Kastellanos Nikolaos
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+#endregion
+
+using System.Globalization;
+using Microsoft.Xna.Framework;
+
+namespace System.Xml
+{
+    public static class XMLExtensions
+    {
+
+        public static string GetAttribute(this XmlNode xmlNode, string attributeName)
+        {
+            var attribute = xmlNode.Attributes[attributeName];
+            if (attribute == null) return null;
+            return attribute.Value;
+        }
+
+        public static int? GetAttributeAsInt(this XmlNode xmlNode, string attributeName)
+        {
+            var attribute = xmlNode.Attributes[attributeName];
+            if (attribute == null) return null;
+            return Int32.Parse(attribute.Value, CultureInfo.InvariantCulture);
+        }
+
+        public static Color? GetAttributeAsColor(this XmlNode xmlNode, string attributeName)
+        {
+            var attribute = xmlNode.Attributes[attributeName];
+            if (attribute == null) return null;
+            attribute.Value = attribute.Value.TrimStart(new char[] { '#' });
+            return new Color(
+                Int32.Parse(attribute.Value.Substring(0, 2), System.Globalization.NumberStyles.HexNumber),
+                Int32.Parse(attribute.Value.Substring(2, 2), System.Globalization.NumberStyles.HexNumber),
+                Int32.Parse(attribute.Value.Substring(4, 2), System.Globalization.NumberStyles.HexNumber));
+        }
+    }
+}

+ 66 - 0
Content.Pipeline/TilemapImporters/TilemapImporter.WINDOWS.MG.csproj

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{9B0F9C6B-3C43-472D-B0C1-91E11A9FDE89}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>tainicom.Aether.Content.Pipeline</RootNamespace>
+    <AssemblyName>Aether.Content.Pipeline.TilemapImporter</AssemblyName>
+    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>..\..\bin\Debug\Windows\</OutputPath>
+    <DefineConstants>TRACE;DEBUG;WINDOWS MG</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\..\bin\Release\Windows\</OutputPath>
+    <DefineConstants>TRACE;WINDOWS MG</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <PackageReference Include="MonoGame.Framework.Portable.9000" Version="3.8.9008">
+      <PrivateAssets>all</PrivateAssets>
+      <ExcludeAssets>runtime</ExcludeAssets>
+    </PackageReference>
+    <PackageReference Include="MonoGame.Framework.Content.Pipeline.Portable.9000" Version="3.8.9008">
+      <PrivateAssets>all</PrivateAssets>
+      <ExcludeAssets>runtime</ExcludeAssets>
+    </PackageReference>
+    <Reference Include="System.XML" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="TilemapImporter.cs" />
+    <Compile Include="Tilemap\TileContent.cs" />
+    <Compile Include="Tilemap\TilemapContent.cs" />
+    <Compile Include="Tilemap\TilesetContent.cs" />
+    <Compile Include="Tilemap\XMLExtensions.cs" />
+    <Compile Include="Processors\TilePacker.cs" />
+    <Compile Include="Processors\TilemapProcessor.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Serialization\TilemapWriter.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 65 - 0
Content.Pipeline/TilemapImporters/TilemapImporter.WINDOWS.XNA.csproj

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
+  <PropertyGroup>
+    <ProjectGuid>{B5FAF2D1-7164-4956-9474-0F4B19706DE8}</ProjectGuid>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>tainicom.Aether.Content.Pipeline</RootNamespace>
+    <AssemblyName>Aether.Content.Pipeline.TilemapImporter</AssemblyName>
+    <XnaFrameworkVersion>v4.0</XnaFrameworkVersion>
+    <XnaPlatform>Windows</XnaPlatform>
+    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>..\..\bin\Debug\Windows.XNA\</OutputPath>
+    <DefineConstants>TRACE;DEBUG;WINDOWS XNA</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
+    <DebugType>none</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\..\bin\Release\Windows.XNA\</OutputPath>
+    <DefineConstants>TRACE;WINDOWS XNA</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" />
+    <Reference Include="Microsoft.Xna.Framework.Content.Pipeline.TextureImporter, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" />
+    <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" />
+    <Reference Include="Microsoft.Xna.Framework.Content.Pipeline, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" />
+    <Reference Include="System" />
+    <Reference Include="System.Core">
+      <RequiredTargetFramework>4.0</RequiredTargetFramework>
+    </Reference>
+    <Reference Include="System.XML" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="TilemapImporter.cs" />
+    <Compile Include="Tilemap\TileContent.cs" />
+    <Compile Include="Tilemap\TilemapContent.cs" />
+    <Compile Include="Tilemap\TilesetContent.cs" />
+    <Compile Include="Tilemap\XMLExtensions.cs" />
+    <Compile Include="Processors\TilePacker.cs" />
+    <Compile Include="Processors\TilemapProcessor.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Serialization\TilemapWriter.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA Game Studio\Microsoft.Xna.GameStudio.ContentPipelineExtensions.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+     Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 183 - 0
Content.Pipeline/TilemapImporters/TilemapImporter.cs

@@ -0,0 +1,183 @@
+#region License
+//   Copyright 2021 Kastellanos Nikolaos
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//       http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Xml;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace tainicom.Aether.Content.Pipeline
+{
+    [ContentImporter(".tmx", DisplayName = "Tilemap Importer - Aether", DefaultProcessor = "TilemapProcessor")]
+    public class TilemapImporter : ContentImporter<TilemapContent>
+    {
+        public override TilemapContent Import(string filename, ContentImporterContext context)
+        {
+            TilemapContent output;
+
+            if (Path.GetExtension(filename) == ".tmx")
+                output = ImportTMX(filename, context);
+            else
+                throw new InvalidContentException("File type not supported");
+
+            TilemapContent.PackTiles(output, output.TileWidth, output.TileHeight);
+            TilemapContent.RenderAtlas(output);
+            TilemapContent.RenderMap(output);
+
+            return output;
+        }
+        
+        private static TilemapContent ImportTMX(string filename, ContentImporterContext context)
+        {
+            TilemapContent output = new TilemapContent();
+            output.Identity = new ContentIdentity(filename);
+
+            XmlDocument xmlDoc = new XmlDocument();
+            xmlDoc.Load(filename);
+
+            var map = xmlDoc.DocumentElement;
+            var orientation = map.GetAttribute("orientation");
+            if (orientation != "orthogonal")
+                throw new InvalidContentException("Invalid orientation. Only 'orthogonal' is supported for atlases.");
+            output.Renderorder = map.GetAttribute("renderorder");
+            output.MapColumns = map.GetAttributeAsInt("width").Value;
+            output.MapRows = map.GetAttributeAsInt("height").Value;
+            output.TileWidth = map.GetAttributeAsInt("tilewidth").Value;
+            output.TileHeight = map.GetAttributeAsInt("tileheight").Value;
+            output.Width = output.MapColumns * output.TileWidth;
+            output.Height = output.MapRows * output.TileHeight;
+
+            XmlNode tilesetNode = map["tileset"];
+            output.Firstgid = tilesetNode.GetAttributeAsInt("firstgid").Value;
+
+            if (tilesetNode.Attributes["source"] != null)
+            {
+                var tsxFilename = tilesetNode.Attributes["source"].Value;
+                var baseDirectory = Path.GetDirectoryName(filename);
+                tsxFilename = Path.Combine(baseDirectory, tsxFilename);
+                output.Tileset = ImportTSX(tsxFilename, context);
+                context.AddDependency(tsxFilename);
+            }
+            else
+            {
+                var rootDirectory = Path.GetDirectoryName(filename);
+                output.Tileset = ImportTileset(tilesetNode, context, rootDirectory);
+            }
+
+            XmlNode layerNode = map["layer"];
+            var layerColumns = Convert.ToInt32(map.Attributes["width"].Value, CultureInfo.InvariantCulture);
+            var layerRows = Convert.ToInt32(map.Attributes["height"].Value, CultureInfo.InvariantCulture);
+            output.LayerColumns = layerColumns;
+            output.LayerRows = layerRows;
+
+            XmlNode layerDataNode = layerNode["data"];
+            var encoding = layerDataNode.Attributes["encoding"].Value;
+            if (encoding != "csv")
+                throw new InvalidContentException("Invalid encoding. Only 'csv' is supported for data.");
+            var data = layerDataNode.InnerText;
+            var dataStringList = data.Split(',');
+            var mapData = new int[dataStringList.Length];
+            for (int i = 0; i < dataStringList.Length; i++)
+                mapData[i] = Convert.ToInt32(dataStringList[i].Trim(), CultureInfo.InvariantCulture);
+            output.MapData = mapData;
+
+            return output;
+        }
+
+        private static TilesetContent ImportTSX(string tsxFilename, ContentImporterContext context)
+        {
+            XmlDocument xmlDoc = new XmlDocument();
+            xmlDoc.Load(tsxFilename);
+            XmlNode tileset = xmlDoc.DocumentElement;
+            var baseDirectory = Path.GetDirectoryName(tsxFilename);
+            return ImportTileset(tileset, context, baseDirectory);
+        }
+
+        private static TilesetContent ImportTileset(XmlNode tilesetNode, ContentImporterContext context, string baseDirectory)
+        {
+            TilesetContent tileset = new TilesetContent();
+
+            if (tilesetNode["tileoffset"] != null)
+                throw new InvalidContentException("tileoffset is not supported.");
+
+            tileset.TileWidth = tilesetNode.GetAttributeAsInt("tilewidth").Value;
+            tileset.tileHeight = tilesetNode.GetAttributeAsInt("tileheight").Value;
+
+            BitmapContent bm = new PixelBitmapContent<Color>(tileset.TileWidth, tileset.tileHeight);
+            Texture2DContent mt = new Texture2DContent();
+            mt.Faces[0].Add(bm);
+            mt.Name = "null";            
+            var nullTile = new TileContent();
+            nullTile.Tileset = tileset;
+            nullTile.Id = -1;
+            nullTile.SrcTexture = mt;
+            nullTile.SrcBounds.Location = Point.Zero;
+            nullTile.SrcBounds.Width = mt.Mipmaps[0].Width;
+            nullTile.SrcBounds.Height = mt.Mipmaps[0].Height;
+            nullTile.DstBounds.Location = Point.Zero;
+            nullTile.DstBounds.Width = tileset.TileWidth;
+            nullTile.DstBounds.Height = tileset.tileHeight;
+
+            tileset.SourceTiles.Add(nullTile);
+
+            foreach (XmlNode tileNode in tilesetNode.ChildNodes)
+            {
+                if (tileNode.Name != "tile") continue;
+                var tileId = tileNode.GetAttributeAsInt("id").Value;
+
+                XmlNode imageNode = tileNode["image"];
+                
+                //var format = GetAttribute(imageNode, "format");
+                var imageSource = imageNode.GetAttribute("source");
+                var fullImageSource = Path.Combine(baseDirectory, imageSource);
+                TextureImporter txImporter = new TextureImporter();
+                var textureContent = (Texture2DContent)txImporter.Import(fullImageSource, context);
+                textureContent.Name = Path.GetFileNameWithoutExtension(fullImageSource);
+
+                var source = new TileContent();
+                source.Tileset = tileset;
+                source.Id = tileId;
+                source.SrcTexture = textureContent;
+                source.SrcBounds.Location = Point.Zero;
+                source.SrcBounds.Width  = textureContent.Mipmaps[0].Width;
+                source.SrcBounds.Height = textureContent.Mipmaps[0].Height;
+                source.DstBounds.Location = Point.Zero;
+                source.DstBounds.Width = textureContent.Mipmaps[0].Width;
+                source.DstBounds.Height = textureContent.Mipmaps[0].Height;
+
+
+                var transKeyColor = imageNode.GetAttributeAsColor("trans");
+                if (transKeyColor != null)
+                    foreach (var mips in textureContent.Faces)
+                        foreach (var mip in mips)
+                            ((PixelBitmapContent<Color>)mip).ReplaceColor(transKeyColor.Value, Color.Transparent);
+
+                if (tileId != tileset.SourceTiles.Count-1)
+                    throw new InvalidContentException("Invalid id");
+                tileset.SourceTiles.Add(source);
+            }
+
+            return tileset;
+        }
+        
+    }
+
+}