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