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; 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 (optional) frm (or from) instruction defines a texture region (defined before the current region) whose data should be copied. All data from the region will be copied, but adding additional instructions afterwards modifies the data. It requires one argument: the name of the region whose data to copy. If this instruction is used, the loc instruction is not required. /// /// /// /// 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; } /// /// Converts this data texture atlas to a and returns the result. /// The resulting dictionary will contain all named regions that this data texture atlas contains. /// /// The dictionary representation of this data texture atlas. public Dictionary ToDictionary() { return new Dictionary(this.regions); } /// /// 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; case "frm": case "from": var fromRegion = atlas[words[i + 1]]; customData.Clear(); foreach (var key in fromRegion.GetDataKeys()) customData.Add(key, fromRegion.GetData(key)); // our main texture might be a sub-region already, so we have to take that into account location = fromRegion.Area.OffsetCopy(new Point(-texture.U, -texture.V)); pivot = fromRegion.PivotPixels; if (pivot != Vector2.Zero && !pivotRelative) pivot += location.Location.ToVector2(); offset = Vector2.Zero; i += 1; break; default: // if we have data for the previous regions, they're valid so we add them AddCurrentRegions(); // 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); } } // add the last region that was started on AddCurrentRegions(); return atlas; void AddCurrentRegions() { // the location is the only mandatory information, which is why we check it here if (location == Rectangle.Empty || namesOffsets.Count <= 0) return; foreach (var (name, addedOff) in namesOffsets) { var loc = location; var piv = pivot; var off = offset + addedOff; loc.Offset(off.ToPoint()); if (piv != Vector2.Zero) { piv += off; if (!pivotRelative) piv -= loc.Location.ToVector2(); } var region = new TextureRegion(texture, loc) { PivotPixels = piv, Name = name }; foreach (var kv in customData) region.SetData(kv.Key, kv.Value); atlas.regions.Add(name, region); } // we only clear names offsets if the location was valid, otherwise we ignore multiple names for a region namesOffsets.Clear(); } } } /// /// 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); } } }