using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text.RegularExpressions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using MLEM.Extensions; using MLEM.Misc; using MLEM.Textures; #if FNA using MLEM.Extensions; #endif namespace MLEM.Data { /// /// /// This class represents an atlas of objects which are loaded from a special texture atlas file. /// To load a data texture atlas, you can use . /// /// /// Data texture atlases are designed to be easy to write by hand. Because of this, their structure is very simple. /// Each texture region defined in the atlas consists of its names (where multiple names can be separated by whitespace), followed by a set of possible instructions and their arguments, also separated by whitespace. /// /// The loc (or location) instruction defines the of the texture region as a rectangle whose origin is its top-left corner. It requires four arguments: x, y, width and height of the rectangle. /// The (optional) piv (or pivot) instruction defines the of the texture region. It requires two arguments: x and y. If it is not supplied, the pivot defaults to the top-left corner of the texture region. /// The (optional) off (of offset) instruction defines an offset that is added onto the location and pivot of this texture region. This is useful when duplicating a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset. /// The (optional and repeatable) cpy (or copy) instruction defines an additional texture region that should also be generated from the same data, but with a given offset that will be applied to the location and pivot. It requires three arguments: the copy region's name and the x and y offsets. /// The (optional and repeatable) dat (or data) instruction defines a custom data point that can be added to the resulting 's data. It requires two arguments: the data point's name and the data point's value, the latter of which is also stored as a string value. /// /// /// /// The following entry defines a texture region with the names LongTableRight and LongTableUp, whose will be a rectangle with X=32, Y=30, Width=64, Height=48, and whose will be a vector with X=80, Y=46. /// /// LongTableRight LongTableUp /// loc 32 30 64 48 /// piv 80 46 /// /// /// /// /// To see a Data Texture Atlas in action, you can check out the sandbox project: https://github.com/Ellpeck/MLEM/blob/main/Sandbox/Content/Textures/Furniture.atlas. /// Additionally, if you are using Aseprite, there is a script to automatically populate it: https://gist.github.com/Ellpeck/e597c1412465c10f41a42050ec117ea2. /// public class DataTextureAtlas { /// /// The texture to use for this atlas /// public readonly TextureRegion Texture; /// /// Returns the texture region with the given name, or null if it does not exist. /// /// The region's name public TextureRegion this[string name] { get { this.regions.TryGetValue(name, out var ret); return ret; } } /// /// Returns an enumerable of all of the values in this atlas. /// public IEnumerable Regions => this.regions.Values; /// /// Returns an enumerable of all of the names in this atlas. /// public IEnumerable RegionNames => this.regions.Keys; private readonly Dictionary regions = new Dictionary(); private DataTextureAtlas(TextureRegion texture) { this.Texture = texture; } /// /// Loads a from the given loaded texture and texture data file. /// For more information on data texture atlases, see the type documentation. /// /// The texture to use for this data texture atlas /// The content manager to use for loading /// The path, including extension, to the atlas info file /// If this value is true, then the pivot points passed in the info file will be relative to the coordinates of the texture region, not relative to the entire texture's origin. /// A new data texture atlas with the given settings public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) { var info = Path.Combine(content.RootDirectory, infoPath); string text; try { if (Path.IsPathRooted(info)) { text = File.ReadAllText(info); } else { using (var reader = new StreamReader(TitleContainer.OpenStream(info))) text = reader.ReadToEnd(); } } catch (Exception e) { throw new ContentLoadException($"Couldn't load data texture atlas data from {info}", e); } var atlas = new DataTextureAtlas(texture); var words = Regex.Split(text, @"\s+"); var namesOffsets = new List<(string, Vector2)>(); var customData = new Dictionary(); var location = Rectangle.Empty; var pivot = Vector2.Zero; var offset = Vector2.Zero; for (var i = 0; i < words.Length; i++) { var word = words[i]; try { switch (word) { case "loc": case "location": location = new Rectangle( int.Parse(words[i + 1], CultureInfo.InvariantCulture), int.Parse(words[i + 2], CultureInfo.InvariantCulture), int.Parse(words[i + 3], CultureInfo.InvariantCulture), int.Parse(words[i + 4], CultureInfo.InvariantCulture)); i += 4; break; case "piv": case "pivot": pivot = new Vector2( float.Parse(words[i + 1], CultureInfo.InvariantCulture), float.Parse(words[i + 2], CultureInfo.InvariantCulture)); i += 2; break; case "off": case "offset": offset = new Vector2( float.Parse(words[i + 1], CultureInfo.InvariantCulture), float.Parse(words[i + 2], CultureInfo.InvariantCulture)); i += 2; break; case "cpy": case "copy": var copyOffset = new Vector2( float.Parse(words[i + 2], CultureInfo.InvariantCulture), float.Parse(words[i + 3], CultureInfo.InvariantCulture)); namesOffsets.Add((words[i + 1], copyOffset)); i += 3; break; case "dat": case "data": customData.Add(words[i + 1], words[i + 2]); i += 2; break; default: // if we have a location for the previous regions, they're valid so we add them if (location != Rectangle.Empty && namesOffsets.Count > 0) { location.Offset(offset.ToPoint()); pivot += offset; if (!pivotRelative) pivot -= location.Location.ToVector2(); foreach (var (name, off) in namesOffsets) { var region = new TextureRegion(texture, location.OffsetCopy(off.ToPoint())) { PivotPixels = pivot + off, Name = name }; foreach (var kv in customData) region.SetData(kv.Key, kv.Value); atlas.regions.Add(name, region); } namesOffsets.Clear(); } // we're starting a new region (or adding another name for a new region), so clear old data namesOffsets.Add((word.Trim(), Vector2.Zero)); customData.Clear(); location = Rectangle.Empty; pivot = Vector2.Zero; offset = Vector2.Zero; break; } } catch (Exception e) { throw new ContentLoadException($"Couldn't parse data texture atlas instruction {word} for region(s) {string.Join(", ", namesOffsets)}", e); } } return atlas; } } /// /// A set of extension methods for dealing with . /// public static class DataTextureAtlasExtensions { /// /// Loads a from the given texture and texture data file. /// For more information on data texture atlases, see the type documentation. /// /// The content manager to use for loading /// The path to the texture file /// The path, including extension, to the atlas info file, or null if ".atlas" should be used /// If this value is true, then the pivot points passed in the info file will be relative to the coordinates of the texture region, not relative to the entire texture's origin. /// A new data texture atlas with the given settings public static DataTextureAtlas LoadTextureAtlas(this ContentManager content, string texturePath, string infoPath = null, bool pivotRelative = false) { var texture = new TextureRegion(content.Load(texturePath)); return DataTextureAtlas.LoadAtlasData(texture, content, infoPath ?? $"{texturePath}.atlas", pivotRelative); } } }