diff --git a/CHANGELOG.md b/CHANGELOG.md index 725726d..a25f29d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,9 @@ Fixes - Fixed Element.OnChildAdded and Element.OnChildRemoved being called for grandchildren when a child is added ### MLEM.Data +Additions +- Added data and copy instructions to DataTextureAtlas + Improvements - Allow data texture atlas pivots and offsets to be negative - Made RuntimeTexturePacker restore texture region name and pivot when packing diff --git a/MLEM.Data/DataTextureAtlas.cs b/MLEM.Data/DataTextureAtlas.cs index 400a200..fc801f3 100644 --- a/MLEM.Data/DataTextureAtlas.cs +++ b/MLEM.Data/DataTextureAtlas.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -5,6 +6,8 @@ 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; @@ -18,15 +21,19 @@ namespace MLEM.Data { /// /// /// 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 name, followed by a set of possible keywords and their arguments, separated by spaces. - /// The loc keyword 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 keyword 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 keyword defines an offset that is added onto the location and pivot of this texture region. This is useful when copying and pasting a previously defined texture region to create a second region that has a constant offset. It requires two arguments: The x and y offset. + /// 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 name LongTableRight, 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. + /// 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 + /// LongTableRight LongTableUp /// loc 32 30 64 48 /// piv 80 46 /// @@ -69,6 +76,7 @@ namespace MLEM.Data { /// /// 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 @@ -78,41 +86,92 @@ namespace MLEM.Data { public static DataTextureAtlas LoadAtlasData(TextureRegion texture, ContentManager content, string infoPath, bool pivotRelative = false) { var info = Path.Combine(content.RootDirectory, infoPath); string text; - if (Path.IsPathRooted(info)) { - text = File.ReadAllText(info); - } else { - using (var reader = new StreamReader(TitleContainer.OpenStream(info))) - text = reader.ReadToEnd(); + 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+"); - // parse each texture region: " loc [piv ] [off ]" - foreach (Match match in Regex.Matches(text, @"(.+)\s+loc\s+([0-9+]+)\s+([0-9+]+)\s+([0-9+]+)\s+([0-9+]+)\s*(?:piv\s+([0-9.+-]+)\s+([0-9.+-]+))?\s*(?:off\s+([0-9.+-]+)\s+([0-9.+-]+))?")) { - // offset - var off = !match.Groups[8].Success ? Vector2.Zero : new Vector2( - float.Parse(match.Groups[8].Value, CultureInfo.InvariantCulture), - float.Parse(match.Groups[9].Value, CultureInfo.InvariantCulture)); + 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(); - // location - var loc = new Rectangle( - int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value), - int.Parse(match.Groups[4].Value), int.Parse(match.Groups[5].Value)); - loc.Offset(off.ToPoint()); + 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(); + } - // pivot - var piv = !match.Groups[6].Success ? Vector2.Zero : off + new Vector2( - float.Parse(match.Groups[6].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.X), - float.Parse(match.Groups[7].Value, CultureInfo.InvariantCulture) - (pivotRelative ? 0 : loc.Y)); - - foreach (var name in Regex.Split(match.Groups[1].Value, @"\s")) { - var trimmed = name.Trim(); - if (trimmed.Length <= 0) - continue; - var region = new TextureRegion(texture, loc) { - PivotPixels = piv, - Name = trimmed - }; - atlas.regions.Add(trimmed, region); + // 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); } } @@ -128,6 +187,7 @@ namespace MLEM.Data { /// /// 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 diff --git a/Tests/Content/Texture.atlas b/Tests/Content/Texture.atlas index fd90b79..ecf9128 100644 --- a/Tests/Content/Texture.atlas +++ b/Tests/Content/Texture.atlas @@ -13,11 +13,19 @@ TestRegionNegativePivot loc 0 32 +16 16 piv -32 +46 +DataTest +loc 0 0 16 16 +dat DataPoint1 ThisIsSomeData +dat DataPoint2 3.5 +dat DataPoint3 --- + LongTableUp -loc 0 32 64 48 piv 16 48 +loc 0 32 64 48 +copy Copy1 16 0 +cpy Copy2 32 4 LongTableRight LongTableDown LongTableLeft -loc 32 30 64 48 +location 32 30 64 48 piv 80 46 -off 32 2 +offset 32 2 diff --git a/Tests/DataTextureAtlasTests.cs b/Tests/DataTextureAtlasTests.cs index 551e9ae..35ed699 100644 --- a/Tests/DataTextureAtlasTests.cs +++ b/Tests/DataTextureAtlasTests.cs @@ -13,7 +13,7 @@ namespace Tests { using var game = TestGame.Create(); using var texture = new Texture2D(game.GraphicsDevice, 1, 1); var atlas = DataTextureAtlas.LoadAtlasData(new TextureRegion(texture), game.RawContent, "Texture.atlas"); - Assert.AreEqual(atlas.Regions.Count(), 8); + Assert.AreEqual(11, atlas.Regions.Count()); // no added offset var table = atlas["LongTableUp"]; @@ -29,6 +29,20 @@ namespace Tests { var negativePivot = atlas["TestRegionNegativePivot"]; Assert.AreEqual(negativePivot.Area, new Rectangle(0, 32, 16, 16)); Assert.AreEqual(negativePivot.PivotPixels, new Vector2(-32, 46 - 32)); + + // copies + var copy1 = atlas["Copy1"]; + Assert.AreEqual(copy1.Area, new Rectangle(0 + 16, 32, 64, 48)); + Assert.AreEqual(copy1.PivotPixels, new Vector2(16 + 16, 48 - 32)); + var copy2 = atlas["Copy2"]; + Assert.AreEqual(copy2.Area, new Rectangle(0 + 32, 32 + 4, 64, 48)); + Assert.AreEqual(copy2.PivotPixels, new Vector2(16 + 32, 48 - 32 + 4)); + + // data + var data = atlas["DataTest"]; + Assert.AreEqual("ThisIsSomeData", data.GetData("DataPoint1")); + Assert.AreEqual("3.5", data.GetData("DataPoint2")); + Assert.AreEqual("---", data.GetData("DataPoint3")); } }