using System.IO; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEditor; using System.Xml.Linq; using System; namespace SuperTiled2Unity.Editor { internal class SpriteRectsManager { internal static SpriteRectsManager Instance { get; } private readonly HashSet m_RectangleEntries = new HashSet(); private readonly List m_TextureAssetsReimported = new List(); static SpriteRectsManager() { Instance = CreateInstance(); } internal IEnumerable GetSpriteRectsForTexture(string assetPathTexture) { var absolutePathTexture = Path.GetFullPath(assetPathTexture).SanitizePath().ToLower(); return m_RectangleEntries.Where(r => r.AbsolutePathTexture == absolutePathTexture).Select(r => new Rect(r.X, r.Y, r.Width, r.Height)).Distinct(); } private static string GetSanitizedAbsolutePathFromRelative(string absolutePathParent, string relativeImagePath) { using (ChDir.FromFilename(absolutePathParent)) { return Path.GetFullPath(relativeImagePath).SanitizePath(); } } private void ProcessTiledFile(string path) { var absoluteTsxPath = Path.GetFullPath(path).SanitizePath().ToLower(); m_RectangleEntries.RemoveWhere(r => r.AbsolutePathTsx == absoluteTsxPath); try { // Open up the filed file and look for "tileset" elements XDocument doc = XDocument.Load(absoluteTsxPath); if (doc == null) { Debug.LogError($"'Looking for tilesets: {absoluteTsxPath}' failed to load."); return; } foreach (var xTileset in doc.Descendants("tileset")) { // External tilesets (which have a source) are ignored here. They will be processed on their own. var source = xTileset.GetAttributeAs("source"); if (string.IsNullOrEmpty(source)) { ProcessTileset(absoluteTsxPath, xTileset); } } } catch (Exception e) { Debug.Log($"Looking for tilesets. Unknown error processing Tiled file '{absoluteTsxPath}': {e.Message}"); } } private void ProcessTileset(string abspath, XElement xTileset) { // The tileset either has one element or elements var xImage = xTileset.Element("image"); var xTiles = xTileset.Elements("tile"); if (xImage != null) { ProcessTilesetSingle(abspath, xTileset, xImage); } else { ProcessTilesetMultiple(abspath, xTileset, xTiles); } } private void ProcessTilesetSingle(string absolutePathTsx, XElement xTileset, XElement xImage) { var relativePathTexture = xImage.GetAttributeAs("source"); var absolutePathTexture = GetSanitizedAbsolutePathFromRelative(absolutePathTsx, relativePathTexture); var tileCount = xTileset.GetAttributeAs("tilecount"); var tileWidth = xTileset.GetAttributeAs("tilewidth"); var tileHeight = xTileset.GetAttributeAs("tileheight"); var columns = xTileset.GetAttributeAs("columns"); var spacing = xTileset.GetAttributeAs("spacing", 0); var margin = xTileset.GetAttributeAs("margin", 0); var imageHeight = xImage.GetAttributeAs("height"); for (int i = 0; i < tileCount; i++) { // Get grid x,y coords int x = i % columns; int y = i / columns; // Get x source on texture int srcx = x * tileWidth; srcx += x * spacing; srcx += margin; // Get y source on texture int srcy = y * tileHeight; srcy += y * spacing; srcy += margin; // In Tiled, texture origin is the top-left. However, in Unity the origin is bottom-left. srcy = (imageHeight - srcy) - tileHeight; if (srcy < 0) { // This is an edge condition in Tiled if a tileset's texture has been resized break; } var entry = new RectangleEntry { AbsolutePathTsx = absolutePathTsx.ToLower(), AbsolutePathTexture = absolutePathTexture.ToLower(), X = srcx, Y = srcy, Width = tileWidth, Height = tileHeight, }; m_RectangleEntries.Add(entry); } } private void ProcessTilesetMultiple(string absolutePathTsx, XElement xTileset, IEnumerable xTiles) { foreach (var xTile in xTiles) { var xImage = xTile.Element("image"); if (xImage != null) { var relativePathTexture = xImage.GetAttributeAs("source"); var absolutePathTexture = GetSanitizedAbsolutePathFromRelative(absolutePathTsx, relativePathTexture); int texture_w = xImage.GetAttributeAs("width"); int texture_h = xImage.GetAttributeAs("height"); int tile_x = xTile.GetAttributeAs("x", 0); int tile_y = xTile.GetAttributeAs("y", 0); int tile_w = xTile.GetAttributeAs("width", texture_w); int tile_h = xTile.GetAttributeAs("height", texture_h); var entry = new RectangleEntry { AbsolutePathTsx = absolutePathTsx.ToLower(), AbsolutePathTexture = absolutePathTexture.ToLower(), X = tile_x, Y = tile_y, Width = tile_w, Height = tile_h, }; m_RectangleEntries.Add(entry); } } } private void CheckForTextureReimports(string tiledAssetPath) { // Does the the tiled asset have any import errors? var importErrors = AssetDatabase.LoadAssetAtPath(tiledAssetPath); if (importErrors != null) { // Do not try to reimport textures if pixels per unit settings don't match // That requires user input if (!importErrors.m_WrongPixelsPerUnits.Any()) { // Do we have any errors of the missing sprite variety? foreach (var missing in importErrors.m_MissingTileSprites) { // Only try to reimport these textures once // Otherwise we may introduce a cyclic depenency chain var textureAssetPath = missing.m_TextureAssetPath.ToLower(); if (!m_TextureAssetsReimported.Contains(textureAssetPath)) { m_TextureAssetsReimported.Add(textureAssetPath); AssetDatabase.ImportAsset(textureAssetPath); } } } } } private void ImportTiledFile(string path) { ProcessTiledFile(path); // Does the tiled file we've just imported require textures to have sprites in them? // We can't have cyclic dependencies between textures -> tilesets -> textures // But we can try a one-time reimport of textures here // This will help in cases where users imported their tiled files before their textures // Or they are upgrading from an older version of ST2U that created its own sprite atlases CheckForTextureReimports(path); } private void ImportImageFile(string path) { } private void DeleteTiledFile(string path) { } private void DeleteImageFile(string path) { } private void MoveTiledFile(string oldPath, string newPath) { } private void MoveImageFile(string oldPath, string newPath) { } // Seed the collection of sprite rectangles needed to make tiles // Further imports will keep this collection updated private static SpriteRectsManager CreateInstance() { var instance = new SpriteRectsManager(); // Maps (tmx) and tilesets (tsx) may contain tiles and will instruct us on how a texture is to be cut up into sprites // We have to look by file extension and not by asset type because these files may not yet be imported var tmxFiles = Directory.EnumerateFiles(Application.dataPath, "*.tmx", SearchOption.AllDirectories).ToList(); foreach (string file in tmxFiles) { var path = file.SanitizePath(); instance.ProcessTiledFile(path); } var tsxFiles = Directory.EnumerateFiles(Application.dataPath, "*.tsx", SearchOption.AllDirectories).ToList(); foreach (string file in tsxFiles) { var path = file.SanitizePath(); instance.ProcessTiledFile(path); } return instance; } private struct RectangleEntry : IEquatable { public string AbsolutePathTsx { get; set; } public string AbsolutePathTexture { get; set; } public int X { get; set; } public int Y { get; set; } public int Width { get; set; } public int Height { get; set; } public static bool operator ==(RectangleEntry lhs, RectangleEntry rhs) => lhs.Equals(rhs); public static bool operator !=(RectangleEntry lhs, RectangleEntry rhs) => !(lhs == rhs); public override bool Equals(object obj) { return obj is RectangleEntry other && this.Equals(other); } public override int GetHashCode() { return (AbsolutePathTsx, AbsolutePathTexture, X, Y, Width, Height).GetHashCode(); } public bool Equals(RectangleEntry other) { return AbsolutePathTsx == other.AbsolutePathTsx && AbsolutePathTexture == other.AbsolutePathTexture && X == other.X && Y == other.Y && Width == other.Width && Height == other.Height; } } private class InternalAssetPostprocessor : AssetPostprocessor { private static readonly string[] TiledExtensions = new string[] { ".tmx", ".tsx", }; private static readonly string[] ImageExtensions = new string[] { ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".tga", ".tif", ".tiff", }; private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { // Assets that were imported foreach (var imported in importedAssets) { if (TiledExtensions.Any(x => imported.EndsWith(x))) { Instance.ImportTiledFile(imported); } else if (ImageExtensions.Any(x => imported.EndsWith(x))) { Instance.ImportImageFile(imported); } } // Assets that were deleted foreach (var deleted in deletedAssets) { if (TiledExtensions.Any(x => deleted.EndsWith(x))) { Instance.DeleteTiledFile(deleted); } else if (ImageExtensions.Any(x => deleted.EndsWith(x))) { Instance.DeleteImageFile(deleted); } } // Assets that were moved or renamed for (int i = 0; i < movedAssets.Length; i++) { string moved = movedAssets[i]; string movedFrom = movedFromAssetPaths[i]; if (TiledExtensions.Any(x => moved.EndsWith(x))) { Instance.MoveTiledFile(movedFrom, moved); } else if (ImageExtensions.Any(x => moved.EndsWith(x))) { Instance.MoveImageFile(movedFrom, moved); } } } } } }