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"));
}
}