diff --git a/.build/Build.csproj b/.build/Build.csproj index 7d4f7fe..ff9f862 100644 --- a/.build/Build.csproj +++ b/.build/Build.csproj @@ -1,8 +1,8 @@ Exe - net7.0 - enable + net8.0 + enable $(MSBuildProjectDirectory) diff --git a/.github/release-notes.txt b/.github/release-notes.txt index fc4048f..4347992 100644 --- a/.github/release-notes.txt +++ b/.github/release-notes.txt @@ -51,4 +51,24 @@ ## 5.1.2 - Resolves issue where AsepriteFile.TryGetSlice always returned false even when slice exists +## 5.1.3-beta-1 +- Added `MonoGame.Aseprite.Configuration` + - When set to `false` this will assume frame index gets from the `AsepriteFile` start at index 1 and not 0. + +## 5.1.3-beta-2 +- Removed `MonoGame.Aserpite.Configuration` +- Moved `ZeroIndexedFrames` to be property of `AsepriteFile` +- Added unit testing for `AsepriteFile.ZeroIndexedFrames` functionality + +## 5.1.3-beta-3 +- Added proper support for new tile rotation introduced in Aseprite 1.3 + +## 5.1.3 +- Added `MonoGame.Aseprite.Configuration` + - When set to `false` this will assume frame index gets from the `AsepriteFile` start at index 1 and not 0. +- Removed `MonoGame.Aserpite.Configuration` +- Moved `ZeroIndexedFrames` to be property of `AsepriteFile` +- Added unit testing for `AsepriteFile.ZeroIndexedFrames` functionality +- Added proper support for new tile rotation introduced in Aseprite 1.3 +- Read Old Palette Chunk for Indexed mode due to Aseprite 1.3.5 file spec change \ No newline at end of file diff --git a/.nuget/README.md b/.nuget/README.md index d7c96bd..ae594a5 100644 --- a/.nuget/README.md +++ b/.nuget/README.md @@ -3,7 +3,7 @@ A Cross Platform C# Library That Adds Support For Aseprite Files in MonoGame Projects. [![build-and-test](https://github.com/AristurtleDev/monogame-aseprite/actions/workflows/buildandtest.yml/badge.svg)](https://github.com/AristurtleDev/monogame-aseprite/actions/workflows/buildandtest.yml) -[![Nuget 5.1.2](https://img.shields.io/nuget/v/MonoGame.Aseprite?color=blue&style=flat-square)](https://www.nuget.org/packages/MonoGame.Aseprite/5.1.2) +[![Nuget 5.1.3](https://img.shields.io/nuget/v/MonoGame.Aseprite?color=blue&style=flat-square)](https://www.nuget.org/packages/MonoGame.Aseprite/5.1.3) [![License: MIT](https://img.shields.io/badge/📃%20license-MIT-blue?style=flat)](LICENSE) [![Twitter](https://img.shields.io/badge/%20-Share%20On%20Twitter-555?style=flat&logo=twitter)](https://twitter.com/intent/tweet?text=MonoGame.Aseprite%20by%20%40aristurtledev%0A%0AA%20cross-platform%20C%23%20library%20that%20adds%20support%20for%20Aseprite%20files%20in%20MonoGame%20projects.%20https%3A%2F%2Fgithub.com%2FAristurtleDev%2Fmonogame-aseprite%0A%0A%23monogame%20%23aseprite%20%23dotnet%20%23csharp%20%23oss%0A) diff --git a/README.md b/README.md index e5acdec..1fafd96 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A Cross Platform C# Library That Adds Support For Aseprite Files in MonoGame Projects. [![build-and-test](https://github.com/AristurtleDev/monogame-aseprite/actions/workflows/buildandtest.yml/badge.svg)](https://github.com/AristurtleDev/monogame-aseprite/actions/workflows/buildandtest.yml) -[![Nuget 5.1.2](https://img.shields.io/nuget/v/MonoGame.Aseprite?color=blue&style=flat-square)](https://www.nuget.org/packages/MonoGame.Aseprite/5.1.2) +[![Nuget 5.1.3](https://img.shields.io/nuget/v/MonoGame.Aseprite?color=blue&style=flat-square)](https://www.nuget.org/packages/MonoGame.Aseprite/5.1.3) [![License: MIT](https://img.shields.io/badge/📃%20license-MIT-blue?style=flat)](LICENSE) [![Twitter](https://img.shields.io/badge/%20-Share%20On%20Twitter-555?style=flat&logo=twitter)](https://twitter.com/intent/tweet?text=MonoGame.Aseprite%20by%20%40aristurtledev%0A%0AA%20cross-platform%20C%23%20library%20that%20adds%20support%20for%20Aseprite%20files%20in%20MonoGame%20projects.%20https%3A%2F%2Fgithub.com%2FAristurtleDev%2Fmonogame-aseprite%0A%0A%23monogame%20%23aseprite%20%23dotnet%20%23csharp%20%23oss%0A) diff --git a/source/MonoGame.Aseprite.Content.Pipeline/MonoGame.Aseprite.Content.Pipeline.csproj b/source/MonoGame.Aseprite.Content.Pipeline/MonoGame.Aseprite.Content.Pipeline.csproj index 7dfdb91..7afabf0 100644 --- a/source/MonoGame.Aseprite.Content.Pipeline/MonoGame.Aseprite.Content.Pipeline.csproj +++ b/source/MonoGame.Aseprite.Content.Pipeline/MonoGame.Aseprite.Content.Pipeline.csproj @@ -4,7 +4,7 @@ enable enable True - 5.1.2 + 5.1.3 @@ -38,9 +38,14 @@ MonoGame;Aseprite;import;processes;read;write;sprite;animation;tileset;tilemap;spritesheet;pipeline;mgcb - Version 5.1.2 - The following changes were implemented: - - Resolves issue where AsepriteFile.TryGetSlice always returned false even when slice exists + Version 5.1.3 + - Added `MonoGame.Aseprite.Configuration` + - When set to `false` this will assume frame index gets from the `AsepriteFile` start at index 1 and not 0. + - Removed `MonoGame.Aserpite.Configuration` + - Moved `ZeroIndexedFrames` to be property of `AsepriteFile` + - Added unit testing for `AsepriteFile.ZeroIndexedFrames` functionality + - Added proper support for new tile rotation introduced in Aseprite 1.3 + - Read Old Palette Chunk for Indexed mode due to Aseprite 1.3.5 file spec change MonoGame.Aseprite.Content.Pipeline is a cross-platform C# library that adds an extension to the MonoGame diff --git a/source/MonoGame.Aseprite.Content.Pipeline/Processors/SpriteContentProcessor.cs b/source/MonoGame.Aseprite.Content.Pipeline/Processors/SpriteContentProcessor.cs index f240bd9..a09bcd5 100644 --- a/source/MonoGame.Aseprite.Content.Pipeline/Processors/SpriteContentProcessor.cs +++ b/source/MonoGame.Aseprite.Content.Pipeline/Processors/SpriteContentProcessor.cs @@ -34,6 +34,9 @@ namespace MonoGame.Aseprite.Content.Pipeline.Processors; [ContentProcessor(DisplayName = "Aseprite Sprite Processor - MonoGame.Aseprite")] internal sealed class SpriteContentProcessor : ContentProcessor { + [DisplayName("Zero Indexed Frames")] + public bool ZeroIndexedFrames {get; set;} = true; + [DisplayName("Frame Index")] public int FrameIndex { get; set; } = 0; @@ -51,9 +54,21 @@ internal sealed class SpriteContentProcessor : ContentProcessor= content.AsepriteFile.FrameCount) { - throw new ProcessorParameterException($"The 'Frame Index' parameter cannot be less than zero or greater than or equal to the total number of frames in the Aseprite file", nameof(SpriteContentProcessor), nameof(FrameIndex)); + ProcessorParameterException ex; + if(ZeroIndexedFrames) + { + ex = new ($"The 'Frame Index' parameter cannot be less than zero or greater than or equal to the total number of frames in the Aseprite file", nameof(SpriteContentProcessor), nameof(FrameIndex)); + } + else + { + ex = new ($"The 'Frame Index' parameter cannot be less than one or greater than the total number of frames in the Aseprite file when {nameof(ZeroIndexedFrames)} is 'false'.", nameof(SpriteContentProcessor), nameof(FrameIndex)); + } + + throw ex; } AsepriteFrame aseFrame = content.AsepriteFile.Frames[FrameIndex]; diff --git a/source/MonoGame.Aseprite.Content.Pipeline/Processors/TilemapContentProcessor.cs b/source/MonoGame.Aseprite.Content.Pipeline/Processors/TilemapContentProcessor.cs index 7b8a53a..a741fcb 100644 --- a/source/MonoGame.Aseprite.Content.Pipeline/Processors/TilemapContentProcessor.cs +++ b/source/MonoGame.Aseprite.Content.Pipeline/Processors/TilemapContentProcessor.cs @@ -34,6 +34,9 @@ namespace MonoGame.Aseprite.Content.Pipeline.Processors; [ContentProcessor(DisplayName = "Aseprite Tilemap Processor - MonoGame.Aseprite")] internal sealed class TilemapContentProcessor : ContentProcessor { + [DisplayName("Zero Indexed Frames")] + public bool ZeroIndexedFrames { get; set; } = true; + [DisplayName("Frame Index")] public int FrameIndex { get; set; } = 0; @@ -45,6 +48,7 @@ internal sealed class TilemapContentProcessor : ContentProcessor + /// if, when referencing frames by index, the first frame is index 0; otherwise + /// if the first frame is index 1. + /// + public bool ZeroIndexedFrames = true; + /// /// Gets a read-only span of all elements in this . /// + /// + /// The ordering of elements is not affected by . + /// public ReadOnlySpan Frames => _frames; /// @@ -142,6 +151,10 @@ internal AsepriteFile(string name, int width, int height, Color[] palette, Asepr /// /// The index of the to locate. /// + /// + /// You can specify non-zero indexed frames using + /// + /// /// /// The located. /// @@ -151,14 +164,25 @@ internal AsepriteFile(string name, int width, int height, Color[] palette, Asepr /// public AsepriteFrame GetFrame(int frameIndex) { - if (frameIndex < 0 || frameIndex >= _frames.Length) + int index = ZeroIndexedFrames ? frameIndex : frameIndex - 1; + + if (index < 0 || index >= _frames.Length) { - ArgumentOutOfRangeException ex = new(nameof(frameIndex), $"{nameof(frameIndex)} cannot be less than zero or greater than or equal to the total number of frames in this aseprite file."); + ArgumentOutOfRangeException ex; + if (ZeroIndexedFrames) + { + ex = new(nameof(frameIndex), $"{nameof(frameIndex)} cannot be less than zero or greater than or equal to the total number of frames in this aseprite file."); + } + else + { + ex = new(nameof(frameIndex), $"{nameof(frameIndex)} cannot be less than one or greater than the total number of frames in this aseprite file when '{nameof(ZeroIndexedFrames)}' is 'false'."); + } ex.Data.Add("TotalFrames", _frames.Length); + ex.Data.Add("ZeroIndexed", ZeroIndexedFrames); throw ex; } - return _frames[frameIndex]; + return _frames[index]; } /// @@ -179,14 +203,15 @@ public AsepriteFrame GetFrame(int frameIndex) /// public bool TryGetFrame(int frameIndex, [NotNullWhen(true)] out AsepriteFrame? located) { + int index = ZeroIndexedFrames ? frameIndex : frameIndex - 1; located = default; - if (frameIndex < 0 || frameIndex >= _frames.Length) + if (index < 0 || index >= _frames.Length) { return false; } - located = _frames[frameIndex]; + located = _frames[index]; return located is not null; } diff --git a/source/MonoGame.Aseprite.Shared/AsepriteTypes/AsepriteFileBuilder.cs b/source/MonoGame.Aseprite.Shared/AsepriteTypes/AsepriteFileBuilder.cs index 4aba891..42f8850 100644 --- a/source/MonoGame.Aseprite.Shared/AsepriteTypes/AsepriteFileBuilder.cs +++ b/source/MonoGame.Aseprite.Shared/AsepriteTypes/AsepriteFileBuilder.cs @@ -1,302 +1,302 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -using System.IO.Compression; -using Microsoft.Xna.Framework; - -namespace MonoGame.Aseprite.AsepriteTypes; - -internal class AsepriteFileBuilder -{ - private Color[] _palette = Array.Empty(); - private byte _transparentIndex = new(); - private ushort _colorDepth; - private bool _layerOpacityValid; - private ushort _frameWidth; - private ushort _frameHeight; - private List _frames = new(); - private List _layers = new(); - private List _tags = new(); - private List _slices = new(); - private List _tilesets = new(); - private List _nextFrameCels = new(); - private string _name; - private AsepriteUserData _spriteUserData = new(); - - internal AsepriteFileBuilder(string name) => _name = name; - - internal void SetFrameWidth(ushort width) => _frameWidth = width; - internal void SetFrameHeight(ushort height) => _frameHeight = height; - internal void SetColorDepth(ushort depth) => _colorDepth = depth; - internal void SetTransparentIndex(byte index) - { - if (_colorDepth != 8) - { - // Non-zero transparent index is only valid when color depth is 8 (Indexed mode) - _transparentIndex = 0; - } - else - { - _transparentIndex = index; - } - } - internal void SetLayerOpacityValid(bool isValid) => _layerOpacityValid = isValid; - - internal void AddFrame(int duration) - { - AsepriteFrame frame = new($"{_name} {_frames.Count}", _frameWidth, _frameHeight, duration, _nextFrameCels.ToArray()); - _nextFrameCels.Clear(); - _frames.Add(frame); - } - - internal void AddLayer(AsepriteLayerFlags flags, ushort blend, byte opacity, string name) - { - if (!_layerOpacityValid) - { - opacity = 255; - } - - AsepriteLayer layer = new(flags, (AsepriteBlendMode)blend, opacity, name); - _layers.Add(layer); - } - - internal void AddTilemapLayer(uint tilesetIndex, AsepriteLayerFlags flags, ushort blend, byte opacity, string name) - { - AsepriteTileset tileset = _tilesets[(int)tilesetIndex]; - AsepriteTilemapLayer layer = new(tileset, /*(int)tilesetIndex,*/ flags, (AsepriteBlendMode)blend, opacity, name); - _layers.Add(layer); - } - - internal void AddRawImageCel(short x, short y, ushort width, ushort height, ushort layerIndex, byte opacity, ReadOnlySpan data) - { - Color[] pixels = new Color[width * height]; - ToColor(data, pixels); - AsepriteLayer layer = _layers[layerIndex]; - Point position = new(x, y); - AsepriteImageCel cel = new(width, height, pixels, layer, position, opacity); - _nextFrameCels.Add(cel); - } - - internal void AddLinkedCel(ushort frameIndex) - { - AsepriteFrame frame = _frames[frameIndex]; - // If the first cel is a linked cel, then we haven't added cels yet - // so the " - 1" will result in -1. So we only do so when the count is - // greater than 0 - AsepriteCel linkedCel = frame.Cels[_nextFrameCels.Count > 0 ? _nextFrameCels.Count - 1 : 0]; - _nextFrameCels.Add(linkedCel); - } - - internal void AddCompressedImageCel(short x, short y, ushort width, ushort height, ushort layerIndex, byte opacity, byte[] compressedData) - { - Color[] pixels = new Color[width * height]; - byte[] decompressedData = Decompress(compressedData); - AddRawImageCel(x, y, width, height, layerIndex, opacity, decompressedData); - } - - internal void AddCompressedTilemapCel(short x, short y, ushort columns, ushort rows, ushort layerIndex, byte opacity, byte[] compressedData, ushort bitsPerTile, uint idBitmask, uint xFlipBitmask, uint yFlipBitmask, uint rotationBitmask) - { - Span decompressedData = Decompress(compressedData); - - int bytesPerTile = bitsPerTile / 8; - int tileCount = decompressedData.Length / bytesPerTile; - AsepriteTile[] tiles = new AsepriteTile[tileCount]; - - for (int i = 0, b = 0; i < tileCount; i++, b += bytesPerTile) - { - ReadOnlySpan dword = decompressedData.Slice(b, bytesPerTile); - uint value = BitConverter.ToUInt32(dword); - uint id = (value & idBitmask) >> 0; - uint xFlip = (value & xFlipBitmask); - uint yFlip = (value & yFlipBitmask); - uint rotation = (value & rotationBitmask); - AsepriteTile tile = new((int)id, (int)xFlip, (int)yFlip, (int)rotation); - tiles[i] = tile; - } - - AsepriteLayer layer = _layers[layerIndex]; - Point position = new(x, y); - AsepriteTilemapCel cel = new(columns, rows, tiles, layer, position, opacity); - _nextFrameCels.Add(cel); - } - - internal void AddTag(ushort from, ushort to, byte direction, ushort repeat, ReadOnlySpan rgb, string name) - { - Color color = Color.FromNonPremultiplied(rgb[0], rgb[1], rgb[2], (byte)255); - AsepriteTag tag = new(from, to, (AsepriteLoopDirection)direction, repeat, color, name); - _tags.Add(tag); - } - - internal void ResizePalette(uint newSize) - { - if (newSize > 0 && newSize > _palette.Length) - { - Color[] tmp = new Color[newSize]; - Array.Copy(_palette, tmp, _palette.Length); - _palette = tmp; - } - } - - internal void AddPaletteEntry(uint index, ReadOnlySpan rgba) - { - _palette[index] = Color.FromNonPremultiplied(rgba[0], rgba[1], rgba[2], rgba[3]); - } - - internal void AddSlice(string name, bool isNinePatch, bool hasPivot, AsepriteSliceKey[] keys) - { - AsepriteSlice slice = new(name, isNinePatch, hasPivot, keys); - _slices.Add(slice); - } - - internal void AddTileset(uint id, uint count, ushort tileWidth, ushort tileHeight, string name, byte[] compressedData) - { - byte[] decompressedData = Decompress(compressedData); - Color[] pixels = new Color[tileWidth * (tileHeight * count)]; - ToColor(decompressedData, pixels); - AsepriteTileset tileset = new((int)id, (int)count, tileWidth, tileHeight, name, pixels); - _tilesets.Add(tileset); - } - - internal void SetLastCelUserData(string? text, Color? color) - { - AsepriteCel cel = _nextFrameCels[_nextFrameCels.Count - 1]; - cel.UserData.Text = text; - cel.UserData.Color = color; - } - - internal void SetLastLayerUserData(string? text, Color? color) - { - AsepriteLayer layer = _layers[_layers.Count - 1]; - layer.UserData.Text = text; - layer.UserData.Color = color; - } - - internal void SetLastSliceUserData(string? text, Color? color) - { - AsepriteSlice slice = _slices[_slices.Count - 1]; - slice.UserData.Text = text; - slice.UserData.Color = color; - } - - internal void SetTagUserData(int index, string? text, Color? color) - { - AsepriteTag tag = _tags[index]; - tag.UserData.Text = text; - tag.UserData.Color = color; - } - - internal void SetSpriteUserData(string? text, Color? color) - { - _spriteUserData.Text = text; - _spriteUserData.Color = color; - } - - internal void SetTilesetUserData(string? text, Color? color) - { - AsepriteTileset tileset = _tilesets[_tilesets.Count - 1]; - tileset.UserData.Text = text; - tileset.UserData.Color = color; - } - - internal AsepriteFile Build() => - new(_name, _frameWidth, _frameHeight, _palette, _frames.ToArray(), _layers.ToArray(), _tags.ToArray(), _slices.ToArray(), _tilesets.ToArray(), _spriteUserData); - - private static byte[] Decompress(byte[] buffer) - { - using MemoryStream compressedStream = new(buffer); - - // First 2 bytes are the zlib header information, skip past them. - _ = compressedStream.ReadByte(); - _ = compressedStream.ReadByte(); - - using MemoryStream decompressedStream = new(); - using DeflateStream deflateStream = new(compressedStream, CompressionMode.Decompress); - deflateStream.CopyTo(decompressedStream); - return decompressedStream.ToArray(); - } - - private void ToColor(ReadOnlySpan source, Span dest) - { - const ushort INDEXED_COLOR_DEPTH = 8; - const ushort GRAYSCALE_COLOR_DEPTH = 16; - const ushort RGBA_COLOR_DEPTH = 32; - - int bytesPerPixel = _colorDepth / 8; - - switch (_colorDepth) - { - case INDEXED_COLOR_DEPTH: - IndexedToColor(source, dest); - break; - case GRAYSCALE_COLOR_DEPTH: - GrayscaleToColor(source, dest); - break; - case RGBA_COLOR_DEPTH: - RgbaToColor(source, dest); - break; - default: - throw new InvalidOperationException($"Invalid Color Depth '{_colorDepth}'"); - } - } - - private void IndexedToColor(ReadOnlySpan source, Span dest) - { - for (int i = 0; i < source.Length; i++) - { - int paletteIndex = source[i]; - - if (paletteIndex == _transparentIndex) - { - dest[i] = Color.FromNonPremultiplied(0, 0, 0, 0); - } - else - { - dest[i] = _palette[paletteIndex]; - } - } - } - - private void GrayscaleToColor(ReadOnlySpan source, Span dest) - { - for (int i = 0, b = 0; i < dest.Length; i++, b += 2) - { - byte rgb = source[b]; - byte a = source[b + 1]; - dest[i] = Color.FromNonPremultiplied(rgb, rgb, rgb, a); - } - } - - private void RgbaToColor(ReadOnlySpan source, Span dest) - { - for (int i = 0, b = 0; i < dest.Length; i++, b += 4) - { - byte red = source[b]; - byte green = source[b + 1]; - byte blue = source[b + 2]; - byte alpha = source[b + 3]; - dest[i] = Color.FromNonPremultiplied(red, green, blue, alpha); - } - } -} - +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +using System.IO.Compression; +using Microsoft.Xna.Framework; + +namespace MonoGame.Aseprite.AsepriteTypes; + +internal class AsepriteFileBuilder +{ + private Color[] _palette = Array.Empty(); + private byte _transparentIndex = new(); + private ushort _colorDepth; + private bool _layerOpacityValid; + private ushort _frameWidth; + private ushort _frameHeight; + private List _frames = new(); + private List _layers = new(); + private List _tags = new(); + private List _slices = new(); + private List _tilesets = new(); + private List _nextFrameCels = new(); + private string _name; + private AsepriteUserData _spriteUserData = new(); + + internal AsepriteFileBuilder(string name) => _name = name; + + internal void SetFrameWidth(ushort width) => _frameWidth = width; + internal void SetFrameHeight(ushort height) => _frameHeight = height; + internal void SetColorDepth(ushort depth) => _colorDepth = depth; + internal void SetTransparentIndex(byte index) + { + if (_colorDepth != 8) + { + // Non-zero transparent index is only valid when color depth is 8 (Indexed mode) + _transparentIndex = 0; + } + else + { + _transparentIndex = index; + } + } + internal void SetLayerOpacityValid(bool isValid) => _layerOpacityValid = isValid; + + internal void AddFrame(int duration) + { + AsepriteFrame frame = new($"{_name} {_frames.Count}", _frameWidth, _frameHeight, duration, _nextFrameCels.ToArray()); + _nextFrameCels.Clear(); + _frames.Add(frame); + } + + internal void AddLayer(AsepriteLayerFlags flags, ushort blend, byte opacity, string name) + { + if (!_layerOpacityValid) + { + opacity = 255; + } + + AsepriteLayer layer = new(flags, (AsepriteBlendMode)blend, opacity, name); + _layers.Add(layer); + } + + internal void AddTilemapLayer(uint tilesetIndex, AsepriteLayerFlags flags, ushort blend, byte opacity, string name) + { + AsepriteTileset tileset = _tilesets[(int)tilesetIndex]; + AsepriteTilemapLayer layer = new(tileset, /*(int)tilesetIndex,*/ flags, (AsepriteBlendMode)blend, opacity, name); + _layers.Add(layer); + } + + internal void AddRawImageCel(short x, short y, ushort width, ushort height, ushort layerIndex, byte opacity, ReadOnlySpan data) + { + Color[] pixels = new Color[width * height]; + ToColor(data, pixels); + AsepriteLayer layer = _layers[layerIndex]; + Point position = new(x, y); + AsepriteImageCel cel = new(width, height, pixels, layer, position, opacity); + _nextFrameCels.Add(cel); + } + + internal void AddLinkedCel(ushort frameIndex) + { + AsepriteFrame frame = _frames[frameIndex]; + // If the first cel is a linked cel, then we haven't added cels yet + // so the " - 1" will result in -1. So we only do so when the count is + // greater than 0 + AsepriteCel linkedCel = frame.Cels[_nextFrameCels.Count > 0 ? _nextFrameCels.Count - 1 : 0]; + _nextFrameCels.Add(linkedCel); + } + + internal void AddCompressedImageCel(short x, short y, ushort width, ushort height, ushort layerIndex, byte opacity, byte[] compressedData) + { + Color[] pixels = new Color[width * height]; + byte[] decompressedData = Decompress(compressedData); + AddRawImageCel(x, y, width, height, layerIndex, opacity, decompressedData); + } + + internal void AddCompressedTilemapCel(short x, short y, ushort columns, ushort rows, ushort layerIndex, byte opacity, byte[] compressedData, ushort bitsPerTile, uint idBitmask, uint xFlipBitmask, uint yFlipBitmask, uint dFlipBitmask) + { + Span decompressedData = Decompress(compressedData); + + int bytesPerTile = bitsPerTile / 8; + int tileCount = decompressedData.Length / bytesPerTile; + AsepriteTile[] tiles = new AsepriteTile[tileCount]; + + for (int i = 0, b = 0; i < tileCount; i++, b += bytesPerTile) + { + ReadOnlySpan dword = decompressedData.Slice(b, bytesPerTile); + uint value = BitConverter.ToUInt32(dword); + uint id = (value & idBitmask) >> 0; + bool xFlip = (value & xFlipBitmask) == xFlipBitmask; + bool yFlip = (value & yFlipBitmask) == yFlipBitmask; + bool dFlip = (value & dFlipBitmask) == dFlipBitmask; + AsepriteTile tile = new((int)id, xFlip, yFlip, dFlip); + tiles[i] = tile; + } + + AsepriteLayer layer = _layers[layerIndex]; + Point position = new(x, y); + AsepriteTilemapCel cel = new(columns, rows, tiles, layer, position, opacity); + _nextFrameCels.Add(cel); + } + + internal void AddTag(ushort from, ushort to, byte direction, ushort repeat, ReadOnlySpan rgb, string name) + { + Color color = Color.FromNonPremultiplied(rgb[0], rgb[1], rgb[2], (byte)255); + AsepriteTag tag = new(from, to, (AsepriteLoopDirection)direction, repeat, color, name); + _tags.Add(tag); + } + + internal void ResizePalette(uint newSize) + { + if (newSize > 0 && newSize > _palette.Length) + { + Color[] tmp = new Color[newSize]; + Array.Copy(_palette, tmp, _palette.Length); + _palette = tmp; + } + } + + internal void AddPaletteEntry(uint index, ReadOnlySpan rgba) + { + _palette[index] = Color.FromNonPremultiplied(rgba[0], rgba[1], rgba[2], rgba[3]); + } + + internal void AddSlice(string name, bool isNinePatch, bool hasPivot, AsepriteSliceKey[] keys) + { + AsepriteSlice slice = new(name, isNinePatch, hasPivot, keys); + _slices.Add(slice); + } + + internal void AddTileset(uint id, uint count, ushort tileWidth, ushort tileHeight, string name, byte[] compressedData) + { + byte[] decompressedData = Decompress(compressedData); + Color[] pixels = new Color[tileWidth * (tileHeight * count)]; + ToColor(decompressedData, pixels); + AsepriteTileset tileset = new((int)id, (int)count, tileWidth, tileHeight, name, pixels); + _tilesets.Add(tileset); + } + + internal void SetLastCelUserData(string? text, Color? color) + { + AsepriteCel cel = _nextFrameCels[_nextFrameCels.Count - 1]; + cel.UserData.Text = text; + cel.UserData.Color = color; + } + + internal void SetLastLayerUserData(string? text, Color? color) + { + AsepriteLayer layer = _layers[_layers.Count - 1]; + layer.UserData.Text = text; + layer.UserData.Color = color; + } + + internal void SetLastSliceUserData(string? text, Color? color) + { + AsepriteSlice slice = _slices[_slices.Count - 1]; + slice.UserData.Text = text; + slice.UserData.Color = color; + } + + internal void SetTagUserData(int index, string? text, Color? color) + { + AsepriteTag tag = _tags[index]; + tag.UserData.Text = text; + tag.UserData.Color = color; + } + + internal void SetSpriteUserData(string? text, Color? color) + { + _spriteUserData.Text = text; + _spriteUserData.Color = color; + } + + internal void SetTilesetUserData(string? text, Color? color) + { + AsepriteTileset tileset = _tilesets[_tilesets.Count - 1]; + tileset.UserData.Text = text; + tileset.UserData.Color = color; + } + + internal AsepriteFile Build() => + new(_name, _frameWidth, _frameHeight, _palette, _frames.ToArray(), _layers.ToArray(), _tags.ToArray(), _slices.ToArray(), _tilesets.ToArray(), _spriteUserData); + + private static byte[] Decompress(byte[] buffer) + { + using MemoryStream compressedStream = new(buffer); + + // First 2 bytes are the zlib header information, skip past them. + _ = compressedStream.ReadByte(); + _ = compressedStream.ReadByte(); + + using MemoryStream decompressedStream = new(); + using DeflateStream deflateStream = new(compressedStream, CompressionMode.Decompress); + deflateStream.CopyTo(decompressedStream); + return decompressedStream.ToArray(); + } + + private void ToColor(ReadOnlySpan source, Span dest) + { + const ushort INDEXED_COLOR_DEPTH = 8; + const ushort GRAYSCALE_COLOR_DEPTH = 16; + const ushort RGBA_COLOR_DEPTH = 32; + + int bytesPerPixel = _colorDepth / 8; + + switch (_colorDepth) + { + case INDEXED_COLOR_DEPTH: + IndexedToColor(source, dest); + break; + case GRAYSCALE_COLOR_DEPTH: + GrayscaleToColor(source, dest); + break; + case RGBA_COLOR_DEPTH: + RgbaToColor(source, dest); + break; + default: + throw new InvalidOperationException($"Invalid Color Depth '{_colorDepth}'"); + } + } + + private void IndexedToColor(ReadOnlySpan source, Span dest) + { + for (int i = 0; i < source.Length; i++) + { + int paletteIndex = source[i]; + + if (paletteIndex == _transparentIndex) + { + dest[i] = Color.FromNonPremultiplied(0, 0, 0, 0); + } + else + { + dest[i] = _palette[paletteIndex]; + } + } + } + + private void GrayscaleToColor(ReadOnlySpan source, Span dest) + { + for (int i = 0, b = 0; i < dest.Length; i++, b += 2) + { + byte rgb = source[b]; + byte a = source[b + 1]; + dest[i] = Color.FromNonPremultiplied(rgb, rgb, rgb, a); + } + } + + private void RgbaToColor(ReadOnlySpan source, Span dest) + { + for (int i = 0, b = 0; i < dest.Length; i++, b += 4) + { + byte red = source[b]; + byte green = source[b + 1]; + byte blue = source[b + 2]; + byte alpha = source[b + 3]; + dest[i] = Color.FromNonPremultiplied(red, green, blue, alpha); + } + } +} + diff --git a/source/MonoGame.Aseprite.Shared/AsepriteTypes/AsepriteTile.cs b/source/MonoGame.Aseprite.Shared/AsepriteTypes/AsepriteTile.cs index c236dbd..202504a 100644 --- a/source/MonoGame.Aseprite.Shared/AsepriteTypes/AsepriteTile.cs +++ b/source/MonoGame.Aseprite.Shared/AsepriteTypes/AsepriteTile.cs @@ -1,84 +1,75 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -namespace MonoGame.Aseprite.AsepriteTypes; - -/// -/// Defines a tile in a -/// -public sealed class AsepriteTile : IEquatable -{ - /// - /// Gets the id of the source tile in the that contains the source image data for - /// this . - /// - public int TilesetTileID { get; } - - /// - /// Gets the x-flip value for this .. - /// - /// - /// As of the current Aseprite 1.3-beta-21, tile x-flip has not been implemented. As such, this value will - /// always be 0. - /// - public int XFlip { get; } - - /// - /// Gets the y-flip value for this .. - /// - /// - /// As of the current Aseprite 1.3-beta-21, tile y-flip has not been implemented. As such, this value will - /// always be 0. - /// - public int YFlip { get; } - - /// - /// Gets the 90deg clockwise rotation value for this .. - /// - /// - /// As of the current Aseprite 1.3-beta-21, tile rotation has not been implemented. As such, this value will - /// always be 0. - /// - public int Rotation { get; } - - internal AsepriteTile(int tilesetTileId, int xFlip, int yFlip, int rotation) => - (TilesetTileID, XFlip, YFlip, Rotation) = (tilesetTileId, xFlip, yFlip, rotation); - - /// - /// Returns a value that indicates whether the specified . is equal to this - /// .. - /// - /// - /// The other to check for equality with this . - /// - /// - /// if the specified is equal to this - /// ; otherwise, . - /// - public bool Equals(AsepriteTile? other) => other is not null && - XFlip == other.XFlip && - YFlip == other.YFlip && - Rotation == other.Rotation && - TilesetTileID == other.TilesetTileID; -} +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +namespace MonoGame.Aseprite.AsepriteTypes; + +/// +/// Defines a tile in a +/// +public sealed class AsepriteTile : IEquatable +{ + /// + /// Gets the id of the source tile in the that contains the source image data for + /// this . + /// + public int TilesetTileID { get; } + + /// + /// Gets a value that indicates if this is + /// flipped on the x-axis. + /// + public bool XFlip { get; } + + /// + /// Gets a value that indicates if this is + /// flipped on the y-axis. + /// + public bool YFlip { get; } + + /// + /// Gets a value that indicates if this is + /// flipped on its diagonal-axis. + /// + public bool DFlip { get; } + + internal AsepriteTile(int tilesetTileId, bool xFlip, bool yFlip, bool dFlip) => + (TilesetTileID, XFlip, YFlip, DFlip) = (tilesetTileId, xFlip, yFlip, dFlip); + + /// + /// Returns a value that indicates whether the specified . is equal to this + /// .. + /// + /// + /// The other to check for equality with this . + /// + /// + /// if the specified is equal to this + /// ; otherwise, . + /// + public bool Equals(AsepriteTile? other) => other is not null && + XFlip == other.XFlip && + YFlip == other.YFlip && + DFlip == other.DFlip && + TilesetTileID == other.TilesetTileID; +} diff --git a/source/MonoGame.Aseprite.Shared/Content/BinaryReaderExtensions.cs b/source/MonoGame.Aseprite.Shared/Content/BinaryReaderExtensions.cs index e00039f..17910c1 100644 --- a/source/MonoGame.Aseprite.Shared/Content/BinaryReaderExtensions.cs +++ b/source/MonoGame.Aseprite.Shared/Content/BinaryReaderExtensions.cs @@ -1,277 +1,277 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -using Microsoft.Xna.Framework; -using MonoGame.Aseprite.RawTypes; - -namespace MonoGame.Aseprite.Content; - -internal static class BinaryReaderExtensions -{ - internal static void ReadMagic(this BinaryReader reader) - { - byte m = reader.ReadByte(); // [M]onoGame - byte a = reader.ReadByte(); // [A]seprite - byte c = reader.ReadByte(); // [C]ontent - - - if (m != 'M' || a != 'A' || c != 'C') - { - // Dispose of reader so stream is closed in case the caller has a try-catch that handles this exception. - reader.Dispose(); - throw new InvalidOperationException($"File contains invalid magic number. Was this processed using the MonoGame.Aseprite library?"); - } - } - - internal static Rectangle ReadRectangle(this BinaryReader reader) - { - Rectangle result = new(); - result.Location = ReadPoint(reader); - result.Size = ReadPoint(reader); - return result; - } - - internal static Point ReadPoint(this BinaryReader reader) - { - Point result = new(); - result.X = reader.ReadInt32(); - result.Y = reader.ReadInt32(); - return result; - } - - internal static Vector2 ReadVector2(this BinaryReader reader) - { - Vector2 result = new(); - result.X = reader.ReadSingle(); - result.Y = reader.ReadSingle(); - return result; - } - - internal static Color ReadColor(this BinaryReader reader) - { - Color result = new(); - result.R = reader.ReadByte(); - result.G = reader.ReadByte(); - result.B = reader.ReadByte(); - result.A = reader.ReadByte(); - return result; - } - - internal static RawTexture ReadRawTexture(this BinaryReader reader) - { - string name = reader.ReadString(); - int width = reader.ReadInt32(); - int height = reader.ReadInt32(); - - Color[] pixels = reader.ReadColors(); - - return new(name, pixels, width, height); - } - - internal static Color[] ReadColors(this BinaryReader reader) - { - int count = reader.ReadInt32(); - Color[] pixels = new Color[count]; - for (int i = 0; i < count; i++) - { - pixels[i] = reader.ReadColor(); - } - return pixels; - } - - internal static RawSlice ReadRawSlice(this BinaryReader reader) - { - string name = reader.ReadString(); - Rectangle bounds = reader.ReadRectangle(); - Vector2 origin = reader.ReadVector2(); - Color color = reader.ReadColor(); - - bool isNine = reader.ReadBoolean(); - - if (isNine) - { - Rectangle centerBounds = reader.ReadRectangle(); - return new RawNinePatchSlice(name, bounds, centerBounds, origin, color); - } - - return new RawSlice(name, bounds, origin, color); - } - - internal static RawAnimationTag[] ReadRawAnimationTags(this BinaryReader reader) - { - int count = reader.ReadInt32(); - RawAnimationTag[] tags = new RawAnimationTag[count]; - for (int i = 0; i < count; i++) - { - tags[i] = reader.ReadRawAnimationTag(); - } - return tags; - } - - internal static RawAnimationTag ReadRawAnimationTag(this BinaryReader reader) - { - string name = reader.ReadString(); - int loopCount = reader.ReadInt32(); - bool isReversed = reader.ReadBoolean(); - bool isPingPong = reader.ReadBoolean(); - RawAnimationFrame[] frames = reader.ReadRawAnimationFrames(); - return new(name, frames, loopCount, isReversed, isPingPong); - } - - internal static RawAnimationFrame[] ReadRawAnimationFrames(this BinaryReader reader) - { - int count = reader.ReadInt32(); - RawAnimationFrame[] frames = new RawAnimationFrame[count]; - for (int i = 0; i < count; i++) - { - frames[i] = reader.ReadRawAnimationFrame(); - } - return frames; - } - - internal static RawAnimationFrame ReadRawAnimationFrame(this BinaryReader reader) - { - int index = reader.ReadInt32(); - int duration = reader.ReadInt32(); - return new(index, duration); - } - - internal static RawTextureAtlas ReadRawTextureAtlas(this BinaryReader reader) - { - string name = reader.ReadString(); - RawTexture texture = reader.ReadRawTexture(); - RawTextureRegion[] regions = reader.ReadRawTextureRegions(); - return new(name, texture, regions); - } - - internal static RawTextureRegion[] ReadRawTextureRegions(this BinaryReader reader) - { - int count = reader.ReadInt32(); - RawTextureRegion[] regions = new RawTextureRegion[count]; - - for (int i = 0; i < count; i++) - { - regions[i] = reader.ReadRawTextureRegion(); - } - - return regions; - } - - internal static RawTextureRegion ReadRawTextureRegion(this BinaryReader reader) - { - string name = reader.ReadString(); - Rectangle bounds = reader.ReadRectangle(); - RawSlice[] slices = reader.ReadRawSlices(); - return new(name, bounds, slices); - } - - internal static RawSlice[] ReadRawSlices(this BinaryReader reader) - { - int count = reader.ReadInt32(); - RawSlice[] slices = new RawSlice[count]; - - for (int i = 0; i < count; i++) - { - slices[i] = reader.ReadRawSlice(); - } - - return slices; - } - - internal static RawTileset ReadRawTileset(this BinaryReader reader) - { - int id = reader.ReadInt32(); - string name = reader.ReadString(); - RawTexture texture = reader.ReadRawTexture(); - int tileWidth = reader.ReadInt32(); - int tileHeight = reader.ReadInt32(); - - return new(id, name, texture, tileWidth, tileHeight); - } - - internal static RawTilemapLayer[] ReadRawTilemapLayers(this BinaryReader reader) - { - int count = reader.ReadInt32(); - RawTilemapLayer[] layers = new RawTilemapLayer[count]; - for (int i = 0; i < count; i++) - { - layers[i] = reader.ReadRawTilemapLayer(); - } - return layers; - } - - internal static RawTilemapLayer ReadRawTilemapLayer(this BinaryReader reader) - { - string name = reader.ReadString(); - int tilesetID = reader.ReadInt32(); - int columns = reader.ReadInt32(); - int rows = reader.ReadInt32(); - Point offset = reader.ReadPoint(); - RawTilemapTile[] tiles = reader.ReadRawTilemapTiles(); - return new(name, tilesetID, columns, rows, tiles, offset); - } - - internal static RawTilemapTile[] ReadRawTilemapTiles(this BinaryReader reader) - { - int count = reader.ReadInt32(); - RawTilemapTile[] rawTilemapTiles = new RawTilemapTile[count]; - - for (int i = 0; i < count; i++) - { - rawTilemapTiles[i] = reader.ReadRawTilemapTile(); - } - - return rawTilemapTiles; - } - - internal static RawTilemapTile ReadRawTilemapTile(this BinaryReader reader) - { - int tilesetTileID = reader.ReadInt32(); - bool flipHorizontally = reader.ReadBoolean(); - bool flipVertically = reader.ReadBoolean(); - float rotation = reader.ReadSingle(); - - return new(tilesetTileID, flipHorizontally, flipVertically, rotation); - } - - internal static RawTilemapFrame[] ReadRawTilemapFrames(this BinaryReader reader) - { - int count = reader.ReadInt32(); - RawTilemapFrame[] rawTilemapFrames = new RawTilemapFrame[count]; - - for (int i = 0; i < count; i++) - { - rawTilemapFrames[i] = reader.ReadRawTilemapFrame(); - } - - return rawTilemapFrames; - } - - internal static RawTilemapFrame ReadRawTilemapFrame(this BinaryReader reader) - { - int duration = reader.ReadInt32(); - RawTilemapLayer[] layers = reader.ReadRawTilemapLayers(); - return new(duration, layers); - } -} +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +using Microsoft.Xna.Framework; +using MonoGame.Aseprite.RawTypes; + +namespace MonoGame.Aseprite.Content; + +internal static class BinaryReaderExtensions +{ + internal static void ReadMagic(this BinaryReader reader) + { + byte m = reader.ReadByte(); // [M]onoGame + byte a = reader.ReadByte(); // [A]seprite + byte c = reader.ReadByte(); // [C]ontent + + + if (m != 'M' || a != 'A' || c != 'C') + { + // Dispose of reader so stream is closed in case the caller has a try-catch that handles this exception. + reader.Dispose(); + throw new InvalidOperationException($"File contains invalid magic number. Was this processed using the MonoGame.Aseprite library?"); + } + } + + internal static Rectangle ReadRectangle(this BinaryReader reader) + { + Rectangle result = new(); + result.Location = ReadPoint(reader); + result.Size = ReadPoint(reader); + return result; + } + + internal static Point ReadPoint(this BinaryReader reader) + { + Point result = new(); + result.X = reader.ReadInt32(); + result.Y = reader.ReadInt32(); + return result; + } + + internal static Vector2 ReadVector2(this BinaryReader reader) + { + Vector2 result = new(); + result.X = reader.ReadSingle(); + result.Y = reader.ReadSingle(); + return result; + } + + internal static Color ReadColor(this BinaryReader reader) + { + Color result = new(); + result.R = reader.ReadByte(); + result.G = reader.ReadByte(); + result.B = reader.ReadByte(); + result.A = reader.ReadByte(); + return result; + } + + internal static RawTexture ReadRawTexture(this BinaryReader reader) + { + string name = reader.ReadString(); + int width = reader.ReadInt32(); + int height = reader.ReadInt32(); + + Color[] pixels = reader.ReadColors(); + + return new(name, pixels, width, height); + } + + internal static Color[] ReadColors(this BinaryReader reader) + { + int count = reader.ReadInt32(); + Color[] pixels = new Color[count]; + for (int i = 0; i < count; i++) + { + pixels[i] = reader.ReadColor(); + } + return pixels; + } + + internal static RawSlice ReadRawSlice(this BinaryReader reader) + { + string name = reader.ReadString(); + Rectangle bounds = reader.ReadRectangle(); + Vector2 origin = reader.ReadVector2(); + Color color = reader.ReadColor(); + + bool isNine = reader.ReadBoolean(); + + if (isNine) + { + Rectangle centerBounds = reader.ReadRectangle(); + return new RawNinePatchSlice(name, bounds, centerBounds, origin, color); + } + + return new RawSlice(name, bounds, origin, color); + } + + internal static RawAnimationTag[] ReadRawAnimationTags(this BinaryReader reader) + { + int count = reader.ReadInt32(); + RawAnimationTag[] tags = new RawAnimationTag[count]; + for (int i = 0; i < count; i++) + { + tags[i] = reader.ReadRawAnimationTag(); + } + return tags; + } + + internal static RawAnimationTag ReadRawAnimationTag(this BinaryReader reader) + { + string name = reader.ReadString(); + int loopCount = reader.ReadInt32(); + bool isReversed = reader.ReadBoolean(); + bool isPingPong = reader.ReadBoolean(); + RawAnimationFrame[] frames = reader.ReadRawAnimationFrames(); + return new(name, frames, loopCount, isReversed, isPingPong); + } + + internal static RawAnimationFrame[] ReadRawAnimationFrames(this BinaryReader reader) + { + int count = reader.ReadInt32(); + RawAnimationFrame[] frames = new RawAnimationFrame[count]; + for (int i = 0; i < count; i++) + { + frames[i] = reader.ReadRawAnimationFrame(); + } + return frames; + } + + internal static RawAnimationFrame ReadRawAnimationFrame(this BinaryReader reader) + { + int index = reader.ReadInt32(); + int duration = reader.ReadInt32(); + return new(index, duration); + } + + internal static RawTextureAtlas ReadRawTextureAtlas(this BinaryReader reader) + { + string name = reader.ReadString(); + RawTexture texture = reader.ReadRawTexture(); + RawTextureRegion[] regions = reader.ReadRawTextureRegions(); + return new(name, texture, regions); + } + + internal static RawTextureRegion[] ReadRawTextureRegions(this BinaryReader reader) + { + int count = reader.ReadInt32(); + RawTextureRegion[] regions = new RawTextureRegion[count]; + + for (int i = 0; i < count; i++) + { + regions[i] = reader.ReadRawTextureRegion(); + } + + return regions; + } + + internal static RawTextureRegion ReadRawTextureRegion(this BinaryReader reader) + { + string name = reader.ReadString(); + Rectangle bounds = reader.ReadRectangle(); + RawSlice[] slices = reader.ReadRawSlices(); + return new(name, bounds, slices); + } + + internal static RawSlice[] ReadRawSlices(this BinaryReader reader) + { + int count = reader.ReadInt32(); + RawSlice[] slices = new RawSlice[count]; + + for (int i = 0; i < count; i++) + { + slices[i] = reader.ReadRawSlice(); + } + + return slices; + } + + internal static RawTileset ReadRawTileset(this BinaryReader reader) + { + int id = reader.ReadInt32(); + string name = reader.ReadString(); + RawTexture texture = reader.ReadRawTexture(); + int tileWidth = reader.ReadInt32(); + int tileHeight = reader.ReadInt32(); + + return new(id, name, texture, tileWidth, tileHeight); + } + + internal static RawTilemapLayer[] ReadRawTilemapLayers(this BinaryReader reader) + { + int count = reader.ReadInt32(); + RawTilemapLayer[] layers = new RawTilemapLayer[count]; + for (int i = 0; i < count; i++) + { + layers[i] = reader.ReadRawTilemapLayer(); + } + return layers; + } + + internal static RawTilemapLayer ReadRawTilemapLayer(this BinaryReader reader) + { + string name = reader.ReadString(); + int tilesetID = reader.ReadInt32(); + int columns = reader.ReadInt32(); + int rows = reader.ReadInt32(); + Point offset = reader.ReadPoint(); + RawTilemapTile[] tiles = reader.ReadRawTilemapTiles(); + return new(name, tilesetID, columns, rows, tiles, offset); + } + + internal static RawTilemapTile[] ReadRawTilemapTiles(this BinaryReader reader) + { + int count = reader.ReadInt32(); + RawTilemapTile[] rawTilemapTiles = new RawTilemapTile[count]; + + for (int i = 0; i < count; i++) + { + rawTilemapTiles[i] = reader.ReadRawTilemapTile(); + } + + return rawTilemapTiles; + } + + internal static RawTilemapTile ReadRawTilemapTile(this BinaryReader reader) + { + int tilesetTileID = reader.ReadInt32(); + bool flipHorizontally = reader.ReadBoolean(); + bool flipVertically = reader.ReadBoolean(); + bool flipDiagonally = reader.ReadBoolean(); + + return new RawTilemapTile(tilesetTileID, flipHorizontally, flipVertically, flipDiagonally); + } + + internal static RawTilemapFrame[] ReadRawTilemapFrames(this BinaryReader reader) + { + int count = reader.ReadInt32(); + RawTilemapFrame[] rawTilemapFrames = new RawTilemapFrame[count]; + + for (int i = 0; i < count; i++) + { + rawTilemapFrames[i] = reader.ReadRawTilemapFrame(); + } + + return rawTilemapFrames; + } + + internal static RawTilemapFrame ReadRawTilemapFrame(this BinaryReader reader) + { + int duration = reader.ReadInt32(); + RawTilemapLayer[] layers = reader.ReadRawTilemapLayers(); + return new(duration, layers); + } +} diff --git a/source/MonoGame.Aseprite.Shared/Content/BinaryWriterExtensions.cs b/source/MonoGame.Aseprite.Shared/Content/BinaryWriterExtensions.cs index 75adbad..74d3878 100644 --- a/source/MonoGame.Aseprite.Shared/Content/BinaryWriterExtensions.cs +++ b/source/MonoGame.Aseprite.Shared/Content/BinaryWriterExtensions.cs @@ -1,224 +1,224 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -using Microsoft.Xna.Framework; -using MonoGame.Aseprite.RawTypes; - -namespace MonoGame.Aseprite.Content; - -internal static class BinaryWriterExtensions -{ - internal static void WriteMagic(this BinaryWriter writer) - { - writer.Write((byte)'M'); // [M]onoGame - writer.Write((byte)'A'); // [A]seprite - writer.Write((byte)'C'); // [C]ontent - } - - internal static void Write(this BinaryWriter writer, Rectangle value) - { - writer.Write(value.Location); - writer.Write(value.Size); - } - - internal static void Write(this BinaryWriter writer, Point value) - { - writer.Write(value.X); - writer.Write(value.Y); - } - - internal static void Write(this BinaryWriter writer, Vector2 value) - { - writer.Write(value.X); - writer.Write(value.Y); - } - - internal static void Write(this BinaryWriter writer, Color value) - { - writer.Write(value.R); - writer.Write(value.G); - writer.Write(value.B); - writer.Write(value.A); - } - - internal static void Write(this BinaryWriter writer, RawTexture value) - { - writer.Write(value.Name); - writer.Write(value.Width); - writer.Write(value.Height); - writer.Write(value.Pixels); - } - - internal static void Write(this BinaryWriter writer, ReadOnlySpan value) - { - writer.Write(value.Length); - for (int i = 0; i < value.Length; i++) - { - writer.Write(value[i]); - } - } - - internal static void Write(this BinaryWriter writer, RawSlice value) - { - writer.Write(value.Name); - writer.Write(value.Bounds); - writer.Write(value.Origin); - writer.Write(value.Color); - - if (value is RawNinePatchSlice ninePatch) - { - writer.Write(true); - writer.Write(ninePatch.CenterBounds); - } - else - { - writer.Write(false); - } - } - - internal static void Write(this BinaryWriter writer, RawTextureAtlas value) - { - writer.Write(value.Name); - writer.Write(value.RawTexture); - writer.Write(value.RawTextureRegions); - } - - internal static void Write(this BinaryWriter writer, ReadOnlySpan value) - { - writer.Write(value.Length); - for (int i = 0; i < value.Length; i++) - { - writer.Write(value[i]); - } - } - - internal static void Write(this BinaryWriter writer, RawTextureRegion value) - { - writer.Write(value.Name); - writer.Write(value.Bounds); - writer.Write(value.Slices); - } - - internal static void Write(this BinaryWriter writer, ReadOnlySpan value) - { - writer.Write(value.Length); - for (int i = 0; i < value.Length; i++) - { - writer.Write(value[i]); - } - } - - internal static void Write(this BinaryWriter writer, ReadOnlySpan value) - { - writer.Write(value.Length); - for (int i = 0; i < value.Length; i++) - { - writer.Write(value[i]); - } - } - - internal static void Write(this BinaryWriter writer, RawAnimationTag value) - { - writer.Write(value.Name); - writer.Write(value.LoopCount); - writer.Write(value.IsReversed); - writer.Write(value.IsPingPong); - writer.Write(value.RawAnimationFrames); - } - - internal static void Write(this BinaryWriter writer, ReadOnlySpan value) - { - writer.Write(value.Length); - for (int i = 0; i < value.Length; i++) - { - writer.Write(value[i]); - } - } - - internal static void Write(this BinaryWriter writer, RawAnimationFrame value) - { - writer.Write(value.FrameIndex); - writer.Write(value.DurationInMilliseconds); - } - - internal static void Write(this BinaryWriter writer, RawTileset value) - { - writer.Write(value.ID); - writer.Write(value.Name); - writer.Write(value.RawTexture); - writer.Write(value.TileWidth); - writer.Write(value.TileHeight); - } - - internal static void Write(this BinaryWriter writer, ReadOnlySpan value) - { - writer.Write(value.Length); - for (int i = 0; i < value.Length; i++) - { - writer.Write(value[i]); - } - } - - internal static void Write(this BinaryWriter writer, RawTilemapLayer value) - { - writer.Write(value.Name); - writer.Write(value.TilesetID); - writer.Write(value.Columns); - writer.Write(value.Rows); - writer.Write(value.Offset); - writer.Write(value.RawTilemapTiles); - } - - internal static void Write(this BinaryWriter writer, ReadOnlySpan value) - { - writer.Write(value.Length); - for (int i = 0; i < value.Length; i++) - { - writer.Write(value[i]); - } - } - - internal static void Write(this BinaryWriter writer, RawTilemapTile value) - { - writer.Write(value.TilesetTileID); - writer.Write(value.FlipHorizontally); - writer.Write(value.FlipVertically); - writer.Write(value.Rotation); - } - - internal static void Write(this BinaryWriter writer, ReadOnlySpan value) - { - writer.Write(value.Length); - for (int i = 0; i < value.Length; i++) - { - writer.Write(value[i]); - } - } - - internal static void Write(this BinaryWriter writer, RawTilemapFrame value) - { - writer.Write(value.DurationInMilliseconds); - writer.Write(value.RawTilemapLayers); - } -} +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +using Microsoft.Xna.Framework; +using MonoGame.Aseprite.RawTypes; + +namespace MonoGame.Aseprite.Content; + +internal static class BinaryWriterExtensions +{ + internal static void WriteMagic(this BinaryWriter writer) + { + writer.Write((byte)'M'); // [M]onoGame + writer.Write((byte)'A'); // [A]seprite + writer.Write((byte)'C'); // [C]ontent + } + + internal static void Write(this BinaryWriter writer, Rectangle value) + { + writer.Write(value.Location); + writer.Write(value.Size); + } + + internal static void Write(this BinaryWriter writer, Point value) + { + writer.Write(value.X); + writer.Write(value.Y); + } + + internal static void Write(this BinaryWriter writer, Vector2 value) + { + writer.Write(value.X); + writer.Write(value.Y); + } + + internal static void Write(this BinaryWriter writer, Color value) + { + writer.Write(value.R); + writer.Write(value.G); + writer.Write(value.B); + writer.Write(value.A); + } + + internal static void Write(this BinaryWriter writer, RawTexture value) + { + writer.Write(value.Name); + writer.Write(value.Width); + writer.Write(value.Height); + writer.Write(value.Pixels); + } + + internal static void Write(this BinaryWriter writer, ReadOnlySpan value) + { + writer.Write(value.Length); + for (int i = 0; i < value.Length; i++) + { + writer.Write(value[i]); + } + } + + internal static void Write(this BinaryWriter writer, RawSlice value) + { + writer.Write(value.Name); + writer.Write(value.Bounds); + writer.Write(value.Origin); + writer.Write(value.Color); + + if (value is RawNinePatchSlice ninePatch) + { + writer.Write(true); + writer.Write(ninePatch.CenterBounds); + } + else + { + writer.Write(false); + } + } + + internal static void Write(this BinaryWriter writer, RawTextureAtlas value) + { + writer.Write(value.Name); + writer.Write(value.RawTexture); + writer.Write(value.RawTextureRegions); + } + + internal static void Write(this BinaryWriter writer, ReadOnlySpan value) + { + writer.Write(value.Length); + for (int i = 0; i < value.Length; i++) + { + writer.Write(value[i]); + } + } + + internal static void Write(this BinaryWriter writer, RawTextureRegion value) + { + writer.Write(value.Name); + writer.Write(value.Bounds); + writer.Write(value.Slices); + } + + internal static void Write(this BinaryWriter writer, ReadOnlySpan value) + { + writer.Write(value.Length); + for (int i = 0; i < value.Length; i++) + { + writer.Write(value[i]); + } + } + + internal static void Write(this BinaryWriter writer, ReadOnlySpan value) + { + writer.Write(value.Length); + for (int i = 0; i < value.Length; i++) + { + writer.Write(value[i]); + } + } + + internal static void Write(this BinaryWriter writer, RawAnimationTag value) + { + writer.Write(value.Name); + writer.Write(value.LoopCount); + writer.Write(value.IsReversed); + writer.Write(value.IsPingPong); + writer.Write(value.RawAnimationFrames); + } + + internal static void Write(this BinaryWriter writer, ReadOnlySpan value) + { + writer.Write(value.Length); + for (int i = 0; i < value.Length; i++) + { + writer.Write(value[i]); + } + } + + internal static void Write(this BinaryWriter writer, RawAnimationFrame value) + { + writer.Write(value.FrameIndex); + writer.Write(value.DurationInMilliseconds); + } + + internal static void Write(this BinaryWriter writer, RawTileset value) + { + writer.Write(value.ID); + writer.Write(value.Name); + writer.Write(value.RawTexture); + writer.Write(value.TileWidth); + writer.Write(value.TileHeight); + } + + internal static void Write(this BinaryWriter writer, ReadOnlySpan value) + { + writer.Write(value.Length); + for (int i = 0; i < value.Length; i++) + { + writer.Write(value[i]); + } + } + + internal static void Write(this BinaryWriter writer, RawTilemapLayer value) + { + writer.Write(value.Name); + writer.Write(value.TilesetID); + writer.Write(value.Columns); + writer.Write(value.Rows); + writer.Write(value.Offset); + writer.Write(value.RawTilemapTiles); + } + + internal static void Write(this BinaryWriter writer, ReadOnlySpan value) + { + writer.Write(value.Length); + for (int i = 0; i < value.Length; i++) + { + writer.Write(value[i]); + } + } + + internal static void Write(this BinaryWriter writer, RawTilemapTile value) + { + writer.Write(value.TilesetTileID); + writer.Write(value.FlipHorizontally); + writer.Write(value.FlipVertically); + writer.Write(value.FlipDiagonally); + } + + internal static void Write(this BinaryWriter writer, ReadOnlySpan value) + { + writer.Write(value.Length); + for (int i = 0; i < value.Length; i++) + { + writer.Write(value[i]); + } + } + + internal static void Write(this BinaryWriter writer, RawTilemapFrame value) + { + writer.Write(value.DurationInMilliseconds); + writer.Write(value.RawTilemapLayers); + } +} diff --git a/source/MonoGame.Aseprite.Shared/Content/Processors/AnimatedTilemapProcessor.cs b/source/MonoGame.Aseprite.Shared/Content/Processors/AnimatedTilemapProcessor.cs index f0019e2..f9f96a9 100644 --- a/source/MonoGame.Aseprite.Shared/Content/Processors/AnimatedTilemapProcessor.cs +++ b/source/MonoGame.Aseprite.Shared/Content/Processors/AnimatedTilemapProcessor.cs @@ -1,121 +1,118 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -using Microsoft.Xna.Framework; -using MonoGame.Aseprite.AsepriteTypes; -using MonoGame.Aseprite.RawTypes; - -namespace MonoGame.Aseprite.Content.Processors; - -/// -public static partial class AnimatedTilemapProcessor -{ - /// - /// Processes a from the given . - /// - /// - /// The to processes the from. - /// - /// - /// Indicates if only elements that are visible should be processed. - /// - /// - /// The created by this method. - /// - /// - /// Thrown if elements are found in the with duplicate - /// names. Tilemaps must contain layers with unique names even though Aseprite does not enforce unique names - /// for layers. - /// - public static RawAnimatedTilemap ProcessRaw(AsepriteFile aseFile, bool onlyVisibleLayers = true) - { - List rawTilesets = new(); - RawTilemapFrame[] rawFrames = new RawTilemapFrame[aseFile.Frames.Length]; - HashSet tilesetIDCheck = new(); - - for (int f = 0; f < aseFile.Frames.Length; f++) - { - AsepriteFrame aseFrame = aseFile.Frames[f]; - List rawLayers = new(); - HashSet layerNameCheck = new(); - - for (int c = 0; c < aseFrame.Cels.Length; c++) - { - // Only care about tilemap cels. - if (aseFrame.Cels[c] is not AsepriteTilemapCel tilemapCel) - { - continue; - } - - // Only continue if layer is visible or if explicitly told to include non-visible layers. - if (!tilemapCel.Layer.IsVisible && onlyVisibleLayers) - { - continue; - } - - // Need to perform a check that we don't have duplicate layer names. This is because aseprite allows - // duplicate layer names, but we require unique names from this point on. - AsepriteTilemapLayer aseTilemapLayer = tilemapCel.LayerAs(); - string layerName = aseTilemapLayer.Name; - - if (!layerNameCheck.Add(layerName)) - { - throw new InvalidOperationException($"Duplicate layer name '{tilemapCel.Layer.Name}' found. Layer names must be unique for tilemaps"); - } - - int tilesetID = aseTilemapLayer.Tileset.ID; - - if (tilesetIDCheck.Add(tilesetID)) - { - RawTileset rawTileset = TilesetProcessor.ProcessRaw(aseTilemapLayer.Tileset); - rawTilesets.Add(rawTileset); - } - - RawTilemapTile[] tiles = new RawTilemapTile[tilemapCel.Tiles.Length]; - - for (int t = 0; t < tilemapCel.Tiles.Length; t++) - { - AsepriteTile aseTile = tilemapCel.Tiles[t]; - bool flipHorizontally = aseTile.XFlip != 0; - bool flipVertically = aseTile.YFlip != 0; - - tiles[t] = new(aseTile.TilesetTileID, flipHorizontally, flipVertically, aseTile.Rotation); - } - - int columns = tilemapCel.Columns; - int rows = tilemapCel.Rows; - Point offset = tilemapCel.Position; - - RawTilemapLayer rawLayer = new(layerName, tilesetID, columns, rows, tiles, offset); - rawLayers.Add(rawLayer); - - } - - rawFrames[f] = new(aseFrame.DurationInMilliseconds, rawLayers.ToArray()); - } - - return new(aseFile.Name, rawTilesets.ToArray(), rawFrames); - - } -} +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +using Microsoft.Xna.Framework; +using MonoGame.Aseprite.AsepriteTypes; +using MonoGame.Aseprite.RawTypes; + +namespace MonoGame.Aseprite.Content.Processors; + +/// +public static partial class AnimatedTilemapProcessor +{ + /// + /// Processes a from the given . + /// + /// + /// The to processes the from. + /// + /// + /// Indicates if only elements that are visible should be processed. + /// + /// + /// The created by this method. + /// + /// + /// Thrown if elements are found in the with duplicate + /// names. Tilemaps must contain layers with unique names even though Aseprite does not enforce unique names + /// for layers. + /// + public static RawAnimatedTilemap ProcessRaw(AsepriteFile aseFile, bool onlyVisibleLayers = true) + { + List rawTilesets = new(); + RawTilemapFrame[] rawFrames = new RawTilemapFrame[aseFile.Frames.Length]; + HashSet tilesetIDCheck = new(); + + for (int f = 0; f < aseFile.Frames.Length; f++) + { + AsepriteFrame aseFrame = aseFile.Frames[f]; + List rawLayers = new(); + HashSet layerNameCheck = new(); + + for (int c = 0; c < aseFrame.Cels.Length; c++) + { + // Only care about tilemap cels. + if (aseFrame.Cels[c] is not AsepriteTilemapCel tilemapCel) + { + continue; + } + + // Only continue if layer is visible or if explicitly told to include non-visible layers. + if (!tilemapCel.Layer.IsVisible && onlyVisibleLayers) + { + continue; + } + + // Need to perform a check that we don't have duplicate layer names. This is because aseprite allows + // duplicate layer names, but we require unique names from this point on. + AsepriteTilemapLayer aseTilemapLayer = tilemapCel.LayerAs(); + string layerName = aseTilemapLayer.Name; + + if (!layerNameCheck.Add(layerName)) + { + throw new InvalidOperationException($"Duplicate layer name '{tilemapCel.Layer.Name}' found. Layer names must be unique for tilemaps"); + } + + int tilesetID = aseTilemapLayer.Tileset.ID; + + if (tilesetIDCheck.Add(tilesetID)) + { + RawTileset rawTileset = TilesetProcessor.ProcessRaw(aseTilemapLayer.Tileset); + rawTilesets.Add(rawTileset); + } + + RawTilemapTile[] tiles = new RawTilemapTile[tilemapCel.Tiles.Length]; + + for (int t = 0; t < tilemapCel.Tiles.Length; t++) + { + AsepriteTile aseTile = tilemapCel.Tiles[t]; + tiles[t] = new RawTilemapTile(aseTile.TilesetTileID, aseTile.YFlip, aseTile.XFlip, aseTile.DFlip); + } + + int columns = tilemapCel.Columns; + int rows = tilemapCel.Rows; + Point offset = tilemapCel.Position; + + RawTilemapLayer rawLayer = new(layerName, tilesetID, columns, rows, tiles, offset); + rawLayers.Add(rawLayer); + + } + + rawFrames[f] = new(aseFrame.DurationInMilliseconds, rawLayers.ToArray()); + } + + return new(aseFile.Name, rawTilesets.ToArray(), rawFrames); + + } +} diff --git a/source/MonoGame.Aseprite.Shared/Content/Processors/TilemapProcessor.cs b/source/MonoGame.Aseprite.Shared/Content/Processors/TilemapProcessor.cs index b9ef7c0..3f4216a 100644 --- a/source/MonoGame.Aseprite.Shared/Content/Processors/TilemapProcessor.cs +++ b/source/MonoGame.Aseprite.Shared/Content/Processors/TilemapProcessor.cs @@ -1,121 +1,118 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -using Microsoft.Xna.Framework; -using MonoGame.Aseprite.AsepriteTypes; -using MonoGame.Aseprite.RawTypes; - -namespace MonoGame.Aseprite.Content.Processors; - -/// -public static partial class TilemapProcessor -{ - /// - /// Processes a from the elements as the specified index - /// in the given . - /// - /// - /// The to processes the from. - /// - /// - /// The index of the element in the to processes. - /// - /// - /// Indicates if only elements that are visible should be processed. - /// - /// - /// The created by this method. - /// - /// - /// Thrown if the index specified is less than zero or is greater than or equal to the total number of - /// elements in the given . - /// - /// - /// Thrown if elements are found in the with duplicate - /// names. Tilemaps must contain layers with unique names even though aseprite does not enforce unique names - /// for elements. - /// - public static RawTilemap ProcessRaw(AsepriteFile aseFile, int frameIndex, bool onlyVisibleLayers = true) - { - AsepriteFrame aseFrame = aseFile.GetFrame(frameIndex); - - List rawTilesets = new(); - List rawLayers = new(); - HashSet layerNameCheck = new(); - HashSet tilesetIDCheck = new(); - - for (int c = 0; c < aseFrame.Cels.Length; c++) - { - // Only care about tilemap cels. - if (aseFrame.Cels[c] is not AsepriteTilemapCel tilemapCel) - { - continue; - } - - // Only continue if layer is visible or if explicitly told to include non-visible elements. - if (!tilemapCel.Layer.IsVisible && onlyVisibleLayers) - { - continue; - } - - // Need to perform a check that we don't have duplicate layer names. This is because aseprite allows - // duplicate layer names, but we require unique names from this point on. - AsepriteTilemapLayer aseTilemapLayer = tilemapCel.LayerAs(); - string layerName = aseTilemapLayer.Name; - - if (!layerNameCheck.Add(layerName)) - { - throw new InvalidOperationException($"Duplicate layer name '{tilemapCel.Layer.Name}' found. Layer names must be unique for tilemaps"); - } - - int tilesetID = aseTilemapLayer.Tileset.ID; - - if (tilesetIDCheck.Add(tilesetID)) - { - RawTileset rawTileset = TilesetProcessor.ProcessRaw(aseTilemapLayer.Tileset); - rawTilesets.Add(rawTileset); - } - - RawTilemapTile[] tiles = new RawTilemapTile[tilemapCel.Tiles.Length]; - - for (int t = 0; t < tilemapCel.Tiles.Length; t++) - { - AsepriteTile aseTile = tilemapCel.Tiles[t]; - bool flipHorizontally = aseTile.XFlip != 0; - bool flipVertically = aseTile.YFlip != 0; - - tiles[t] = new(aseTile.TilesetTileID, flipHorizontally, flipVertically, aseTile.Rotation); - } - - int columns = tilemapCel.Columns; - int rows = tilemapCel.Rows; - Point offset = tilemapCel.Position; - - RawTilemapLayer rawLayer = new(layerName, tilesetID, columns, rows, tiles, offset); - rawLayers.Add(rawLayer); - } - - return new(aseFile.Name, rawLayers.ToArray(), rawTilesets.ToArray()); - } -} +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +using Microsoft.Xna.Framework; +using MonoGame.Aseprite.AsepriteTypes; +using MonoGame.Aseprite.RawTypes; + +namespace MonoGame.Aseprite.Content.Processors; + +/// +public static partial class TilemapProcessor +{ + /// + /// Processes a from the elements as the specified index + /// in the given . + /// + /// + /// The to processes the from. + /// + /// + /// The index of the element in the to processes. + /// + /// + /// Indicates if only elements that are visible should be processed. + /// + /// + /// The created by this method. + /// + /// + /// Thrown if the index specified is less than zero or is greater than or equal to the total number of + /// elements in the given . + /// + /// + /// Thrown if elements are found in the with duplicate + /// names. Tilemaps must contain layers with unique names even though aseprite does not enforce unique names + /// for elements. + /// + public static RawTilemap ProcessRaw(AsepriteFile aseFile, int frameIndex, bool onlyVisibleLayers = true) + { + AsepriteFrame aseFrame = aseFile.GetFrame(frameIndex); + + List rawTilesets = new(); + List rawLayers = new(); + HashSet layerNameCheck = new(); + HashSet tilesetIDCheck = new(); + + for (int c = 0; c < aseFrame.Cels.Length; c++) + { + // Only care about tilemap cels. + if (aseFrame.Cels[c] is not AsepriteTilemapCel tilemapCel) + { + continue; + } + + // Only continue if layer is visible or if explicitly told to include non-visible elements. + if (!tilemapCel.Layer.IsVisible && onlyVisibleLayers) + { + continue; + } + + // Need to perform a check that we don't have duplicate layer names. This is because aseprite allows + // duplicate layer names, but we require unique names from this point on. + AsepriteTilemapLayer aseTilemapLayer = tilemapCel.LayerAs(); + string layerName = aseTilemapLayer.Name; + + if (!layerNameCheck.Add(layerName)) + { + throw new InvalidOperationException($"Duplicate layer name '{tilemapCel.Layer.Name}' found. Layer names must be unique for tilemaps"); + } + + int tilesetID = aseTilemapLayer.Tileset.ID; + + if (tilesetIDCheck.Add(tilesetID)) + { + RawTileset rawTileset = TilesetProcessor.ProcessRaw(aseTilemapLayer.Tileset); + rawTilesets.Add(rawTileset); + } + + RawTilemapTile[] tiles = new RawTilemapTile[tilemapCel.Tiles.Length]; + + for (int t = 0; t < tilemapCel.Tiles.Length; t++) + { + AsepriteTile aseTile = tilemapCel.Tiles[t]; + tiles[t] = new RawTilemapTile(aseTile.TilesetTileID, aseTile.YFlip, aseTile.XFlip, aseTile.DFlip); + } + + int columns = tilemapCel.Columns; + int rows = tilemapCel.Rows; + Point offset = tilemapCel.Position; + + RawTilemapLayer rawLayer = new(layerName, tilesetID, columns, rows, tiles, offset); + rawLayers.Add(rawLayer); + } + + return new(aseFile.Name, rawLayers.ToArray(), rawTilesets.ToArray()); + } +} diff --git a/source/MonoGame.Aseprite.Shared/Content/Readers/AsepriteFileReader.cs b/source/MonoGame.Aseprite.Shared/Content/Readers/AsepriteFileReader.cs index a9cbe21..460de24 100644 --- a/source/MonoGame.Aseprite.Shared/Content/Readers/AsepriteFileReader.cs +++ b/source/MonoGame.Aseprite.Shared/Content/Readers/AsepriteFileReader.cs @@ -1,565 +1,598 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Microsoft.Xna.Framework; -using MonoGame.Aseprite.AsepriteTypes; - -namespace MonoGame.Aseprite.Content.Readers; - -/// -/// Defines a reader that reads an . -/// -public static class AsepriteFileReader -{ - const ushort CHUNK_TYPE_OLD_PALETTE_1 = 0x0004; - const ushort CHUNK_TYPE_OLD_PALETTE_2 = 0x0011; - const ushort CHUNK_TYPE_LAYER = 0x2004; - const ushort CHUNK_TYPE_CEL = 0x2005; - const ushort CHUNK_TYPE_CEL_EXTRA = 0x2006; - const ushort CHUNK_TYPE_COLOR_PROFILE = 0x2007; - const ushort CHUNK_TYPE_EXTERNAL_FILES = 0x2008; - const ushort CHUNK_TYPE_MASK = 0x2016; - const ushort CHUNK_TYPE_PATH = 0x2017; - const ushort CHUNK_TYPE_TAGS = 0x2018; - const ushort CHUNK_TYPE_PALETTE = 0x2019; - const ushort CHUNK_TYPE_USER_DATA = 0x2020; - const ushort CHUNK_TYPE_SLICE = 0x2022; - const ushort CHUNK_TYPE_TILESET = 0x2023; - - /// - /// Reads the at the given path. - /// - /// - /// The path and name of the aseprite file to read. - /// - /// - /// The created by this method. - /// - /// - /// Thrown if no file is located at the specified path. - /// - /// - /// Thrown if an error occurs during the reading of the aseprite file. The exception message will contain the - /// cause of the exception. - /// - public static AsepriteFile ReadFile(string path) - { - if (!File.Exists(path)) - { - throw new FileNotFoundException($"Unable to locate a file at the path '{path}'"); - } - - using Stream stream = File.OpenRead(path); - using BinaryReader reader = new(stream); - - string name = Path.GetFileNameWithoutExtension(path); - return Read(name, reader); - } - - /// - /// Reads the using the provided . - ///
- /// Use this method with to load raw .aseprite files on Android - /// or other platforms. - ///
- /// - /// The name of the Aseprite file. - /// - /// - /// A file stream, preferably instantiated from calling . - /// - /// - /// The created by this method. - /// - /// - /// Thrown if no file is located at the specified path. - /// - /// - /// Thrown if an error occurs during the reading of the aseprite file. The exception message will contain the - /// cause of the exception. - /// - public static AsepriteFile ReadStream(string name, [NotNull] Stream fileStream) - { - using BinaryReader reader = new(fileStream); - return Read(name, reader); - } - - internal static AsepriteFile Read(string name, BinaryReader reader) - { - AsepriteFileBuilder fileBuilder = new(name); - - ReadFileHeader(reader, fileBuilder, out ushort nFrames); - ReadFrames(reader, fileBuilder, nFrames); - - AsepriteFile file = fileBuilder.Build(); - - return file; - } - - private static void ReadFileHeader(BinaryReader reader, AsepriteFileBuilder builder, out ushort nFrames) - { - const uint LAYER_OPACITY_VALID_FLAG = 1; - - IgnoreDword(reader); - ushort magic = ReadWord(reader); - - if (magic != 0xA5E0) - { - throw new InvalidOperationException($"Invalid header magic number (0x{magic:X4})."); - } - - nFrames = ReadWord(reader); - ushort frameWidth = ReadWord(reader); - ushort frameHeight = ReadWord(reader); - - ushort colorDepth = ReadWord(reader); - uint flags = ReadDword(reader); - - bool layerOpacityValid = (flags & LAYER_OPACITY_VALID_FLAG) != 0; - - IgnoreWord(reader); - IgnoreDword(reader); - IgnoreDword(reader); - - byte transparentIndex = ReadByte(reader); - IgnoreBytes(reader, 3); - ushort nColors = ReadWord(reader); - - reader.BaseStream.Position = 128; - - builder.SetFrameWidth(frameWidth); - builder.SetFrameHeight(frameHeight); - builder.SetColorDepth(colorDepth); - builder.SetTransparentIndex(transparentIndex); - builder.SetLayerOpacityValid(layerOpacityValid); - } - - private static void ReadFrames(BinaryReader reader, AsepriteFileBuilder builder, ushort nFrames) - { - for (int frameNum = 0; frameNum < nFrames; frameNum++) - { - IgnoreDword(reader); - ushort magic = ReadWord(reader); - - if (magic != 0xF1FA) - { - throw new InvalidOperationException($"Invalid frame magic in frame number {frameNum}"); - } - - ushort nChunksA = ReadWord(reader); - ushort duration = ReadWord(reader); - IgnoreBytes(reader, 2); - uint nChunksB = ReadDword(reader); - - uint nChunks = nChunksA == 0xFFFF && nChunksA < nChunksB ? - nChunksB : - nChunksA; - - - // Start iterator at -1 so after tags chunk is read, it'll - // increment to 0 to kick off the user data read. - int tagIterator = -1; - ushort lastChunkType = default; - - List cels = new(); - - for (uint chunkNum = 0; chunkNum < nChunks; chunkNum++) - { - lastChunkType = ReadChunk(reader, builder, lastChunkType, tagIterator, cels); - if (lastChunkType == CHUNK_TYPE_TAGS) - { - tagIterator++; - } - } - - builder.AddFrame(duration); - } - - } - - private static ushort ReadChunk(BinaryReader reader, AsepriteFileBuilder builder, ushort lastChunkType, int tagIterator, List cels) - { - long start = reader.BaseStream.Position; - - uint len = ReadDword(reader); - ushort type = ReadWord(reader); - - long end = start + len; - - switch (type) - { - case CHUNK_TYPE_LAYER: - ReadLayerChunk(reader, builder); - break; - case CHUNK_TYPE_CEL: - ReadCelChunk(reader, builder, end); - break; - case CHUNK_TYPE_TAGS: - ReadTagsChunk(reader, builder); - break; - case CHUNK_TYPE_PALETTE: - ReadPaletteChunk(reader, builder); - break; - case CHUNK_TYPE_SLICE: - ReadSliceChunk(reader, builder); - break; - case CHUNK_TYPE_TILESET: - ReadTilesetChunk(reader, builder); - break; - case CHUNK_TYPE_USER_DATA: - ReadUserDataChunk(reader, builder, lastChunkType, tagIterator); - break; - case CHUNK_TYPE_OLD_PALETTE_1: - case CHUNK_TYPE_OLD_PALETTE_2: - case CHUNK_TYPE_CEL_EXTRA: - case CHUNK_TYPE_COLOR_PROFILE: - case CHUNK_TYPE_EXTERNAL_FILES: - case CHUNK_TYPE_MASK: - case CHUNK_TYPE_PATH: - reader.BaseStream.Position = end; - break; - default: - throw new InvalidOperationException($"Unknown chunk type (0x{type:X4})"); - } - - if (type == CHUNK_TYPE_USER_DATA) - { - return lastChunkType; - } - - return type; - } - - private static void ReadLayerChunk(BinaryReader reader, AsepriteFileBuilder builder) - { - const int NORMAL_LAYER_TYPE = 0; - const int GROUP_LAYER_TYPE = 1; - const int TILEMAP_LAYER_TYPE = 2; - - ushort flagsValue = ReadWord(reader); - ushort type = ReadWord(reader); - IgnoreWord(reader); - IgnoreWord(reader); - IgnoreWord(reader); - ushort blend = ReadWord(reader); - byte opacity = ReadByte(reader); - IgnoreBytes(reader, 3); - string name = ReadString(reader); - - AsepriteLayerFlags flags = (AsepriteLayerFlags)flagsValue; - - switch (type) - { - case NORMAL_LAYER_TYPE: - case GROUP_LAYER_TYPE: - builder.AddLayer(flags, blend, opacity, name); - break; - case TILEMAP_LAYER_TYPE: - ReadTilemapLayerChunk(reader, builder, flags, blend, opacity, name); - break; - default: - throw new InvalidOperationException($"Unknown layer type '{type}'"); - } - } - - private static void ReadTilemapLayerChunk(BinaryReader reader, AsepriteFileBuilder builder, AsepriteLayerFlags flags, ushort blend, byte opacity, string name) - { - uint index = ReadDword(reader); - builder.AddTilemapLayer(index, flags, blend, opacity, name); - } - - private static void ReadCelChunk(BinaryReader reader, AsepriteFileBuilder builder, long chunkEnd) - { - const ushort RAW_IMAGE_TYPE = 0; - const ushort LINKED_CEL_TYPE = 1; - const ushort COMPRESSED_IMAGE_TYPE = 2; - const ushort COMPRESSED_TILEMAP_TYPE = 3; - - ushort layerIndex = ReadWord(reader); - short x = ReadShort(reader); - short y = ReadShort(reader); - byte opacity = ReadByte(reader); - ushort type = ReadWord(reader); - IgnoreBytes(reader, 7); - - Point position = new(x, y); - - switch (type) - { - case RAW_IMAGE_TYPE: - ReadRawImageCel(reader, builder, x, y, layerIndex, opacity, chunkEnd); - break; - case LINKED_CEL_TYPE: - ReadLinkedCel(reader, builder); - break; - case COMPRESSED_IMAGE_TYPE: - ReadCompressedImageCel(reader, builder, x, y, layerIndex, opacity, chunkEnd); - break; - case COMPRESSED_TILEMAP_TYPE: - ReadCompressedTilemapCel(reader, builder, x, y, layerIndex, opacity, chunkEnd); - break; - default: - throw new InvalidOperationException($"Unknown cel type '{type}'"); - } - } - - private static void ReadRawImageCel(BinaryReader reader, AsepriteFileBuilder builder, short x, short y, ushort layerIndex, byte opacity, long chunkEnd) - { - ushort width = ReadWord(reader); - ushort height = ReadWord(reader); - - int len = (int)(chunkEnd - reader.BaseStream.Position); - byte[] data = ReadBytes(reader, len); - - builder.AddRawImageCel(x, y, width, height, layerIndex, opacity, data); - } - - private static void ReadLinkedCel(BinaryReader reader, AsepriteFileBuilder builder) - { - ushort frameIndex = ReadWord(reader); - builder.AddLinkedCel(frameIndex); - } - - private static void ReadCompressedImageCel(BinaryReader reader, AsepriteFileBuilder builder, short x, short y, ushort layerIndex, byte opacity, long chunkEnd) - { - ushort width = ReadWord(reader); - ushort height = ReadWord(reader); - - int len = (int)(chunkEnd - reader.BaseStream.Position); - byte[] compressedData = ReadBytes(reader, len); - builder.AddCompressedImageCel(x, y, width, height, layerIndex, opacity, compressedData); - } - - private static void ReadCompressedTilemapCel(BinaryReader reader, AsepriteFileBuilder builder, short x, short y, ushort layerIndex, byte opacity, long chunkEnd) - { - ushort columns = ReadWord(reader); - ushort rows = ReadWord(reader); - ushort bitsPerTile = ReadWord(reader); - uint idBitmask = ReadDword(reader); - uint xFlipBitmask = ReadDword(reader); - uint yFlipBitmask = ReadDword(reader); - uint rotationBitmask = ReadDword(reader); - IgnoreBytes(reader, 10); - - int len = (int)(chunkEnd - reader.BaseStream.Position); - byte[] compressedData = ReadBytes(reader, len); - builder.AddCompressedTilemapCel(x, y, columns, rows, layerIndex, opacity, compressedData, bitsPerTile, idBitmask, xFlipBitmask, yFlipBitmask, rotationBitmask); - } - - private static void ReadTagsChunk(BinaryReader reader, AsepriteFileBuilder builder) - { - ushort count = ReadWord(reader); - IgnoreBytes(reader, 8); - - for (int tagNum = 0; tagNum < count; tagNum++) - { - ushort from = ReadWord(reader); - ushort to = ReadWord(reader); - byte direction = ReadByte(reader); - ushort repeat = ReadWord(reader); - IgnoreBytes(reader, 6); - ReadOnlySpan rgb = ReadBytes(reader, 3); - IgnoreByte(reader); - string name = ReadString(reader); - - builder.AddTag(from, to, direction, repeat, rgb, name); - } - } - - private static void ReadPaletteChunk(BinaryReader reader, AsepriteFileBuilder builder) - { - const ushort HAS_NAME_FLAG = 1; - - uint newSize = ReadDword(reader); - uint from = ReadDword(reader); - uint to = ReadDword(reader); - IgnoreBytes(reader, 8); - - builder.ResizePalette(newSize); - - - for (uint entry = from; entry <= to; entry++) - { - ushort flags = ReadWord(reader); - ReadOnlySpan rgba = ReadBytes(reader, 4); - - if ((flags & HAS_NAME_FLAG) != 0) - { - IgnoreString(reader); - } - - builder.AddPaletteEntry(entry, rgba); - } - } - - private static void ReadSliceChunk(BinaryReader reader, AsepriteFileBuilder builder) - { - const uint NINE_PATCH_FLAG = 1; - const uint HAS_PIVOT_FLAG = 2; - - uint count = ReadDword(reader); - uint flags = ReadDword(reader); - IgnoreDword(reader); - string name = ReadString(reader); - - bool isNinePatch = (flags & NINE_PATCH_FLAG) != 0; - bool hasPivot = (flags & HAS_PIVOT_FLAG) != 0; - - AsepriteSliceKey[] keys = new AsepriteSliceKey[count]; - - for (uint keyNum = 0; keyNum < count; keyNum++) - { - uint start = ReadDword(reader); - int x = ReadLong(reader); - int y = ReadLong(reader); - uint width = ReadDword(reader); - uint height = ReadDword(reader); - - Rectangle bounds = new(x, y, (int)width, (int)height); - Rectangle? center = default; - Point? pivot = default; - - if (isNinePatch) - { - int cx = ReadLong(reader); - int cy = ReadLong(reader); - uint cw = ReadDword(reader); - uint ch = ReadDword(reader); - - center = new(cx, cy, (int)cw, (int)ch); - } - - if (hasPivot) - { - int px = ReadLong(reader); - int py = ReadLong(reader); - - pivot = new(px, py); - } - - AsepriteSliceKey key = new((int)start, bounds, center, pivot); - keys[keyNum] = key; - } - - builder.AddSlice(name, isNinePatch, hasPivot, keys); - } - - private static void ReadTilesetChunk(BinaryReader reader, AsepriteFileBuilder builder) - { - const uint EXTERNAL_FILE_FLAG = 1; - const uint EMBEDDED_FLAG = 2; - - uint id = ReadDword(reader); - uint flags = ReadDword(reader); - uint count = ReadDword(reader); - ushort tileWidth = ReadWord(reader); - ushort tileHeight = ReadWord(reader); - IgnoreShort(reader); - IgnoreBytes(reader, 14); - string name = ReadString(reader); - - if ((flags & EXTERNAL_FILE_FLAG) != 0) - { - throw new InvalidOperationException($"Tileset '{name}' includes tileset in external file. This is not supported at this time"); - } - - if ((flags & EMBEDDED_FLAG) == 0) - { - throw new InvalidOperationException($"Tileset '{name}' does not include tileset image in file"); - } - - uint len = ReadDword(reader); - byte[] compressedData = ReadBytes(reader, (int)len); - - builder.AddTileset(id, count, tileWidth, tileHeight, name, compressedData); - } - - private static void ReadUserDataChunk(BinaryReader reader, AsepriteFileBuilder builder, ushort lastChunkType, int tagIterator) - { - const uint HAS_TEXT_FLAG = 1; - const uint HAS_COLOR_FLAG = 2; - uint flags = ReadDword(reader); - - string? text = default; - Color? color = default; - - if ((flags & HAS_TEXT_FLAG) != 0) - { - text = ReadString(reader); - } - - if ((flags & HAS_COLOR_FLAG) != 0) - { - ReadOnlySpan rgba = ReadBytes(reader, 4); - color = new Color(rgba[0], rgba[1], rgba[2], rgba[3]); - } - - switch (lastChunkType) - { - case CHUNK_TYPE_CEL: - builder.SetLastCelUserData(text, color); - break; - case CHUNK_TYPE_LAYER: - builder.SetLastLayerUserData(text, color); - break; - case CHUNK_TYPE_SLICE: - builder.SetLastSliceUserData(text, color); - break; - case CHUNK_TYPE_TAGS: - builder.SetTagUserData(tagIterator, text, color); - break; - case CHUNK_TYPE_OLD_PALETTE_1: - // Starting in Aseprite 1.3-beta21, after the first palette chunk in the first frame, if user data is - // detected, then that is user data for the "sprite" itself - builder.SetSpriteUserData(text, color); - break; - case CHUNK_TYPE_TILESET: - // Starting in Aseprite 1.3-rc1, Tilesets can have user data, though it appears it's not settable in - // the Aseprite UI, and only settable through the LUA Scripting API within Aseprite. Regardless, - // we have to handle it - builder.SetTilesetUserData(text, color); - break; - default: - throw new InvalidOperationException($"Invalid chunk type (0x{lastChunkType:X4}) for user data"); - } - } - - private static byte ReadByte(BinaryReader reader) => reader.ReadByte(); - private static byte[] ReadBytes(BinaryReader reader, int len) => reader.ReadBytes(len); - private static ushort ReadWord(BinaryReader reader) => reader.ReadUInt16(); - private static short ReadShort(BinaryReader reader) => reader.ReadInt16(); - private static uint ReadDword(BinaryReader reader) => reader.ReadUInt32(); - private static int ReadLong(BinaryReader reader) => reader.ReadInt32(); - private static string ReadString(BinaryReader reader) => Encoding.UTF8.GetString(ReadBytes(reader, ReadWord(reader))); - - private static void IgnoreBytes(BinaryReader reader, int len) => reader.BaseStream.Position += len; - private static void IgnoreByte(BinaryReader reader) => IgnoreBytes(reader, 1); - private static void IgnoreWord(BinaryReader reader) => IgnoreBytes(reader, 2); - private static void IgnoreShort(BinaryReader reader) => IgnoreBytes(reader, 2); - private static void IgnoreDword(BinaryReader reader) => IgnoreBytes(reader, 4); - private static void IgnoreLong(BinaryReader reader) => IgnoreBytes(reader, 4); - private static void IgnoreString(BinaryReader reader) => IgnoreBytes(reader, ReadWord(reader)); -} +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.Xna.Framework; +using MonoGame.Aseprite.AsepriteTypes; + +namespace MonoGame.Aseprite.Content.Readers; + +/// +/// Defines a reader that reads an . +/// +public static class AsepriteFileReader +{ + const ushort CHUNK_TYPE_OLD_PALETTE_1 = 0x0004; + const ushort CHUNK_TYPE_OLD_PALETTE_2 = 0x0011; + const ushort CHUNK_TYPE_LAYER = 0x2004; + const ushort CHUNK_TYPE_CEL = 0x2005; + const ushort CHUNK_TYPE_CEL_EXTRA = 0x2006; + const ushort CHUNK_TYPE_COLOR_PROFILE = 0x2007; + const ushort CHUNK_TYPE_EXTERNAL_FILES = 0x2008; + const ushort CHUNK_TYPE_MASK = 0x2016; + const ushort CHUNK_TYPE_PATH = 0x2017; + const ushort CHUNK_TYPE_TAGS = 0x2018; + const ushort CHUNK_TYPE_PALETTE = 0x2019; + const ushort CHUNK_TYPE_USER_DATA = 0x2020; + const ushort CHUNK_TYPE_SLICE = 0x2022; + const ushort CHUNK_TYPE_TILESET = 0x2023; + + /// + /// Reads the at the given path. + /// + /// + /// The path and name of the aseprite file to read. + /// + /// + /// The created by this method. + /// + /// + /// Thrown if no file is located at the specified path. + /// + /// + /// Thrown if an error occurs during the reading of the aseprite file. The exception message will contain the + /// cause of the exception. + /// + public static AsepriteFile ReadFile(string path) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Unable to locate a file at the path '{path}'"); + } + + using Stream stream = File.OpenRead(path); + using BinaryReader reader = new(stream); + + string name = Path.GetFileNameWithoutExtension(path); + return Read(name, reader); + } + + /// + /// Reads the using the provided . + ///
+ /// Use this method with to load raw .aseprite files on Android + /// or other platforms. + ///
+ /// + /// The name of the Aseprite file. + /// + /// + /// A file stream, preferably instantiated from calling . + /// + /// + /// The created by this method. + /// + /// + /// Thrown if no file is located at the specified path. + /// + /// + /// Thrown if an error occurs during the reading of the aseprite file. The exception message will contain the + /// cause of the exception. + /// + public static AsepriteFile ReadStream(string name, [NotNull] Stream fileStream) + { + using BinaryReader reader = new(fileStream); + return Read(name, reader); + } + + internal static AsepriteFile Read(string name, BinaryReader reader) + { + AsepriteFileBuilder fileBuilder = new(name); + + ReadFileHeader(reader, fileBuilder, out ushort nFrames); + ReadFrames(reader, fileBuilder, nFrames); + + AsepriteFile file = fileBuilder.Build(); + + return file; + } + + private static void ReadFileHeader(BinaryReader reader, AsepriteFileBuilder builder, out ushort nFrames) + { + const uint LAYER_OPACITY_VALID_FLAG = 1; + + IgnoreDword(reader); + ushort magic = ReadWord(reader); + + if (magic != 0xA5E0) + { + throw new InvalidOperationException($"Invalid header magic number (0x{magic:X4})."); + } + + nFrames = ReadWord(reader); + ushort frameWidth = ReadWord(reader); + ushort frameHeight = ReadWord(reader); + + ushort colorDepth = ReadWord(reader); + uint flags = ReadDword(reader); + + bool layerOpacityValid = (flags & LAYER_OPACITY_VALID_FLAG) != 0; + + IgnoreWord(reader); + IgnoreDword(reader); + IgnoreDword(reader); + + byte transparentIndex = ReadByte(reader); + IgnoreBytes(reader, 3); + ushort nColors = ReadWord(reader); + + reader.BaseStream.Position = 128; + + builder.SetFrameWidth(frameWidth); + builder.SetFrameHeight(frameHeight); + builder.SetColorDepth(colorDepth); + builder.SetTransparentIndex(transparentIndex); + builder.SetLayerOpacityValid(layerOpacityValid); + } + + private static void ReadFrames(BinaryReader reader, AsepriteFileBuilder builder, ushort nFrames) + { + for (int frameNum = 0; frameNum < nFrames; frameNum++) + { + IgnoreDword(reader); + ushort magic = ReadWord(reader); + + if (magic != 0xF1FA) + { + throw new InvalidOperationException($"Invalid frame magic in frame number {frameNum}"); + } + + ushort nChunksA = ReadWord(reader); + ushort duration = ReadWord(reader); + IgnoreBytes(reader, 2); + uint nChunksB = ReadDword(reader); + + uint nChunks = nChunksA == 0xFFFF && nChunksA < nChunksB ? + nChunksB : + nChunksA; + + + // Start iterator at -1 so after tags chunk is read, it'll + // increment to 0 to kick off the user data read. + int tagIterator = -1; + ushort lastChunkType = default; + + List cels = new(); + + for (uint chunkNum = 0; chunkNum < nChunks; chunkNum++) + { + lastChunkType = ReadChunk(reader, builder, lastChunkType, tagIterator, cels); + if (lastChunkType == CHUNK_TYPE_TAGS) + { + tagIterator++; + } + } + + builder.AddFrame(duration); + } + + } + + private static ushort ReadChunk(BinaryReader reader, AsepriteFileBuilder builder, ushort lastChunkType, int tagIterator, List cels) + { + long start = reader.BaseStream.Position; + + uint len = ReadDword(reader); + ushort type = ReadWord(reader); + + long end = start + len; + + switch (type) + { + case CHUNK_TYPE_LAYER: + ReadLayerChunk(reader, builder); + break; + case CHUNK_TYPE_CEL: + ReadCelChunk(reader, builder, end); + break; + case CHUNK_TYPE_TAGS: + ReadTagsChunk(reader, builder); + break; + case CHUNK_TYPE_PALETTE: + ReadPaletteChunk(reader, builder); + break; + case CHUNK_TYPE_SLICE: + ReadSliceChunk(reader, builder); + break; + case CHUNK_TYPE_TILESET: + ReadTilesetChunk(reader, builder); + break; + case CHUNK_TYPE_USER_DATA: + ReadUserDataChunk(reader, builder, lastChunkType, tagIterator); + break; + case CHUNK_TYPE_OLD_PALETTE_1: + ReadOldPalette1Chunk(reader, builder); + break; + case CHUNK_TYPE_OLD_PALETTE_2: + case CHUNK_TYPE_CEL_EXTRA: + case CHUNK_TYPE_COLOR_PROFILE: + case CHUNK_TYPE_EXTERNAL_FILES: + case CHUNK_TYPE_MASK: + case CHUNK_TYPE_PATH: + reader.BaseStream.Position = end; + break; + default: + throw new InvalidOperationException($"Unknown chunk type (0x{type:X4})"); + } + + if (type == CHUNK_TYPE_USER_DATA) + { + return lastChunkType; + } + + return type; + } + + private static void ReadLayerChunk(BinaryReader reader, AsepriteFileBuilder builder) + { + const int NORMAL_LAYER_TYPE = 0; + const int GROUP_LAYER_TYPE = 1; + const int TILEMAP_LAYER_TYPE = 2; + + ushort flagsValue = ReadWord(reader); + ushort type = ReadWord(reader); + IgnoreWord(reader); + IgnoreWord(reader); + IgnoreWord(reader); + ushort blend = ReadWord(reader); + byte opacity = ReadByte(reader); + IgnoreBytes(reader, 3); + string name = ReadString(reader); + + AsepriteLayerFlags flags = (AsepriteLayerFlags)flagsValue; + + switch (type) + { + case NORMAL_LAYER_TYPE: + case GROUP_LAYER_TYPE: + builder.AddLayer(flags, blend, opacity, name); + break; + case TILEMAP_LAYER_TYPE: + ReadTilemapLayerChunk(reader, builder, flags, blend, opacity, name); + break; + default: + throw new InvalidOperationException($"Unknown layer type '{type}'"); + } + } + + private static void ReadTilemapLayerChunk(BinaryReader reader, AsepriteFileBuilder builder, AsepriteLayerFlags flags, ushort blend, byte opacity, string name) + { + uint index = ReadDword(reader); + builder.AddTilemapLayer(index, flags, blend, opacity, name); + } + + private static void ReadCelChunk(BinaryReader reader, AsepriteFileBuilder builder, long chunkEnd) + { + const ushort RAW_IMAGE_TYPE = 0; + const ushort LINKED_CEL_TYPE = 1; + const ushort COMPRESSED_IMAGE_TYPE = 2; + const ushort COMPRESSED_TILEMAP_TYPE = 3; + + ushort layerIndex = ReadWord(reader); + short x = ReadShort(reader); + short y = ReadShort(reader); + byte opacity = ReadByte(reader); + ushort type = ReadWord(reader); + IgnoreBytes(reader, 7); + + Point position = new(x, y); + + switch (type) + { + case RAW_IMAGE_TYPE: + ReadRawImageCel(reader, builder, x, y, layerIndex, opacity, chunkEnd); + break; + case LINKED_CEL_TYPE: + ReadLinkedCel(reader, builder); + break; + case COMPRESSED_IMAGE_TYPE: + ReadCompressedImageCel(reader, builder, x, y, layerIndex, opacity, chunkEnd); + break; + case COMPRESSED_TILEMAP_TYPE: + ReadCompressedTilemapCel(reader, builder, x, y, layerIndex, opacity, chunkEnd); + break; + default: + throw new InvalidOperationException($"Unknown cel type '{type}'"); + } + } + + private static void ReadRawImageCel(BinaryReader reader, AsepriteFileBuilder builder, short x, short y, ushort layerIndex, byte opacity, long chunkEnd) + { + ushort width = ReadWord(reader); + ushort height = ReadWord(reader); + + int len = (int)(chunkEnd - reader.BaseStream.Position); + byte[] data = ReadBytes(reader, len); + + builder.AddRawImageCel(x, y, width, height, layerIndex, opacity, data); + } + + private static void ReadLinkedCel(BinaryReader reader, AsepriteFileBuilder builder) + { + ushort frameIndex = ReadWord(reader); + builder.AddLinkedCel(frameIndex); + } + + private static void ReadCompressedImageCel(BinaryReader reader, AsepriteFileBuilder builder, short x, short y, ushort layerIndex, byte opacity, long chunkEnd) + { + ushort width = ReadWord(reader); + ushort height = ReadWord(reader); + + int len = (int)(chunkEnd - reader.BaseStream.Position); + byte[] compressedData = ReadBytes(reader, len); + builder.AddCompressedImageCel(x, y, width, height, layerIndex, opacity, compressedData); + } + + private static void ReadCompressedTilemapCel(BinaryReader reader, AsepriteFileBuilder builder, short x, short y, ushort layerIndex, byte opacity, long chunkEnd) + { + ushort columns = ReadWord(reader); + ushort rows = ReadWord(reader); + ushort bitsPerTile = ReadWord(reader); + uint idBitmask = ReadDword(reader); + uint xFlipBitmask = ReadDword(reader); + uint yFlipBitmask = ReadDword(reader); + uint dFlipBitmask = ReadDword(reader); + IgnoreBytes(reader, 10); + + int len = (int)(chunkEnd - reader.BaseStream.Position); + byte[] compressedData = ReadBytes(reader, len); + builder.AddCompressedTilemapCel(x, y, columns, rows, layerIndex, opacity, compressedData, bitsPerTile, idBitmask, xFlipBitmask, yFlipBitmask, dFlipBitmask); + } + + private static void ReadTagsChunk(BinaryReader reader, AsepriteFileBuilder builder) + { + ushort count = ReadWord(reader); + IgnoreBytes(reader, 8); + + for (int tagNum = 0; tagNum < count; tagNum++) + { + ushort from = ReadWord(reader); + ushort to = ReadWord(reader); + byte direction = ReadByte(reader); + ushort repeat = ReadWord(reader); + IgnoreBytes(reader, 6); + ReadOnlySpan rgb = ReadBytes(reader, 3); + IgnoreByte(reader); + string name = ReadString(reader); + + builder.AddTag(from, to, direction, repeat, rgb, name); + } + } + + private static void ReadOldPalette1Chunk(BinaryReader reader, AsepriteFileBuilder builder) + { + ushort packets = ReadWord(reader); + int skip = 0; + int size = 0; + Span rgb = stackalloc byte[4]; + + for(int i = 0; i < packets; i++) + { + skip += ReadByte(reader); + size = ReadByte(reader); + + if(size == 0) + { + size = 256; + } + + builder.ResizePalette((uint)size); + + for(int c = skip; c < skip + size; c++) + { + rgb.Clear(); + rgb[0] = ReadByte(reader); + rgb[1] = ReadByte(reader); + rgb[2] = ReadByte(reader); + rgb[3] = 255; + builder.AddPaletteEntry((uint)c, rgb); + } + } + } + + private static void ReadPaletteChunk(BinaryReader reader, AsepriteFileBuilder builder) + { + const ushort HAS_NAME_FLAG = 1; + + uint newSize = ReadDword(reader); + uint from = ReadDword(reader); + uint to = ReadDword(reader); + IgnoreBytes(reader, 8); + + builder.ResizePalette(newSize); + + + for (uint entry = from; entry <= to; entry++) + { + ushort flags = ReadWord(reader); + ReadOnlySpan rgba = ReadBytes(reader, 4); + + if ((flags & HAS_NAME_FLAG) != 0) + { + IgnoreString(reader); + } + + builder.AddPaletteEntry(entry, rgba); + } + } + + private static void ReadSliceChunk(BinaryReader reader, AsepriteFileBuilder builder) + { + const uint NINE_PATCH_FLAG = 1; + const uint HAS_PIVOT_FLAG = 2; + + uint count = ReadDword(reader); + uint flags = ReadDword(reader); + IgnoreDword(reader); + string name = ReadString(reader); + + bool isNinePatch = (flags & NINE_PATCH_FLAG) != 0; + bool hasPivot = (flags & HAS_PIVOT_FLAG) != 0; + + AsepriteSliceKey[] keys = new AsepriteSliceKey[count]; + + for (uint keyNum = 0; keyNum < count; keyNum++) + { + uint start = ReadDword(reader); + int x = ReadLong(reader); + int y = ReadLong(reader); + uint width = ReadDword(reader); + uint height = ReadDword(reader); + + Rectangle bounds = new(x, y, (int)width, (int)height); + Rectangle? center = default; + Point? pivot = default; + + if (isNinePatch) + { + int cx = ReadLong(reader); + int cy = ReadLong(reader); + uint cw = ReadDword(reader); + uint ch = ReadDword(reader); + + center = new(cx, cy, (int)cw, (int)ch); + } + + if (hasPivot) + { + int px = ReadLong(reader); + int py = ReadLong(reader); + + pivot = new(px, py); + } + + AsepriteSliceKey key = new((int)start, bounds, center, pivot); + keys[keyNum] = key; + } + + builder.AddSlice(name, isNinePatch, hasPivot, keys); + } + + private static void ReadTilesetChunk(BinaryReader reader, AsepriteFileBuilder builder) + { + const uint EXTERNAL_FILE_FLAG = 1; + const uint EMBEDDED_FLAG = 2; + + uint id = ReadDword(reader); + uint flags = ReadDword(reader); + uint count = ReadDword(reader); + ushort tileWidth = ReadWord(reader); + ushort tileHeight = ReadWord(reader); + IgnoreShort(reader); + IgnoreBytes(reader, 14); + string name = ReadString(reader); + + if ((flags & EXTERNAL_FILE_FLAG) != 0) + { + throw new InvalidOperationException($"Tileset '{name}' includes tileset in external file. This is not supported at this time"); + } + + if ((flags & EMBEDDED_FLAG) == 0) + { + throw new InvalidOperationException($"Tileset '{name}' does not include tileset image in file"); + } + + uint len = ReadDword(reader); + byte[] compressedData = ReadBytes(reader, (int)len); + + builder.AddTileset(id, count, tileWidth, tileHeight, name, compressedData); + } + + private static void ReadUserDataChunk(BinaryReader reader, AsepriteFileBuilder builder, ushort lastChunkType, int tagIterator) + { + const uint HAS_TEXT_FLAG = 1; + const uint HAS_COLOR_FLAG = 2; + uint flags = ReadDword(reader); + + string? text = default; + Color? color = default; + + if ((flags & HAS_TEXT_FLAG) != 0) + { + text = ReadString(reader); + } + + if ((flags & HAS_COLOR_FLAG) != 0) + { + ReadOnlySpan rgba = ReadBytes(reader, 4); + color = new Color(rgba[0], rgba[1], rgba[2], rgba[3]); + } + + switch (lastChunkType) + { + case CHUNK_TYPE_CEL: + builder.SetLastCelUserData(text, color); + break; + case CHUNK_TYPE_LAYER: + builder.SetLastLayerUserData(text, color); + break; + case CHUNK_TYPE_SLICE: + builder.SetLastSliceUserData(text, color); + break; + case CHUNK_TYPE_TAGS: + builder.SetTagUserData(tagIterator, text, color); + break; + case CHUNK_TYPE_OLD_PALETTE_1: + // Starting in Aseprite 1.3-beta21, after the first palette chunk in the first frame, if user data is + // detected, then that is user data for the "sprite" itself + builder.SetSpriteUserData(text, color); + break; + case CHUNK_TYPE_TILESET: + // Starting in Aseprite 1.3-rc1, Tilesets can have user data, though it appears it's not settable in + // the Aseprite UI, and only settable through the LUA Scripting API within Aseprite. Regardless, + // we have to handle it + builder.SetTilesetUserData(text, color); + break; + default: + throw new InvalidOperationException($"Invalid chunk type (0x{lastChunkType:X4}) for user data"); + } + } + + private static byte ReadByte(BinaryReader reader) => reader.ReadByte(); + private static byte[] ReadBytes(BinaryReader reader, int len) => reader.ReadBytes(len); + private static ushort ReadWord(BinaryReader reader) => reader.ReadUInt16(); + private static short ReadShort(BinaryReader reader) => reader.ReadInt16(); + private static uint ReadDword(BinaryReader reader) => reader.ReadUInt32(); + private static int ReadLong(BinaryReader reader) => reader.ReadInt32(); + private static string ReadString(BinaryReader reader) => Encoding.UTF8.GetString(ReadBytes(reader, ReadWord(reader))); + + private static void IgnoreBytes(BinaryReader reader, int len) => reader.BaseStream.Position += len; + private static void IgnoreByte(BinaryReader reader) => IgnoreBytes(reader, 1); + private static void IgnoreWord(BinaryReader reader) => IgnoreBytes(reader, 2); + private static void IgnoreShort(BinaryReader reader) => IgnoreBytes(reader, 2); + private static void IgnoreDword(BinaryReader reader) => IgnoreBytes(reader, 4); + private static void IgnoreLong(BinaryReader reader) => IgnoreBytes(reader, 4); + private static void IgnoreString(BinaryReader reader) => IgnoreBytes(reader, ReadWord(reader)); +} diff --git a/source/MonoGame.Aseprite.Shared/RawTypes/RawTilemapTile.cs b/source/MonoGame.Aseprite.Shared/RawTypes/RawTilemapTile.cs index 17f9b67..2a52ebc 100644 --- a/source/MonoGame.Aseprite.Shared/RawTypes/RawTilemapTile.cs +++ b/source/MonoGame.Aseprite.Shared/RawTypes/RawTilemapTile.cs @@ -1,70 +1,70 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -namespace MonoGame.Aseprite.RawTypes; - -/// -/// Defines a class that represents the raw values of a tilemap tile. -/// -public sealed class RawTilemapTile : IEquatable -{ - /// - /// Gets the ID of the source tile in the tileset that represents the texture region used by the tilemap tile. - /// - public int TilesetTileID { get; } - - /// - /// Gets a value that indicates if the tilemap tile should be flipped horizontally. - /// - public bool FlipHorizontally { get; } - - /// - /// Gets a value that indicates if the tilemap tile should be flipped vertically. - /// - public bool FlipVertically { get; } - - /// - /// Gets the rotation, in radians, of the tilemap tile. - /// - public float Rotation { get; } - - internal RawTilemapTile(int tilesetTileID, bool flipHorizontally, bool flipVertically, float rotation) => - (TilesetTileID, FlipHorizontally, FlipVertically, Rotation) = (tilesetTileID, flipHorizontally, flipVertically, rotation); - - /// - /// Returns a value that indicates if the given is equal to this - /// . - /// - /// - /// The other to check for equality with this . - /// - /// - /// if the given is equal to this - /// ; otherwise, . - /// - public bool Equals(RawTilemapTile? other) => other is not null - && TilesetTileID == other.TilesetTileID - && FlipHorizontally == other.FlipHorizontally - && FlipVertically == other.FlipVertically; -} +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +namespace MonoGame.Aseprite.RawTypes; + +/// +/// Defines a class that represents the raw values of a tilemap tile. +/// +public sealed class RawTilemapTile : IEquatable +{ + /// + /// Gets the ID of the source tile in the tileset that represents the texture region used by the tilemap tile. + /// + public int TilesetTileID { get; } + + /// + /// Gets a value that indicates if the tilemap tile should be flipped horizontally. + /// + public bool FlipHorizontally { get; } + + /// + /// Gets a value that indicates if the tilemap tile should be flipped vertically. + /// + public bool FlipVertically { get; } + + /// + /// Gets a value that indicates if the tilemap tile should be flipped diagonally. + /// + public bool FlipDiagonally { get; } + + internal RawTilemapTile(int tilesetTileID, bool flipHorizontally, bool flipVertically, bool flipDiagonally) => + (TilesetTileID, FlipHorizontally, FlipVertically, FlipDiagonally) = (tilesetTileID, flipHorizontally, flipVertically, flipDiagonally); + + /// + /// Returns a value that indicates if the given is equal to this + /// . + /// + /// + /// The other to check for equality with this . + /// + /// + /// if the given is equal to this + /// ; otherwise, . + /// + public bool Equals(RawTilemapTile? other) => other is not null + && TilesetTileID == other.TilesetTileID + && FlipHorizontally == other.FlipHorizontally + && FlipVertically == other.FlipVertically; +} diff --git a/source/MonoGame.Aseprite/Content/Pipeline/Readers/AnimatedTilemapContentTypeReader.cs b/source/MonoGame.Aseprite/Content/Pipeline/Readers/AnimatedTilemapContentTypeReader.cs index b979b33..acd0c43 100644 --- a/source/MonoGame.Aseprite/Content/Pipeline/Readers/AnimatedTilemapContentTypeReader.cs +++ b/source/MonoGame.Aseprite/Content/Pipeline/Readers/AnimatedTilemapContentTypeReader.cs @@ -57,7 +57,7 @@ protected override AnimatedTilemap Read(ContentReader reader, AnimatedTilemap? e for (int t = 0; t < rawLayer.RawTilemapTiles.Length; t++) { RawTilemapTile rawTile = rawLayer.RawTilemapTiles[t]; - tilemapLayer.SetTile(t, rawTile.TilesetTileID, rawTile.FlipVertically, rawTile.FlipHorizontally, rawTile.Rotation); + tilemapLayer.SetTile(t, rawTile.TilesetTileID, rawTile.FlipVertically, rawTile.FlipHorizontally, rawTile.FlipDiagonally); } } } diff --git a/source/MonoGame.Aseprite/Content/Pipeline/Readers/TilemapContentTypeReader.cs b/source/MonoGame.Aseprite/Content/Pipeline/Readers/TilemapContentTypeReader.cs index 7cf9efa..36636a3 100644 --- a/source/MonoGame.Aseprite/Content/Pipeline/Readers/TilemapContentTypeReader.cs +++ b/source/MonoGame.Aseprite/Content/Pipeline/Readers/TilemapContentTypeReader.cs @@ -1,81 +1,81 @@ -/* ---------------------------------------------------------------------------- -MIT License - -Copyright (c) 2018-2023 Christopher Whitley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ----------------------------------------------------------------------------- */ - -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using MonoGame.Aseprite.RawTypes; -using MonoGame.Aseprite.Tilemaps; - -namespace MonoGame.Aseprite.Content.Pipeline.Readers; - -internal sealed class TilemapContentTypeReader : ContentTypeReader -{ - protected override Tilemap Read(ContentReader reader, Tilemap? existingInstance) - { - if (existingInstance is not null) - { - return existingInstance; - } - - string name = reader.ReadString(); - Tilemap tilemap = new(name); - - Dictionary tilesets = ReadTilesets(reader); - RawTilemapLayer[] layers = reader.ReadRawTilemapLayers(); - - for (int i = 0; i < layers.Length; i++) - { - RawTilemapLayer rawLayer = layers[i]; - Tileset tileset = tilesets[rawLayer.TilesetID]; - - TilemapLayer layer = tilemap.CreateLayer(rawLayer.Name, tileset, rawLayer.Columns, rawLayer.Rows, rawLayer.Offset.ToVector2()); - for (int j = 0; j < rawLayer.RawTilemapTiles.Length; j++) - { - RawTilemapTile rawTile = rawLayer.RawTilemapTiles[j]; - layer.SetTile(j, rawTile.TilesetTileID, rawTile.FlipVertically, rawTile.FlipHorizontally, rawTile.Rotation); - } - } - - return tilemap; - } - - private Dictionary ReadTilesets(ContentReader reader) - { - Dictionary tilesets = new(); - - int count = reader.ReadInt32(); - for (int i = 0; i < count; i++) - { - string name = reader.ReadString(); - int id = reader.ReadInt32(); - int tileWidth = reader.ReadInt32(); - int tileHeight = reader.ReadInt32(); - Texture2D texture = reader.ReadObject(); - Tileset tileset = new(name, texture, tileWidth, tileHeight); - tilesets.Add(id, tileset); - } - - return tilesets; - } -} +/* ---------------------------------------------------------------------------- +MIT License + +Copyright (c) 2018-2023 Christopher Whitley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +---------------------------------------------------------------------------- */ + +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Aseprite.RawTypes; +using MonoGame.Aseprite.Tilemaps; + +namespace MonoGame.Aseprite.Content.Pipeline.Readers; + +internal sealed class TilemapContentTypeReader : ContentTypeReader +{ + protected override Tilemap Read(ContentReader reader, Tilemap? existingInstance) + { + if (existingInstance is not null) + { + return existingInstance; + } + + string name = reader.ReadString(); + Tilemap tilemap = new(name); + + Dictionary tilesets = ReadTilesets(reader); + RawTilemapLayer[] layers = reader.ReadRawTilemapLayers(); + + for (int i = 0; i < layers.Length; i++) + { + RawTilemapLayer rawLayer = layers[i]; + Tileset tileset = tilesets[rawLayer.TilesetID]; + + TilemapLayer layer = tilemap.CreateLayer(rawLayer.Name, tileset, rawLayer.Columns, rawLayer.Rows, rawLayer.Offset.ToVector2()); + for (int j = 0; j < rawLayer.RawTilemapTiles.Length; j++) + { + RawTilemapTile rawTile = rawLayer.RawTilemapTiles[j]; + layer.SetTile(j, rawTile.TilesetTileID, rawTile.FlipVertically, rawTile.FlipHorizontally, rawTile.FlipDiagonally); + } + } + + return tilemap; + } + + private Dictionary ReadTilesets(ContentReader reader) + { + Dictionary tilesets = new(); + + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + string name = reader.ReadString(); + int id = reader.ReadInt32(); + int tileWidth = reader.ReadInt32(); + int tileHeight = reader.ReadInt32(); + Texture2D texture = reader.ReadObject(); + Tileset tileset = new(name, texture, tileWidth, tileHeight); + tilesets.Add(id, tileset); + } + + return tilesets; + } +} diff --git a/source/MonoGame.Aseprite/MonoGame.Aseprite.csproj b/source/MonoGame.Aseprite/MonoGame.Aseprite.csproj index 02d5a8e..6a7af45 100644 --- a/source/MonoGame.Aseprite/MonoGame.Aseprite.csproj +++ b/source/MonoGame.Aseprite/MonoGame.Aseprite.csproj @@ -4,7 +4,7 @@ enable enable True - 5.1.2 + 5.1.3 @@ -29,9 +29,14 @@ README.md - Version 5.1.2 - The following changes were implemented: - - Resolves issue where AsepriteFile.TryGetSlice always returned false even when slice exists + Version 5.1.3 + - Added `MonoGame.Aseprite.Configuration` + - When set to `false` this will assume frame index gets from the `AsepriteFile` start at index 1 and not 0. + - Removed `MonoGame.Aserpite.Configuration` + - Moved `ZeroIndexedFrames` to be property of `AsepriteFile` + - Added unit testing for `AsepriteFile.ZeroIndexedFrames` functionality + - Added proper support for new tile rotation introduced in Aseprite 1.3 + - Read Old Palette Chunk for Indexed mode due to Aseprite 1.3.5 file spec change MonoGame.Aseprite is a cross-platofrm C# library that adds support to MonoGame projects for diff --git a/source/MonoGame.Aseprite/SpriteBatchExtensions.cs b/source/MonoGame.Aseprite/SpriteBatchExtensions.cs index 3696bc9..c44c2db 100644 --- a/source/MonoGame.Aseprite/SpriteBatchExtensions.cs +++ b/source/MonoGame.Aseprite/SpriteBatchExtensions.cs @@ -442,16 +442,50 @@ public static void Draw(this SpriteBatch spriteBatch, TilemapLayer layer, Vector tPosition.Y = position.Y + (row * layer.Tileset.TileHeight * scale.Y); Color renderColor = color * layer.Transparency; - SpriteEffects flipEffects = SpriteEffects.None | - (tile.FlipVertically ? SpriteEffects.FlipVertically : 0) | - (tile.FlipHorizontally ? SpriteEffects.FlipHorizontally : 0); + SpriteEffects flipEffects = DetermineFlipEffectForTile(tile.FlipHorizontally, tile.FlipVertically, tile.FlipDiagonally); + float rotation = tile.FlipDiagonally ? MathHelper.ToRadians(90.0f) : 0.0f; + + // Since Aseprite allows tile rotation now, tile are rotated in Aseprite based on + // center origin. So we need an origin point, as well as to offset the position + // draw due to the origin point. + Vector2 origin = new Vector2(layer.Tileset.TileWidth, layer.Tileset.TileHeight) * 0.5f; + tPosition += origin; TextureRegion textureRegion = layer.Tileset[tile.TilesetTileID]; - Draw(spriteBatch, textureRegion, tPosition, renderColor, tile.Rotation, Vector2.Zero, scale, flipEffects, layerDepth); + Draw(spriteBatch, textureRegion, tPosition, renderColor, rotation, origin, scale, flipEffects, layerDepth); } } } + private static SpriteEffects DetermineFlipEffectForTile(bool flipHorizontally, bool flipVertically, bool flipDiagonally) + { + SpriteEffects effects = SpriteEffects.None; + if(!flipDiagonally) + { + if(flipHorizontally) + { + effects |= SpriteEffects.FlipHorizontally; + } + + if(flipVertically) + { + effects |= SpriteEffects.FlipVertically; + } + } + else + { + effects = (flipHorizontally, flipVertically) switch + { + (true, true) => SpriteEffects.FlipHorizontally, + (false, true) => SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically, + (true, false) => SpriteEffects.None, + (false, false) => SpriteEffects.FlipVertically + }; + } + + return effects; + } + #endregion Tilemap Layer } diff --git a/source/MonoGame.Aseprite/Sprites/AnimatedSprite.cs b/source/MonoGame.Aseprite/Sprites/AnimatedSprite.cs index d93125e..169c1a4 100644 --- a/source/MonoGame.Aseprite/Sprites/AnimatedSprite.cs +++ b/source/MonoGame.Aseprite/Sprites/AnimatedSprite.cs @@ -167,9 +167,7 @@ internal AnimatedSprite(AnimationTag tag) /// public void Update(double deltaTimeInSeconds) { - GameTime fakeGameTime = new(); - fakeGameTime.ElapsedGameTime = TimeSpan.FromSeconds(deltaTimeInSeconds); - Update(fakeGameTime); + Update(TimeSpan.FromSeconds(deltaTimeInSeconds)); } /// @@ -182,6 +180,20 @@ public void Update(double deltaTimeInSeconds) /// A snapshot of the game timing values for the current update cycle. /// public void Update(GameTime gameTime) + { + Update(gameTime.ElapsedGameTime); + } + + /// + /// Updates this . + /// + /// + /// This should only be called once per update cycle. + /// + /// + /// The amount of time, that have elapsed since the last update cycle in the game. + /// + public void Update(in TimeSpan elapsedTime) { if (!IsAnimating || IsPaused) { @@ -199,7 +211,7 @@ public void Update(GameTime gameTime) OnFrameBegin?.Invoke(this); } - CurrentFrameTimeRemaining -= gameTime.ElapsedGameTime * Speed; + CurrentFrameTimeRemaining -= elapsedTime * Speed; if (CurrentFrameTimeRemaining <= TimeSpan.Zero) { diff --git a/source/MonoGame.Aseprite/Tilemaps/AnimatedTilemap.cs b/source/MonoGame.Aseprite/Tilemaps/AnimatedTilemap.cs index 22a9300..f47a634 100644 --- a/source/MonoGame.Aseprite/Tilemaps/AnimatedTilemap.cs +++ b/source/MonoGame.Aseprite/Tilemaps/AnimatedTilemap.cs @@ -184,9 +184,7 @@ public AnimatedTilemap(string name, bool isLooping = true, bool isReversed = fal /// public void Update(double deltaTimeInSeconds) { - GameTime fakeGameTime = new(); - fakeGameTime.ElapsedGameTime = TimeSpan.FromSeconds(deltaTimeInSeconds); - Update(fakeGameTime); + Update(TimeSpan.FromSeconds(deltaTimeInSeconds)); } /// @@ -199,6 +197,20 @@ public void Update(double deltaTimeInSeconds) /// A snapshot of the game timing values for the current update cycle. /// public void Update(GameTime gameTime) + { + Update(gameTime.ElapsedGameTime); + } + + /// + /// Updates this . + /// + /// + /// This should only be called once per game update cycle. + /// + /// + /// The amount of time, that have elapsed since the last update cycle in the game. + /// + public void Update(in TimeSpan elapsedTime) { if (!IsAnimating || IsPaused) { @@ -216,7 +228,7 @@ public void Update(GameTime gameTime) OnFrameBegin?.Invoke(this); } - CurrentFrameTimeRemaining -= gameTime.ElapsedGameTime; + CurrentFrameTimeRemaining -= elapsedTime; if (CurrentFrameTimeRemaining <= TimeSpan.Zero) { @@ -683,7 +695,7 @@ public static AnimatedTilemap FromRaw(GraphicsDevice device, RawAnimatedTilemap { RawTilemapTile rawTile = rawLayer.RawTilemapTiles[t]; - layer.SetTile(t, rawTile.TilesetTileID, rawTile.FlipVertically, rawTile.FlipHorizontally, rawTile.Rotation); + layer.SetTile(t, rawTile.TilesetTileID, rawTile.FlipVertically, rawTile.FlipHorizontally, rawTile.FlipDiagonally); } } } diff --git a/source/MonoGame.Aseprite/Tilemaps/Tile.cs b/source/MonoGame.Aseprite/Tilemaps/Tile.cs index 1eac3c7..acfe31f 100644 --- a/source/MonoGame.Aseprite/Tilemaps/Tile.cs +++ b/source/MonoGame.Aseprite/Tilemaps/Tile.cs @@ -51,9 +51,9 @@ public struct Tile public bool FlipVertically = false; /// - /// The amount of rotation, in radians, to apply when rendering this . + /// Indicates whether this should be flipped diagonally when rendered. /// - public float Rotation = 0.0f; + public bool FlipDiagonally = false; /// /// Gets a value that indicates if this is an empty . @@ -90,9 +90,9 @@ public Tile() { } /// /// Indicates whether the should be flipped vertically when rendered. /// - /// - /// The amount of rotation, in radians, to apply when rendering the . + /// + /// Indicates whether the should be flipped diagonally when rendered. /// - public Tile(int tilesetTileID, bool flipHorizontally, bool flipVertically, float rotation) => - (TilesetTileID, FlipHorizontally, FlipVertically, Rotation) = (tilesetTileID, flipHorizontally, flipVertically, rotation); + public Tile(int tilesetTileID, bool flipHorizontally, bool flipVertically, bool flipDiagonally) => + (TilesetTileID, FlipHorizontally, FlipVertically, FlipDiagonally) = (tilesetTileID, flipHorizontally, flipVertically, flipDiagonally); } diff --git a/source/MonoGame.Aseprite/Tilemaps/Tilemap.cs b/source/MonoGame.Aseprite/Tilemaps/Tilemap.cs index 6a2e844..176a479 100644 --- a/source/MonoGame.Aseprite/Tilemaps/Tilemap.cs +++ b/source/MonoGame.Aseprite/Tilemaps/Tilemap.cs @@ -413,7 +413,7 @@ public static Tilemap FromRaw(GraphicsDevice device, RawTilemap rawTilemap) { RawTilemapTile rawTile = rawLayer.RawTilemapTiles[t]; - layer.SetTile(t, rawTile.TilesetTileID, rawTile.FlipVertically, rawTile.FlipHorizontally, rawTile.Rotation); + layer.SetTile(t, rawTile.TilesetTileID, rawTile.FlipVertically, rawTile.FlipHorizontally, rawTile.FlipDiagonally); } } diff --git a/source/MonoGame.Aseprite/Tilemaps/TilemapLayer.cs b/source/MonoGame.Aseprite/Tilemaps/TilemapLayer.cs index 0815667..ba2648a 100644 --- a/source/MonoGame.Aseprite/Tilemaps/TilemapLayer.cs +++ b/source/MonoGame.Aseprite/Tilemaps/TilemapLayer.cs @@ -270,20 +270,20 @@ public TilemapLayer(string name, Tileset tileset, int columns, int rows, Vector2 /// /// Indicates if the element being set should be flipped vertically when rendered. /// - /// - /// The amount of rotation, in radians, to apply when rendering the element being set. + /// + /// Indicates if the element being set should be flipped diagonally when rendered. /// /// /// Thrown if the index specified is less than zero or is greater than or equal to the total number of /// elements in this . /// - public void SetTile(int index, int tilesetTileID, bool flipHorizontally = false, bool flipVertically = false, float rotation = 0.0f) + public void SetTile(int index, int tilesetTileID, bool flipHorizontally = false, bool flipVertically = false, bool flipDiagonally = false) { Tile tile; tile.TilesetTileID = tilesetTileID; tile.FlipHorizontally = flipHorizontally; tile.FlipVertically = flipVertically; - tile.Rotation = rotation; + tile.FlipDiagonally = flipDiagonally; SetTile(index, tile); } @@ -307,20 +307,20 @@ public void SetTile(int index, int tilesetTileID, bool flipHorizontally = false, /// /// Indicates if the element being set should be flipped vertically when rendered. /// - /// - /// The amount of rotation, in radians, to apply when rendering the element being set. + /// + /// Indicates if the element being set should be flipped diagonally when rendered. /// /// /// Thrown if either the column or row specified is less than zero or are greater than or equal to the total /// number of columns or rows in this . /// - public void SetTile(int column, int row, int tilesetTileID, bool flipHorizontally = false, bool flipVertically = false, float rotation = 0.0f) + public void SetTile(int column, int row, int tilesetTileID, bool flipHorizontally = false, bool flipVertically = false, bool flipDiagonally = false) { Tile tile; tile.TilesetTileID = tilesetTileID; tile.FlipHorizontally = flipHorizontally; tile.FlipVertically = flipVertically; - tile.Rotation = rotation; + tile.FlipDiagonally = flipDiagonally; SetTile(column, row, tile); } @@ -341,20 +341,20 @@ public void SetTile(int column, int row, int tilesetTileID, bool flipHorizontall /// /// Indicates if the element being set should be flipped vertically when rendered. /// - /// - /// The amount of rotation, in radians, to apply when rendering the element being set. + /// + /// Indicates if the element being set should be flipped diagonally when rendered. /// /// /// Thrown if either the column or row in the specified location is less than zero or are greater than or equal /// to the total number of columns or rows in this . /// - public void SetTile(Point location, int tilesetTileID, bool flipHorizontally = false, bool flipVertically = false, float rotation = 0.0f) + public void SetTile(Point location, int tilesetTileID, bool flipHorizontally = false, bool flipVertically = false, bool flipDiagonally = false) { Tile tile; tile.TilesetTileID = tilesetTileID; tile.FlipHorizontally = flipHorizontally; tile.FlipVertically = flipVertically; - tile.Rotation = rotation; + tile.FlipDiagonally = flipDiagonally; SetTile(location, tile); } diff --git a/tests/MonoGame.Aseprite.Tests/AsepriteTypeTests/AsepriteFileTests.cs b/tests/MonoGame.Aseprite.Tests/AsepriteTypeTests/AsepriteFileTests.cs index 78e54dd..6679c65 100644 --- a/tests/MonoGame.Aseprite.Tests/AsepriteTypeTests/AsepriteFileTests.cs +++ b/tests/MonoGame.Aseprite.Tests/AsepriteTypeTests/AsepriteFileTests.cs @@ -27,7 +27,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE namespace MonoGame.Aseprite.Tests; -public sealed class AsepriteFileTests +public sealed class AsepriteFileTests { // https://github.com/AristurtleDev/monogame-aseprite/issues/93 [Fact] @@ -51,4 +51,30 @@ public void TryGetSlice_True_When_Slice_Exists() Assert.True(aseFile.TryGetSlice(expectedSliceName, out AsepriteSlice? slice)); } + + [Fact] + public void Get_Frame_When_ZeroIndexed_False() + { + Color[] palette = Array.Empty(); + AsepriteFrame frame = new AsepriteFrame("Frame0", 1, 1, 1, Array.Empty()); + AsepriteFrame[] frames = new AsepriteFrame[] + { + new AsepriteFrame("Frame0", 1, 1, 1, Array.Empty()), + new AsepriteFrame("Frame1", 1, 1, 1, Array.Empty()) + }; + + AsepriteLayer[] layers = Array.Empty(); + AsepriteTag[] tags = Array.Empty(); + AsepriteTileset[] tilesets = Array.Empty(); + AsepriteUserData userData = new AsepriteUserData(); + + AsepriteSlice[] slices = Array.Empty(); + AsepriteFile aseFile = new AsepriteFile("Test", 1, 1, palette, frames, layers, tags, slices, tilesets, userData); + + aseFile.ZeroIndexedFrames = false; + + AsepriteFrame expected = frames[0]; + AsepriteFrame actual = aseFile.GetFrame(1); + Assert.Equal(expected, actual); + } } diff --git a/tests/MonoGame.Aseprite.Tests/AsepriteTypeTests/AsepriteFrameTests.cs b/tests/MonoGame.Aseprite.Tests/AsepriteTypeTests/AsepriteFrameTests.cs index d94370c..077d488 100644 --- a/tests/MonoGame.Aseprite.Tests/AsepriteTypeTests/AsepriteFrameTests.cs +++ b/tests/MonoGame.Aseprite.Tests/AsepriteTypeTests/AsepriteFrameTests.cs @@ -51,10 +51,10 @@ public AsepriteFrameTestsFixture() AsepriteTile[] tiles = new AsepriteTile[] { - new(1, 0, 0, 0), - new(2, 0, 0, 0), - new(3, 0, 0, 0), - new(0, 0, 0, 0) + new AsepriteTile(1, false, false, false), + new AsepriteTile(2, false, false, false), + new AsepriteTile(3, false, false, false), + new AsepriteTile(0, false, false, false) }; AsepriteCel[] cels = new AsepriteCel[] diff --git a/tests/MonoGame.Aseprite.Tests/ContentTests/AnimatedTilemapProcessorTests.cs b/tests/MonoGame.Aseprite.Tests/ContentTests/AnimatedTilemapProcessorTests.cs index f7dbd6c..3f9f397 100644 --- a/tests/MonoGame.Aseprite.Tests/ContentTests/AnimatedTilemapProcessorTests.cs +++ b/tests/MonoGame.Aseprite.Tests/ContentTests/AnimatedTilemapProcessorTests.cs @@ -58,16 +58,16 @@ public AnimatedTilemapProcessorTestFixture() AsepriteTile[] frame0Cel0Tiles = new AsepriteTile[] { - new(0, 0, 0, 0), - new(1, 0, 0, 0), - new(2, 0, 0, 0), - new(3, 0, 0, 0) + new AsepriteTile(0, false, false, false), + new AsepriteTile(1, false, false, false), + new AsepriteTile(2, false, false, false), + new AsepriteTile(3, false, false, false) }; AsepriteTile[] frame0Cel1Tiles = new AsepriteTile[] { - new(2, 0, 0, 0), - new(3, 0, 0, 0) + new AsepriteTile(2, false, false, false), + new AsepriteTile(3, false, false, false) }; AsepriteCel[] frame0Cels = new AsepriteCel[] @@ -78,16 +78,16 @@ public AnimatedTilemapProcessorTestFixture() AsepriteTile[] frame1Cel0Tiles = new AsepriteTile[] { - new(3, 0, 0, 0), - new(2, 0, 0, 0), - new(1, 0, 0, 0), - new(0, 0, 0, 0) + new AsepriteTile(3, false, false, false), + new AsepriteTile(2, false, false, false), + new AsepriteTile(1, false, false, false), + new AsepriteTile(0, false, false, false) }; AsepriteTile[] frame1Cel1Tiles = new AsepriteTile[] { - new(3, 0, 0, 0), - new(2, 0, 0, 0) + new AsepriteTile(3, false, false, false), + new AsepriteTile(2, false, false, false) }; AsepriteCel[] frame1Cels = new AsepriteCel[] @@ -210,10 +210,10 @@ public void ProcessRaw_Duplicate_AsepriteLayer_Names_Throws_Exception() AsepriteTile[] tiles = new AsepriteTile[] { - new(0, 0, 0, 0), - new(1, 0, 0, 0), - new(2, 0, 0, 0), - new(3, 0, 0, 0) + new AsepriteTile(0, false, false, false), + new AsepriteTile(1, false, false, false), + new AsepriteTile(2, false, false, false), + new AsepriteTile(3, false, false, false) }; AsepriteCel[] cels = new AsepriteCel[] @@ -257,13 +257,9 @@ private void AssertRawLayer(RawTilemapLayer tilemapLayer, AsepriteTilemapLayer a RawTilemapTile tilemapTile = tilemapLayer.RawTilemapTiles[i]; Assert.Equal(aseTile.TilesetTileID, tilemapTile.TilesetTileID); - - bool xFlip = aseTile.XFlip != 0; - Assert.Equal(xFlip, tilemapTile.FlipHorizontally); - - bool yFlip = aseTile.YFlip != 0; - Assert.Equal(yFlip, tilemapTile.FlipVertically); - Assert.Equal(aseTile.Rotation, tilemapTile.Rotation); + Assert.Equal(aseTile.XFlip, tilemapTile.FlipHorizontally); + Assert.Equal(aseTile.YFlip, tilemapTile.FlipVertically); + Assert.Equal(aseTile.DFlip, tilemapTile.FlipDiagonally); } } } diff --git a/tests/MonoGame.Aseprite.Tests/ContentTests/RawTypeWriteReadTests.cs b/tests/MonoGame.Aseprite.Tests/ContentTests/RawTypeWriteReadTests.cs index b0072e0..62a37c6 100644 --- a/tests/MonoGame.Aseprite.Tests/ContentTests/RawTypeWriteReadTests.cs +++ b/tests/MonoGame.Aseprite.Tests/ContentTests/RawTypeWriteReadTests.cs @@ -178,10 +178,10 @@ public void Write_Then_Read_RawTilemap() RawTilemapTile[] tiles = new RawTilemapTile[] { - new(0, true, true, 0.0f), - new(1, true, false, 1.0f), - new(2, false, true, 2.0f), - new(3, false, false, 3.0f) + new RawTilemapTile(0, true, true, false), + new RawTilemapTile(1, true, false, true), + new RawTilemapTile(2, false, true, false), + new RawTilemapTile(3, false, false, true) }; RawTilemapLayer[] layers = new RawTilemapLayer[] @@ -209,10 +209,10 @@ public void Write_Then_Read_RawAnimatedTilemap() RawTilemapTile[] tiles = new RawTilemapTile[] { - new(0, true, true, 0.0f), - new(1, true, false, 1.0f), - new(2, false, true, 2.0f), - new(3, false, false, 3.0f) + new RawTilemapTile(0, true, true, false), + new RawTilemapTile(1, true, false, true), + new RawTilemapTile(2, false, true, false), + new RawTilemapTile(3, false, false, true) }; RawTilemapLayer[] layers = new RawTilemapLayer[] diff --git a/tests/MonoGame.Aseprite.Tests/ContentTests/TilemapProcessorTests.cs b/tests/MonoGame.Aseprite.Tests/ContentTests/TilemapProcessorTests.cs index b325c6c..d97bb72 100644 --- a/tests/MonoGame.Aseprite.Tests/ContentTests/TilemapProcessorTests.cs +++ b/tests/MonoGame.Aseprite.Tests/ContentTests/TilemapProcessorTests.cs @@ -57,16 +57,16 @@ public TilemapProcessorTestFixture() AsepriteTile[] cel0Tiles = new AsepriteTile[] { - new(0, 0, 0, 0), - new(1, 0, 0, 0), - new(2, 0, 0, 0), - new(3, 0, 0, 0) + new AsepriteTile(0, false, false, false), + new AsepriteTile(1, false, false, false), + new AsepriteTile(2, false, false, false), + new AsepriteTile(3, false, false, false) }; AsepriteTile[] cel1Tiles = new AsepriteTile[] { - new(2, 0, 0, 0), - new(3, 0, 0, 0) + new AsepriteTile(2, false, false, false), + new AsepriteTile(3, false, false, false) }; AsepriteCel[] cels = new AsepriteCel[] @@ -152,10 +152,10 @@ public void ProcessRaw_Duplicate_AsepriteLayer_Names_Throws_Exception() AsepriteTile[] tiles = new AsepriteTile[] { - new(0, 0, 0, 0), - new(1, 0, 0, 0), - new(2, 0, 0, 0), - new(3, 0, 0, 0) + new AsepriteTile(0, false, false, false), + new AsepriteTile(1, false, false, false), + new AsepriteTile(2, false, false, false), + new AsepriteTile(3, false, false, false) }; AsepriteCel[] cels = new AsepriteCel[] @@ -196,13 +196,9 @@ private void AssertRawLayer(RawTilemapLayer layer, string name, int tilesetID, i for (int i = 0; i < tiles.Length; i++) { Assert.Equal(tiles[i].TilesetTileID, layer.RawTilemapTiles[i].TilesetTileID); - - bool xFlip = tiles[i].XFlip != 0; - Assert.Equal(xFlip, layer.RawTilemapTiles[i].FlipHorizontally); - - bool yFlip = tiles[i].YFlip != 0; - Assert.Equal(yFlip, layer.RawTilemapTiles[i].FlipVertically); - Assert.Equal(tiles[i].Rotation, layer.RawTilemapTiles[i].Rotation); + Assert.Equal(tiles[i].XFlip, layer.RawTilemapTiles[i].FlipHorizontally); + Assert.Equal(tiles[i].YFlip, layer.RawTilemapTiles[i].FlipVertically); + Assert.Equal(tiles[i].DFlip, layer.RawTilemapTiles[i].FlipDiagonally); } } } diff --git a/tests/MonoGame.Aseprite.Tests/IOTests/AsepriteFileReaderTests.cs b/tests/MonoGame.Aseprite.Tests/IOTests/AsepriteFileReaderTests.cs index a12c80e..633c000 100644 --- a/tests/MonoGame.Aseprite.Tests/IOTests/AsepriteFileReaderTests.cs +++ b/tests/MonoGame.Aseprite.Tests/IOTests/AsepriteFileReaderTests.cs @@ -120,10 +120,10 @@ private void Reads_Version_1_3_0_RC1_Assertions(AsepriteFile aseFile) AssertCel(aseFile.Frames[0].Cels[1], "tilemap", 0, 0, 255, null, null); AsepriteTilemapCel frame0cel1 = Assert.IsType(aseFile.Frames[0].Cels[1]); AssertTilemapCel(frame0cel1, 2, 2, 4, "tileset"); - AssertTile(frame0cel1.Tiles[0], 1, 0, 0, 0); - AssertTile(frame0cel1.Tiles[1], 2, 0, 0, 0); - AssertTile(frame0cel1.Tiles[2], 3, 0, 0, 0); - AssertTile(frame0cel1.Tiles[3], 4, 0, 0, 0); + AssertTile(frame0cel1.Tiles[0], 1, false, false, false); + AssertTile(frame0cel1.Tiles[1], 2, false, false, false); + AssertTile(frame0cel1.Tiles[2], 3, false, false, false); + AssertTile(frame0cel1.Tiles[3], 4, false, false, false); AssertCel(aseFile.Frames[0].Cels[2], "eight-frames", 0, 0, 255, null, null); AsepriteImageCel frame0cel2 = Assert.IsType(aseFile.Frames[0].Cels[2]); @@ -289,10 +289,10 @@ private void Reads_Version_1_3_0_Assertions(AsepriteFile aseFile) AssertCel(aseFile.Frames[0].Cels[1], "tilemap", 0, 0, 255, null, null); AsepriteTilemapCel frame0cel1 = Assert.IsType(aseFile.Frames[0].Cels[1]); AssertTilemapCel(frame0cel1, 2, 2, 4, "tileset"); - AssertTile(frame0cel1.Tiles[0], 1, 0, 0, 0); - AssertTile(frame0cel1.Tiles[1], 2, 0, 0, 0); - AssertTile(frame0cel1.Tiles[2], 3, 0, 0, 0); - AssertTile(frame0cel1.Tiles[3], 4, 0, 0, 0); + AssertTile(frame0cel1.Tiles[0], 1, false, false, false); + AssertTile(frame0cel1.Tiles[1], 2, false, false, false); + AssertTile(frame0cel1.Tiles[2], 3, false, false, false); + AssertTile(frame0cel1.Tiles[3], 4, false, false, false); AssertCel(aseFile.Frames[0].Cels[2], "eight-frames", 0, 0, 255, null, null); AsepriteImageCel frame0cel2 = Assert.IsType(aseFile.Frames[0].Cels[2]); @@ -575,12 +575,12 @@ private void AssertTilemapCel(AsepriteTilemapCel tilemapCel, int columns, int ro Assert.Equal(tilesetName, tilemapCel.Tileset.Name); } - private void AssertTile(AsepriteTile tile, int tilesetTileID, int xFlip, int yFlip, int rotation) + private void AssertTile(AsepriteTile tile, int tilesetTileID, bool xFlip, bool yFlip, bool dFlip) { Assert.Equal(tilesetTileID, tile.TilesetTileID); Assert.Equal(xFlip, tile.XFlip); Assert.Equal(yFlip, tile.YFlip); - Assert.Equal(rotation, tile.Rotation); + Assert.Equal(dFlip, tile.DFlip); } private void AssertSlice(AsepriteSlice slice, string name, int keyCount, bool isNine, bool hasPivot, string? userDataText, Color? userDataColor)