diff --git a/PKHeX.Core/Saves/SAV5.cs b/PKHeX.Core/Saves/SAV5.cs index f2ad1be55..5a200ae89 100644 --- a/PKHeX.Core/Saves/SAV5.cs +++ b/PKHeX.Core/Saves/SAV5.cs @@ -136,38 +136,31 @@ private bool CGearSkinPresent private static ReadOnlySpan DLCFooter => [ 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14, 0x27, 0x00, 0x00, 0x27, 0x35, 0x05, 0x31, 0x00, 0x00 ]; - public byte[] CGearSkinData + public Memory CGearSkinData => Data.AsMemory(CGearDataOffset, CGearBackground.SIZE); + + public void SetCGearSkin(ReadOnlySpan value) { - get - { - if (CGearSkinPresent) - return Data.AsSpan(CGearDataOffset, CGearBackground.SIZE_CGB).ToArray(); - return new byte[CGearBackground.SIZE_CGB]; - } - set - { - SetData(value, CGearDataOffset); + ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, CGearBackground.SIZE); + SetData(value, CGearDataOffset); - ushort chk = Checksums.CRC16_CCITT(value); - var footer = Data.AsSpan(CGearDataOffset + value.Length); + ushort chk = Checksums.CRC16_CCITT(value); + var footer = Data.AsSpan(CGearDataOffset + value.Length); - WriteUInt16LittleEndian(footer, 1); // block updated once - WriteUInt16LittleEndian(footer[2..], chk); // checksum - WriteUInt16LittleEndian(footer[0x100..], chk); // second checksum + WriteUInt16LittleEndian(footer, 1); // block updated once + WriteUInt16LittleEndian(footer[2..], chk); // checksum + WriteUInt16LittleEndian(footer[0x100..], chk); // second checksum - DLCFooter.CopyTo(footer[0x102..]); + DLCFooter.CopyTo(footer[0x102..]); - ushort skinchkval = Checksums.CRC16_CCITT(footer[0x100..0x104]); - WriteUInt16LittleEndian(footer[0x112..], skinchkval); + ushort skinchkval = Checksums.CRC16_CCITT(footer[0x100..0x104]); + WriteUInt16LittleEndian(footer[0x112..], skinchkval); - // Indicate in the save file that data is present - WriteUInt16LittleEndian(Data.AsSpan(0x19438), 0xC21E); + // Indicate in the save file that data is present + WriteUInt16LittleEndian(Data.AsSpan(0x19438), 0xC21E); - WriteUInt16LittleEndian(Data.AsSpan(CGearSkinInfoOffset), chk); - CGearSkinPresent = true; - - State.Edited = true; - } + WriteUInt16LittleEndian(Data.AsSpan(CGearSkinInfoOffset), chk); + CGearSkinPresent = true; + State.Edited = true; } public abstract IReadOnlyList AllBlocks { get; } diff --git a/PKHeX.Core/Saves/Substructures/Gen5/CGearBackground.cs b/PKHeX.Core/Saves/Substructures/Gen5/CGearBackground.cs deleted file mode 100644 index f7a7f5558..000000000 --- a/PKHeX.Core/Saves/Substructures/Gen5/CGearBackground.cs +++ /dev/null @@ -1,589 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.InteropServices; -using static System.Buffers.Binary.BinaryPrimitives; - -namespace PKHeX.Core; - -/// -/// Generation 5 C-Gear Background Image -/// -public sealed class CGearBackground -{ - public const string Extension = "cgb"; - public const string Filter = $"C-Gear Background|*.{Extension}"; - public const int Width = 256; // px - public const int Height = 192; // px - private const int ColorCount = 0x10; - private const int TileSize = 8; - private const int TileCount = (Width / TileSize) * (Height / TileSize); // 0x300 - - internal const int CountTilePool = 0xFF; - private const int LengthTilePool = CountTilePool * Tile.SIZE_TILE; // 0x1FE0 - private const int LengthColorData = ColorCount * sizeof(ushort); // 0x20 - private const int OffsetTileMap = LengthTilePool + LengthColorData; // 0x2000 - private const int LengthTileMap = TileCount * sizeof(ushort); // 0x600 - - public const int SIZE_CGB = OffsetTileMap + LengthTileMap; // 0x2600 - - /* CGearBackground Documentation - * CGearBackgrounds (.cgb) are tiled images. - * Tiles are 8x8, and serve as a tileset for building the image. - * The first 0x2000 bytes are the tile building region. - * A tile to have two pixels defined in one byte of space. - * A tile takes up 64 pixels, 32 bytes, 0x20 chunks. - * The last tile is actually the colors used in the image (16bit). - * Only 16 colors can be used for the entire image. - * 255 tiles may be chosen from, as (0x2000-(0x20))/0x20 = 0xFF - * The last 0x600 bytes are the tiles used. - * 256/8 = 32, 192/8 = 24 - * 32 * 24 = 0x300 - * The tiles are chosen based on the 16bit index of the tile. - * 0x300 * 2 = 0x600! - * - * CGearBackgrounds tilemap (when stored on B/W) employs some obfuscation. - * B/W obfuscates by adding 0xA0A0. - * The obfuscated number is then tweaked by adding 15*(i/17) - * To reverse, use a similar reverse calculation - * PSK files are basically raw game rips (obfuscated) - * CGB files are un-obfuscated / B2/W2. - * Due to B/W and B2/W2 using different obfuscation adds, PSK files are incompatible between the versions. - */ - - public readonly int[] ColorPalette; - public readonly Tile[] Tiles; - public readonly TileMap Map; - - public CGearBackground(ReadOnlySpan data) - { - if (data.Length != SIZE_CGB) - throw new ArgumentOutOfRangeException(nameof(data)); - - var dataTiles = data[..LengthTilePool]; - var dataColors = data.Slice(LengthTilePool, LengthColorData); - var dataArrange = data.Slice(OffsetTileMap, LengthTileMap); - - Tiles = ReadTiles(dataTiles); - ColorPalette = ReadColorPalette(dataColors); - Map = new TileMap(dataArrange); - - foreach (var tile in Tiles) - tile.SetTile(ColorPalette); - } - - private CGearBackground(int[] palette, Tile[] tilelist, TileMap tm) - { - Map = tm; - ColorPalette = palette; - Tiles = tilelist; - } - - /// - /// Writes the data to a binary form. - /// - /// Destination buffer to write the skin to - /// True if the destination game is , otherwise false for . - /// Serialized skin data for writing to the save file - public void Write(Span data, bool cgb) - { - var dataTiles = data[..LengthTilePool]; - var dataColors = data.Slice(LengthTilePool, LengthColorData); - var dataArrange = data.Slice(OffsetTileMap, LengthTileMap); - - WriteTiles(dataTiles, Tiles); - WriteColorPalette(dataColors, ColorPalette); - Map.Write(dataArrange, cgb); - } - - private static Tile[] ReadTiles(ReadOnlySpan data) - { - var result = new Tile[data.Length / Tile.SIZE_TILE]; - for (int i = 0; i < result.Length; i++) - { - var span = data.Slice(i * Tile.SIZE_TILE, Tile.SIZE_TILE); - result[i] = new Tile(span); - } - return result; - } - - private static void WriteTiles(Span data, ReadOnlySpan tiles) - { - for (int i = 0; i < tiles.Length; i++) - { - var tile = tiles[i]; - var span = data.Slice(i * Tile.SIZE_TILE, Tile.SIZE_TILE); - tile.Write(span); - } - } - - private static int[] ReadColorPalette(ReadOnlySpan data) - { - var colors = new int[data.Length / 2]; // u16->rgb32 - ReadColorPalette(data, colors); - return colors; - } - - private static void ReadColorPalette(ReadOnlySpan data, Span colors) - { - var buffer = MemoryMarshal.Cast(data)[..colors.Length]; - for (int i = 0; i < colors.Length; i++) - { - var value = buffer[i]; - if (!BitConverter.IsLittleEndian) - value = ReverseEndianness(value); - colors[i] = Color15Bit.GetColorExpand(value); - } - } - - private static void WriteColorPalette(Span data, ReadOnlySpan colors) - { - var buffer = MemoryMarshal.Cast(data)[..colors.Length]; - for (int i = 0; i < colors.Length; i++) - { - var value = Color15Bit.GetColorCompress(colors[i]); - if (!BitConverter.IsLittleEndian) - value = ReverseEndianness(value); - buffer[i] = value; - } - } - - /// - /// Creates a new C-Gear Background object from an input image data byte array, with 32 bits per pixel. - /// - /// Image data - /// new C-Gear Background object - public static CGearBackground GetBackground(ReadOnlySpan data) - { - const int bpp = 4; - const int expectLength = Width * Height * bpp; - ArgumentOutOfRangeException.ThrowIfNotEqual(data.Length, expectLength); - - var colors = GetColorData(data); - var palette = colors.Distinct().ToArray(); - if (palette.Length > ColorCount) - throw new ArgumentException($"Too many unique colors. Expected <= 16, got {palette.Length}"); - - var tiles = GetTiles(colors, palette); - GetTileList(tiles, out List tilelist, out TileMap tm); - if (tilelist.Count >= CountTilePool) - throw new ArgumentException($"Too many unique tiles. Expected < 256, received {tilelist.Count}."); - - // Finished! - return new CGearBackground(palette, [.. tilelist], tm); - } - - private static int[] GetColorData(ReadOnlySpan data) - { - var pixels = MemoryMarshal.Cast(data); - int[] colors = new int[pixels.Length]; - for (int i = 0; i < pixels.Length; i++) - { - var pixel = pixels[i]; - if (!BitConverter.IsLittleEndian) - pixel = ReverseEndianness(pixel); - colors[i] = Color15Bit.GetColorAsOpaque(pixel); - } - return colors; - } - - private static Tile[] GetTiles(ReadOnlySpan colors, ReadOnlySpan palette) - { - var tiles = new Tile[TileCount]; - for (int i = 0; i < tiles.Length; i++) - tiles[i] = GetTile(colors, palette, i); - return tiles; - } - - private static Tile GetTile(ReadOnlySpan colors, ReadOnlySpan palette, int tileIndex) - { - int x = (tileIndex * 8) % Width; - int y = 8 * ((tileIndex * 8) / Width); - - var t = new Tile(); - var choices = t.ColorChoices; - for (uint ix = 0; ix < 8; ix++) - { - for (uint iy = 0; iy < 8; iy++) - { - int index = ((int) (y + iy) * Width) + (int) (x + ix); - var c = colors[index]; - - choices[(ix % 8) + (iy * 8)] = (byte)palette.IndexOf(c); - } - } - - t.SetTile(palette); - return t; - } - - private static void GetTileList(ReadOnlySpan tiles, out List tilelist, out TileMap tm) - { - tilelist = [tiles[0]]; - tm = new TileMap(LengthTileMap); - - // start at 1 as the 0th tile is always non-duplicate - for (int i = 1; i < tm.TileChoices.Length; i++) - FindPossibleRotatedTile(tiles[i], tilelist, tm, i); - } - - private static void FindPossibleRotatedTile(Tile t, List tilelist, TileMap tm, int tileIndex) - { - // Test all tiles currently in the list - for (int i = 0; i < tilelist.Count; i++) - { - var rotVal = t.GetRotationValue(tilelist[i].ColorChoices); - if (rotVal == Tile.ROTATION_BAD) - continue; - tm.TileChoices[tileIndex] = (byte)i; - tm.Rotations[tileIndex] = rotVal; - return; - } - - // No tile found, add to list - tilelist.Add(t); - tm.TileChoices[tileIndex] = (byte)(tilelist.Count - 1); - tm.Rotations[tileIndex] = 0; - } - - public byte[] GetImageData() - { - byte[] data = new byte[4 * Width * Height]; - WriteImageData(data); - return data; - } - - private void WriteImageData(Span data) - { - var tiles = Map.TileChoices; - var rotations = Map.Rotations; - for (int i = 0; i < tiles.Length; i++) - { - var choice = tiles[i]; - var rotation = rotations[i]; - var tile = Tiles[choice]; - var tileData = tile.Rotate(rotation); - - int x = (i * TileSize) % Width; - int y = TileSize * ((i * TileSize) / Width); - - for (int row = 0; row < TileSize; row++) - { - const int pixelLineSize = TileSize * sizeof(int); - int ofsSrc = row * pixelLineSize; - int ofsDest = (((y + row) * Width) + x) * sizeof(int); - var line = tileData.Slice(ofsSrc, pixelLineSize); - line.CopyTo(data[ofsDest..]); - } - } - } -} - -/// -/// Generation 5 image tile composed of 8x8 pixels. -/// -/// Each pixel's color choice is a nibble (4 bits). -public sealed class Tile -{ - internal const int SIZE_TILE = 0x20; - private const int TileWidth = 8; - private const int TileHeight = 8; - internal readonly byte[] ColorChoices = new byte[TileWidth * TileHeight]; - - // Keep track of known rotations for this tile. - // If the tile's rotated value has not yet been calculated, the field is null. - private byte[] PixelData = []; - private byte[]? PixelDataX; - private byte[]? PixelDataY; - - private const byte FlagFlipX = 0b0100; // 0x4 - private const byte FlagFlipY = 0b1000; // 0x8 - private const byte FlagFlipXY = FlagFlipX | FlagFlipY; // 0xC - private const byte FlagFlipNone = 0b0000; // 0x0 - internal const byte ROTATION_BAD = byte.MaxValue; - - internal Tile() { } - - internal Tile(ReadOnlySpan data) : this() - { - ArgumentOutOfRangeException.ThrowIfNotEqual(data.Length, SIZE_TILE); - - // Unpack the nibbles into the color choice array. - for (int i = 0; i < data.Length; i++) - { - var value = data[i]; - var ofs = i * 2; - ColorChoices[ofs + 0] = (byte)(value & 0xF); - ColorChoices[ofs + 1] = (byte)(value >> 4); - } - } - - internal void SetTile(ReadOnlySpan palette) => PixelData = GetTileData(palette, ColorChoices); - - private static byte[] GetTileData(ReadOnlySpan Palette, ReadOnlySpan choices) - { - byte[] data = new byte[choices.Length * 4]; - SetTileData(data, Palette, choices); - return data; - } - - private static void SetTileData(Span result, ReadOnlySpan palette, ReadOnlySpan colorChoices) - { - for (int i = 0; i < colorChoices.Length; i++) - { - var choice = colorChoices[i]; - var value = palette[choice]; - var span = result.Slice(4 * i, 4); - WriteInt32LittleEndian(span, value); - } - } - - public void Write(Span data) => Write(data, ColorChoices); - - private static void Write(Span data, ReadOnlySpan colorChoices) - { - for (int i = 0; i < data.Length; i++) - { - var span = colorChoices.Slice(i * 2, 2); - var second = span[1] & 0xF; - var first = span[0] & 0xF; - data[i] = (byte)(first | (second << 4)); - } - } - - public ReadOnlySpan Rotate(int rotFlip) - { - if (rotFlip == 0) - return PixelData; - if ((rotFlip & FlagFlipXY) == FlagFlipXY) - return FlipY(PixelDataX ??= FlipX(PixelData, TileWidth), TileHeight); - if ((rotFlip & FlagFlipX) == FlagFlipX) - return PixelDataX ??= FlipX(PixelData, TileWidth); - if ((rotFlip & FlagFlipY) == FlagFlipY) - return PixelDataY ??= FlipY(PixelData, TileHeight); - return PixelData; - } - - private static byte[] FlipX(ReadOnlySpan data, [ConstantExpected(Min = 0)] int width, [ConstantExpected(Min = 4, Max = 4)] int bpp = 4) - { - byte[] result = new byte[data.Length]; - FlipX(data, result, width, bpp); - return result; - } - - private static byte[] FlipY(ReadOnlySpan data, [ConstantExpected(Min = 0)] int height, [ConstantExpected(Min = 4, Max = 4)] int bpp = 4) - { - byte[] result = new byte[data.Length]; - FlipY(data, result, height, bpp); - return result; - } - - private static void FlipX(ReadOnlySpan data, Span result, [ConstantExpected(Min = 0)] int width, [ConstantExpected(Min = 4, Max = 4)] int bpp) - { - int pixels = data.Length / bpp; - var resultInt = MemoryMarshal.Cast(result); - var dataInt = MemoryMarshal.Cast(data); - for (int i = 0; i < pixels; i++) - { - int x = i % width; - int y = i / width; - - x = width - x - 1; // flip x - - int dest = ((y * width) + x); - resultInt[dest] = dataInt[i]; - } - } - - private static void FlipY(ReadOnlySpan data, Span result, [ConstantExpected(Min = 0)] int height, [ConstantExpected(Min = 4, Max = 4)] int bpp) - { - int pixels = data.Length / bpp; - int width = pixels / height; - var resultInt = MemoryMarshal.Cast(result); - var dataInt = MemoryMarshal.Cast(data); - for (int i = 0; i < pixels; i++) - { - int x = i % width; - int y = i / width; - - y = height - y - 1; // flip y - - int dest = ((y * width) + x); - resultInt[dest] = dataInt[i]; - } - } - - internal byte GetRotationValue(ReadOnlySpan tileColors) - { - // Check all rotation types - if (tileColors.SequenceEqual(ColorChoices)) - return FlagFlipNone; - - if (IsMirrorX(tileColors)) - return FlagFlipX; - if (IsMirrorY(tileColors)) - return FlagFlipY; - if (IsMirrorXY(tileColors)) - return FlagFlipXY; - - return ROTATION_BAD; - } - - private bool IsMirrorX(ReadOnlySpan tileColors) - { - const int pixels = TileWidth * TileHeight; - for (int i = 0; i < pixels; i++) - { - var index = (7 - (i & 7)) + (8 * (i / 8)); - if (ColorChoices[index] != tileColors[i]) - return false; - } - - return true; - } - - private bool IsMirrorY(ReadOnlySpan tileColors) - { - const int pixels = TileWidth * TileHeight; - for (int i = 0; i < pixels; i++) - { - var index = pixels - (8 * (1 + (i / 8))) + (i & 7); - if (ColorChoices[index] != tileColors[i]) - return false; - } - - return true; - } - - private bool IsMirrorXY(ReadOnlySpan tileColors) - { - const int pixels = TileWidth * TileHeight; - for (int i = 0; i < pixels; i++) - { - var index = pixels - 1 - i; - if (ColorChoices[index] != tileColors[i]) - return false; - } - - return true; - } -} - -public sealed class TileMap(int length) -{ - public readonly byte[] TileChoices = new byte[length / 2]; - public readonly byte[] Rotations = new byte[length / 2]; - - internal TileMap(ReadOnlySpan data) : this(data.Length) => LoadData(data, TileChoices, Rotations); - - private static void LoadData(ReadOnlySpan data, Span tiles, Span rotations) - { - bool isCGB = IsCGB(data); - if (!isCGB) - LoadDataPSK(data, tiles, rotations); - else - LoadDataCGB(data, tiles, rotations); - } - - private static void LoadDataCGB(ReadOnlySpan data, Span tiles, Span rotations) - { - for (int i = 0; i < data.Length; i += 2) - { - var span = data.Slice(i, 2); - var tile = span[0]; - var rot = span[1]; - - tiles[i / 2] = tile; - rotations[i / 2] = rot; - } - } - - private static void LoadDataPSK(ReadOnlySpan data, Span tiles, Span rotations) - { - for (int i = 0; i < data.Length; i += 2) - { - var span = data.Slice(i, 2); - var value = ReadUInt16LittleEndian(span); - var (tile, rot) = DecomposeValuePSK(value); - tiles[i / 2] = tile; - rotations[i / 2] = rot; - } - } - - private static bool IsCGB(ReadOnlySpan data) - { - // check odd bytes for anything not rotation flag - for (int i = 0; i < data.Length; i += 2) - { - if ((data[i + 1] & ~0b1100) != 0) - return false; - } - return true; - } - - public void Write(Span data, bool cgb) => Write(data, TileChoices, Rotations, cgb); - - private static void Write(Span data, ReadOnlySpan choices, ReadOnlySpan rotations, bool cgb) - { - if (choices.Length != rotations.Length) - throw new ArgumentException($"length of {nameof(TileChoices)} and {nameof(Rotations)} must be equal"); - if (data.Length != choices.Length + rotations.Length) - throw new ArgumentException($"data length must be twice the length of the {nameof(TileMap)}"); - - if (!cgb) - WriteDataPSK(data, choices, rotations); - else - WriteDataCGB(data, choices, rotations); - } - - private static void WriteDataCGB(Span data, ReadOnlySpan choices, ReadOnlySpan rotations) - { - for (int i = 0; i < data.Length; i += 2) - { - var span = data.Slice(i, 2); - span[0] = choices[i / 2]; - span[1] = rotations[i / 2]; - } - } - - private static void WriteDataPSK(Span data, ReadOnlySpan choices, ReadOnlySpan rotations) - { - for (int i = 0; i < data.Length; i += 2) - { - var span = data.Slice(i, 2); - var tile = choices[i / 2]; - var rot = rotations[i / 2]; - int val = GetPSKValue(tile, rot); - WriteUInt16LittleEndian(span, (ushort)val); - } - } - - public static (byte Tile, byte Rotation) DecomposeValuePSK(ushort val) - { - ushort value = UnmapPSKValue(val); - byte tile = (byte)value; - byte rot = (byte)(value >> 8); - if (tile == CGearBackground.CountTilePool) // out of range? - tile = 0; - return (tile, rot); - } - - private static ushort UnmapPSKValue(ushort val) - { - var rot = val & 0xFC00; - var trunc = (val & 0x3FF); - if (trunc is < 0xA0 or > 0x280) - return (ushort)(rot | CGearBackground.CountTilePool); // default empty - return (ushort)(rot | ((val & 0x1F) + (17 * ((trunc - 0xA0) >> 5)))); - } - - public static ushort GetPSKValue(byte tile, byte rot) - { - if (tile == CGearBackground.CountTilePool) // out of range? - tile = 0; - - var result = ((rot & 0x0C) << 8) | ((15 * (tile / 17)) + tile + 0xA0) | 0xA000; - return (ushort)result; - } -} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackground.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackground.cs new file mode 100644 index 000000000..e0ccebbee --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackground.cs @@ -0,0 +1,50 @@ +using System; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +/// +/// Lower screen tiled image data for the C-Gear. +/// +/// Data buffer storing the skin. +/// Image tiles, color palette, and tile arrangement map. +public abstract class CGearBackground(Memory Raw) : ITiledImage +{ + /// Pixel width of the background image. + public const int Width = 256; // px + /// Pixel height of the background image. + public const int Height = 192; // px + + /// Byte size of the C-Gear background image. + public const int SIZE = OffsetTileMap + LengthTileMap; // 0x2600 + + private const int ColorCount = 0x10; + private const int TilePoolCount = 0xFF; + + private const int TileSize = 8; + private const int TileCount = (Width / TileSize) * (Height / TileSize); // 0x300 + + private const int LengthTilePool = TilePoolCount * PaletteTile.SIZE; // 0x1FE0 + private const int LengthColorData = ColorCount * sizeof(ushort); // 0x20 + private const int OffsetTileMap = LengthTilePool + LengthColorData; // 0x2000 + private const int LengthTileMap = TileCount * sizeof(ushort); // 0x600 + + static int ITiledImage.Width => Width; + static int ITiledImage.Height => Height; + static int ITiledImage.PixelCount => Height * Width; + static int ITiledImage.ColorCount => ColorCount; + static int ITiledImage.TilePoolCount => TilePoolCount; + static int ITiledImage.TileSize => TileSize; + + public Span Data => Raw.Span; + + /* 0x0000 - 0x1FDF*/ public Span Tiles => Data[..LengthTilePool]; + /* 0x1FE0 - 0x1FFF*/ public Span Colors => Data.Slice(LengthTilePool, LengthColorData); + /* 0x2000 - 0x25FF*/ public Span Arrange => Data.Slice(OffsetTileMap, LengthTileMap); + + public Span GetTileData(int tile) => Tiles.Slice(tile * PaletteTile.SIZE, PaletteTile.SIZE); + public Span GetColorData(int color) => Colors.Slice(color * sizeof(ushort), sizeof(ushort)); + + public ushort GetArrange(int tilePositionIndex) => ReadUInt16LittleEndian(Arrange[(tilePositionIndex * 2)..]); + public void SetArrange(int tilePositionIndex, ushort value) => WriteUInt16LittleEndian(Arrange[(tilePositionIndex * 2)..], value); +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackgroundB2W2.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackgroundB2W2.cs new file mode 100644 index 000000000..0296c7073 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackgroundB2W2.cs @@ -0,0 +1,14 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Generation 5 C-Gear Background Image, tile positions formatted for . +/// +public sealed class CGearBackgroundB2W2(Memory Raw) : CGearBackground(Raw) +{ + /// + /// Standard format, no oddities compared to the format for B/W. + /// + public const string Extension = "cgb"; +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackgroundBW.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackgroundBW.cs new file mode 100644 index 000000000..0cdcffd05 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/CGearBackgroundBW.cs @@ -0,0 +1,34 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Generation 5 C-Gear Background Image, tile positions formatted for . +/// +public sealed class CGearBackgroundBW(Memory Raw) : CGearBackground(Raw) +{ + /// + /// Originally supported by PokeStock, a PokeSkin file. + /// + public const string Extension = "psk"; + + // Tiles in RAM are stored in a 15x17 grid starting at position 160 (0xA0). + private const int TileBWStart = 16 * 10; // 0xA0 + private const int TileBWWidth = 17; + private const int TileBWHeight = 15; + // Could this have been an off-by-one when trying to iterate over 16x16? Who knows. /% 32 works to reverse it. + + /// Gets the 0-based index of the tile from an offset layout index. + public static ushort GetVisualIndex(int tile) + { + if (tile < TileBWStart) + return 0; // force special tiles to be represented by the first tile. + var result = (ushort)((TileBWWidth * ((tile - TileBWStart) / 32)) + (tile % 32)); + if (result >= 300) + return 0; + return result; + } + + /// Gets the offset layout index of the tile from a 0-based index. + public static ushort GetLayoutIndex(int tile) => (ushort)((TileBWHeight * (tile / TileBWWidth)) + tile + TileBWStart); +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/ITiledImage.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/ITiledImage.cs new file mode 100644 index 000000000..7b3e2e432 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/ITiledImage.cs @@ -0,0 +1,32 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Tiled Image interface for Generation 5 C-Gear Backgrounds and PokéDex Skins. +/// +public interface ITiledImage +{ + /// Section of the data buffer storing all the tiles. + Span Tiles { get; } + /// Section of the data buffer storing the color palette. + Span Colors { get; } + /// Section of the data buffer storing the tile arrangement map. + Span Arrange { get; } + + /// Get the tile color choice data for a specific tile index. + Span GetTileData(int tile); + + /// Count of pixels in the image. + static abstract int PixelCount { get; } + /// Count of unique colors in the image. + static abstract int ColorCount { get; } + /// Count of tiles in the tile pool. + static abstract int TilePoolCount { get; } + /// Count of pixels for a side (width/height) of a tile. + static abstract int TileSize { get; } + /// Pixel width of the image. + static abstract int Width { get; } + /// Pixel height of the image. + static abstract int Height { get; } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteColorSet.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteColorSet.cs new file mode 100644 index 000000000..273d54a35 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteColorSet.cs @@ -0,0 +1,62 @@ +using System; +using System.Runtime.InteropServices; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +/// +/// Logic for interacting with a palette color set. +/// +/// +public static class PaletteColorSet +{ + /// + /// Converts a 32-bit ARGB pixel list to a 15-bit color. + /// + public static int GetUniqueColorsFromPixelData(ReadOnlySpan data, Span result) + { + int count = 0; + var pixels = MemoryMarshal.Cast(data); + foreach (var p in pixels) + { + var pixel = p; + if (!BitConverter.IsLittleEndian) + pixel = ReverseEndianness(pixel); + + // Don't support transparency in pixels + pixel = Color15Bit.GetColorAsOpaque(pixel); + + int index = result[..count].IndexOf(pixel); + if (index != -1) + continue; // already exists + if (count == result.Length) + continue; // too many unique colors + result[count++] = pixel; + } + return count; + } + + public static void Read(ReadOnlySpan data, Span colors) + { + var buffer = MemoryMarshal.Cast(data)[..colors.Length]; + for (int i = 0; i < colors.Length; i++) + { + var value = buffer[i]; + if (!BitConverter.IsLittleEndian) + value = ReverseEndianness(value); + colors[i] = Color15Bit.GetColorExpand(value); + } + } + + public static void Write(ReadOnlySpan colors, Span data) + { + var buffer = MemoryMarshal.Cast(data)[..colors.Length]; + for (int i = 0; i < colors.Length; i++) + { + var value = Color15Bit.GetColorCompress(colors[i]); + if (!BitConverter.IsLittleEndian) + value = ReverseEndianness(value); + buffer[i] = value; + } + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTile.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTile.cs new file mode 100644 index 000000000..2f9e451c4 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTile.cs @@ -0,0 +1,68 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Generation 5 image tile composed of 8x8 pixels. +/// +/// Each pixel's color choice is a nibble (4 bits) from a color palette. +/// +public static class PaletteTile +{ + internal const int SIZE = (TileWidth * TileHeight) / 2; // 0x20 + private const int TileSize = 8; + private const int TileWidth = TileSize; + private const int TileHeight = TileSize; + + public static PaletteTileRotation GetRotationValue(ReadOnlySpan tileColors, ReadOnlySpan self) + { + // Check all rotation types + if (tileColors.SequenceEqual(self)) + return PaletteTileRotation.None; + + if (IsMirrorX(tileColors, self)) + return PaletteTileRotation.FlipX; + if (IsMirrorY(tileColors, self)) + return PaletteTileRotation.FlipY; + if (IsMirrorXY(tileColors, self)) + return PaletteTileRotation.FlipXY; + + return PaletteTileRotation.Invalid; + } + + private static bool IsMirrorX(ReadOnlySpan tileColors, ReadOnlySpan self) + { + const int pixels = TileWidth * TileHeight; + for (int i = 0; i < pixels; i++) + { + var index = (7 - (i & 7)) + (8 * (i / 8)); + if (self[index] != tileColors[i]) + return false; + } + return true; + } + + private static bool IsMirrorY(ReadOnlySpan tileColors, ReadOnlySpan self) + { + const int pixels = TileWidth * TileHeight; + for (int i = 0; i < pixels; i++) + { + var index = pixels - (8 * (1 + (i / 8))) + (i & 7); + if (self[index] != tileColors[i]) + return false; + } + return true; + } + + private static bool IsMirrorXY(ReadOnlySpan tileColors, ReadOnlySpan self) + { + const int pixels = TileWidth * TileHeight; + for (int i = 0; i < pixels; i++) + { + var index = pixels - 1 - i; + if (self[index] != tileColors[i]) + return false; + } + return true; + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileOperations.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileOperations.cs new file mode 100644 index 000000000..cf0e44575 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileOperations.cs @@ -0,0 +1,163 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +/// +/// 32-bit ARGB pixel converter for interacting with square pixel tiles. +/// +/// +public static class PaletteTileOperations +{ + /// + /// Reads the color choices from the into the . + /// + public static void SplitNibbles(ReadOnlySpan data, Span colorChoices) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(data.Length * 2, colorChoices.Length); + for (int i = 0; i < data.Length; i++) + { + var value = data[i]; + var span = colorChoices.Slice(i * 2, 2); + span[1] = (byte)(value >> 4); + span[0] = (byte)(value & 0xF); + } + } + + /// + /// Writes the into the . + /// + public static void MergeNibbles(ReadOnlySpan colorChoices, Span data) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(data.Length * 2, colorChoices.Length); + for (int i = 0; i < data.Length; i++) + { + var span = colorChoices.Slice(i * 2, 2); + var second = span[1] & 0xF; + var first = span[0] & 0xF; + data[i] = (byte)(first | (second << 4)); + } + } + + /// + /// Inflates the into the using the . + /// + /// Color choices for each pixel + /// 32-bit ARGB palette + /// Resulting pixel data + public static void Inflate(ReadOnlySpan colorChoices, ReadOnlySpan palette, Span result) + { + for (int i = 0; i < colorChoices.Length; i++) + { + var choice = colorChoices[i]; + var value = palette[choice]; + var span = result.Slice(4 * i, 4); + WriteInt32LittleEndian(span, value); + } + } + + /// + /// Deflates the into the using the . + /// + /// Resulting pixel data + /// 32-bit ARGB palette + /// Color choices for each pixel + public static void Deflate(ReadOnlySpan pixels, ReadOnlySpan palette, Span tileColors) + { + for (int i = 0; i < pixels.Length; i++) + { + var pixel = pixels[i]; + if (!BitConverter.IsLittleEndian) + pixel = ReverseEndianness(pixel); + var color = Color15Bit.GetColorAsOpaque(pixel); + tileColors[i] = (byte)palette.IndexOf(color); + } + } + + /// + /// Flips the pixel data across the X axis. + /// + public static void FlipX(ReadOnlySpan data, Span result, int width, [ConstantExpected(Min = 4, Max = 4)] int bpp) + { + int pixels = data.Length / bpp; + var resultInt = MemoryMarshal.Cast(result); + var dataInt = MemoryMarshal.Cast(data); + for (int i = 0; i < pixels; i++) + { + int x = i % width; + int y = i / width; + + x = width - x - 1; // flip x + + int dest = ((y * width) + x); + resultInt[dest] = dataInt[i]; + } + } + + /// + /// Flips the pixel data across the Y axis. + /// + public static void FlipY(ReadOnlySpan data, Span result, int height, [ConstantExpected(Min = 4, Max = 4)] int bpp) + { + int pixels = data.Length / bpp; + int width = pixels / height; + var resultInt = MemoryMarshal.Cast(result); + var dataInt = MemoryMarshal.Cast(data); + for (int i = 0; i < pixels; i++) + { + int x = i % width; + int y = i / width; + + y = height - y - 1; // flip y + + int dest = ((y * width) + x); + resultInt[dest] = dataInt[i]; + } + } + + /// + /// Flips the pixel data across both the X and Y axis. + /// + public static void FlipXY(ReadOnlySpan data, Span result, int height, [ConstantExpected(Min = 4, Max = 4)] int bpp) + { + int pixels = data.Length / bpp; + int width = pixels / height; + var resultInt = MemoryMarshal.Cast(result); + var dataInt = MemoryMarshal.Cast(data); + for (int i = 0; i < pixels; i++) + { + int x = i % width; + int y = i / width; + + x = width - x - 1; // flip x + y = height - y - 1; // flip y + + int dest = ((y * width) + x); + resultInt[dest] = dataInt[i]; + } + } + + /// + /// Flips the 32-bit pixel data across the requested . + /// + /// If no rotation is requested, the data is only copied. + /// Unpacked (32-bit pixel) tile data to process + /// Resulting tile data + /// Size of the tile (width/height) + /// Rotation type to apply + /// Bits per pixel, requires 4. + public static void Flip(ReadOnlySpan tile, Span result, int tileSize, PaletteTileRotation rot, + [ConstantExpected(Min = 4, Max = 4)] int bpp) + { + if (rot == PaletteTileRotation.FlipXY) + FlipXY(tile, result, tileSize, bpp); + else if (rot.HasFlag(PaletteTileRotation.FlipX)) + FlipX(tile, result, tileSize, bpp); + else if (rot.HasFlag(PaletteTileRotation.FlipY)) + FlipY(tile, result, tileSize, bpp); + else + tile.CopyTo(result); + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileRotation.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileRotation.cs new file mode 100644 index 000000000..3f046188e --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileRotation.cs @@ -0,0 +1,26 @@ +using System; + +namespace PKHeX.Core; + +/// +/// Rotation flags for a palette tile. +/// +/// +[Flags] +public enum PaletteTileRotation +{ + /// No rotation. + None = 0, + + /// Flip the tile horizontally. + FlipX = 1 << 0, + + /// Flip the tile vertically. + FlipY = 1 << 1, + + /// Flip the tile horizontally and vertically. + FlipXY = FlipX | FlipY, + + /// Invalid rotation. + Invalid = 1 << 2, +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileSelection.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileSelection.cs new file mode 100644 index 000000000..3abf941cc --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/PaletteTileSelection.cs @@ -0,0 +1,149 @@ +using System; +using System.Runtime.InteropServices; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core; + +/// +/// Logic for reading and writing the palette tile selection format. +/// +/// +/// The structure is a 16-bit packed value with the following format: +/// +/// 6 bits tile index +/// 2 bits flip y|x +/// 4 bits palette type +/// +/// +public static class PaletteTileSelection +{ + /// + /// Breaks down the selection value into its components. + /// + public static void DecomposeSelection(ushort value, out ushort choice, out byte flip, out byte palette) + { + choice = (ushort)(value & 0x3FF); + flip = (byte)((value >> 10) & 0b11); + palette = (byte)((value >> 12) & 0xF); + } + + /// + /// Combines the components into a selection value. + /// + public static ushort ComposeSelection(ushort choice, byte flip, byte palette) + { + return (ushort)((palette << 12) | ((flip & 3) << 10) | (choice & 0x3FF)); + } + + /// + /// Checks if any of the selection values are in the shifted tile arrangement format. + /// + public static bool IsPaletteShiftFormat(ReadOnlySpan data) + { + var u16 = MemoryMarshal.Cast(data); + foreach (ushort v in u16) + { + ushort value = v; + if (!BitConverter.IsLittleEndian) + value = ReverseEndianness(value); + DecomposeSelection(value, out var choice, out _, out var palette); + if (palette != 0) + return true; + if (choice > 0xFF) + return true; + } + return false; + } + + /// + /// Converts the selection values to the shifted tile arrangement format. + /// + /// Used to convert from B2/W2 to B/W. + public static void ConvertToShiftFormat(Span data) where T : ITiledImage + { + var u16 = MemoryMarshal.Cast(data); + for (int i = 0; i < u16.Length; i++) + { + ushort value = u16[i]; + if (!BitConverter.IsLittleEndian) + value = ReverseEndianness(value); + DecomposeSelection(value, out var choice, out var flip, out var palette); + if (palette != 0) + throw new ArgumentException("Palette shift format already present."); + if (choice >= T.TilePoolCount) + throw new ArgumentException("Choice value too large for shift format."); + + choice = CGearBackgroundBW.GetLayoutIndex(choice); + u16[i] = ComposeSelection(choice, flip, 10); + } + } + + /// + /// Converts the selection values from the shifted tile arrangement format. + /// + /// Used to convert from B/W to B2/W2. + public static void ConvertFromShiftFormat(Span data) + { + var u16 = MemoryMarshal.Cast(data); + for (int i = 0; i < u16.Length; i++) + { + ushort value = u16[i]; + if (!BitConverter.IsLittleEndian) + value = ReverseEndianness(value); + DecomposeSelection(value, out var choice, out var flip, out _); + choice = CGearBackgroundBW.GetVisualIndex(choice); + u16[i] = ComposeSelection(choice, flip, 0); + } + } + + /// + /// Reads all the layout details from the arrangement list. + /// + /// Buffer containing the arrangement list. + /// Result storage for tile choice at index i + /// Result storage for flip type at index i + /// Result storage for palette type at index i + public static void ReadLayoutDetails(ReadOnlySpan data, Span choice, Span flip, Span palette) + { + var u16 = MemoryMarshal.Cast(data); + for (int i = 0; i < choice.Length; i++) + { + var val = u16[i]; + if (!BitConverter.IsLittleEndian) + val = ReverseEndianness(val); + DecomposeSelection(val, out choice[i], out flip[i], out palette[i]); + } + } + + /// + /// Writes all the layout details to the arrangement list. + /// + /// Buffer containing the arrangement list. + /// Input storage for tile choice at index i + /// Input storage for flip type at index i + /// Input storage for palette type at index i + public static void WriteLayoutDetails(Span data, ReadOnlySpan choice, ReadOnlySpan flip, ReadOnlySpan palette) + { + var u16 = MemoryMarshal.Cast(data); + for (int i = 0; i < choice.Length; i++) + { + var val = ComposeSelection(choice[i], flip[i], palette[i]); + if (!BitConverter.IsLittleEndian) + val = ReverseEndianness(val); + u16[i] = val; + } + } + + /// + public static void WriteLayoutDetails(Span data, ReadOnlySpan choice, ReadOnlySpan flip) + { + var u16 = MemoryMarshal.Cast(data); + for (int i = 0; i < choice.Length; i++) + { + var val = ComposeSelection(choice[i], flip[i], 0); + if (!BitConverter.IsLittleEndian) + val = ReverseEndianness(val); + u16[i] = val; + } + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/TiledImageConverter.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/TiledImageConverter.cs new file mode 100644 index 000000000..71402ceb4 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/TiledImageConverter.cs @@ -0,0 +1,201 @@ +using System; +using System.Runtime.InteropServices; + +namespace PKHeX.Core; + +/// +/// Conversion logic for objects. +/// +/// Assumes 32-bit ARGB pixel data. +public static class TiledImageConverter +{ + private const int bpp = 4; // 32-bit ARGB + + /// + /// Allocates a new byte array and fills it with the image data. + /// + /// Input to create image pixel data for + public static byte[] GetImageData(this T bg) + where T : ITiledImage + { + var result = new byte[T.PixelCount * bpp]; + GetImageData(bg, result); + return result; + } + + /// + /// Fills the buffer with the image data. + /// + /// Input to create image pixel data for + /// Pixel data buffer to fill + /// + public static void GetImageData(this T bg, Span result) + where T : ITiledImage + { + if (result.Length < T.PixelCount * bpp) // debug assertion + throw new ArgumentException($"Result buffer must be {T.PixelCount * bpp} bytes long."); + + // Get color palette + Span palette = stackalloc int[T.ColorCount]; + PaletteColorSet.Read(bg.Colors, palette); + + // Get tile choices + var arrange = bg.Arrange; + Span choiceTil = stackalloc ushort[arrange.Length / 2]; + Span choiceRot = stackalloc byte[arrange.Length / 2]; + Span choicePal = stackalloc byte[arrange.Length / 2]; + PaletteTileSelection.ReadLayoutDetails(arrange, choiceTil, choiceRot, choicePal); + if (bg is CGearBackgroundBW) + { + if (!choiceTil.ContainsAnyExcept(0, 0x3FF)) + return; // no tile data to render. + foreach (ref var value in choiceTil) + value = CGearBackgroundBW.GetVisualIndex(value); + } + + // Allocate a temp buffer to store the tile w/ palette + PlaceTiles(bg, result, choiceTil, choiceRot, palette); + } + + private static void PlaceTiles(T bg, Span result, Span choiceTil, Span choiceRot, Span palette) + where T : ITiledImage + { + // Allocate a working buffer to store the tile data + Span colors = stackalloc byte[PaletteTile.SIZE * 2]; // inflated, not nibbles. one pixel per index + Span tileData = stackalloc byte[colors.Length * bpp]; + Span pixelData = stackalloc byte[colors.Length * bpp]; + + for (int i = 0; i < choiceTil.Length; i++) + { + var tile = bg.GetTileData(choiceTil[i]); + var rot = (PaletteTileRotation)choiceRot[i]; + PaletteTileOperations.SplitNibbles(tile, colors); + PaletteTileOperations.Inflate(colors, palette, pixelData); + PaletteTileOperations.Flip(pixelData, tileData, T.TileSize, rot, bpp); + + // Drop the tile into the result buffer at the needed coordinate. + WriteTile(result, i, tileData); + } + } + + private static void WriteTile(Span result, int index, Span tileData) + where T : ITiledImage + { + int pixelLineSize = T.TileSize * bpp; + int x = (index * T.TileSize) % T.Width; + int y = T.TileSize * ((index * T.TileSize) / T.Width); + for (int row = 0; row < T.TileSize; row++) + { + int ofsSrc = row * pixelLineSize; + int ofsDest = (((y + row) * T.Width) + x) * bpp; + var line = tileData.Slice(ofsSrc, pixelLineSize); + line.CopyTo(result[ofsDest..]); + } + } + + /// + /// Sets the image data from the buffer, converting into the background tile object. + /// + /// Result to update with the image data + /// Pixel data buffer to read + /// + public static TiledImageStat SetImageData(this T bg, ReadOnlySpan data) + where T : ITiledImage + { + if (data.Length < T.PixelCount * bpp) // debug assertion + throw new ArgumentException($"Data buffer must be {T.PixelCount * bpp} bytes long."); + + // Get color palette + Span palette = stackalloc int[T.ColorCount]; + int colorCount = PaletteColorSet.GetUniqueColorsFromPixelData(data, palette); + if (colorCount < palette.Length) // If too many colors, don't trim. Only the result will indicate. + palette = palette[..colorCount]; + + // Get the tiles + var arrange = bg.Arrange; + Span choiceTil = stackalloc ushort[arrange.Length / 2]; + Span choiceRot = stackalloc byte[arrange.Length / 2]; + + // Read tile data from the image; for each new tile, add it to the list. + // Store temporary tile list as color choices, and expand the window as we add new tiles. + int tileCount = 0; + Span tileColors = stackalloc byte[T.TileSize * T.TileSize]; + var maxTiles = bg.Tiles.Length / (tileColors.Length / 2); + Span rawTiles = stackalloc byte[tileColors.Length * maxTiles]; + for (int tileIndex = 0; tileIndex < choiceTil.Length; tileIndex++) + { + ReadTile(data, tileIndex, tileColors, palette); + + // Search the existing tiles for a match + var allTiles = rawTiles[..(tileCount * tileColors.Length)]; + bool match = TryFindRotation(tileColors, allTiles, out var index, out var rot); + if (!match) + { + // Add the new tile to the list + if (tileCount < maxTiles) + { + tileColors.CopyTo(rawTiles[(tileCount * tileColors.Length)..]); + index = tileCount++; + } + else + { + // Impossible to store, so just keep default values and increment the total count of the image. + tileCount++; + } + } + + // Set the tile index and rotation + choiceTil[tileIndex] = (ushort)index; + choiceRot[tileIndex] = (byte)rot; + } + + // Set the data + bg.Colors.Clear(); + PaletteColorSet.Write(palette, bg.Colors); + PaletteTileOperations.MergeNibbles(rawTiles, bg.Tiles); + PaletteTileSelection.WriteLayoutDetails(arrange, choiceTil, choiceRot); + + if (bg is CGearBackgroundBW) + PaletteTileSelection.ConvertToShiftFormat(bg.Arrange); + + return new TiledImageStat { TileCount = tileCount, ColorCount = colorCount }; + } + + private static bool TryFindRotation(ReadOnlySpan tileColors, ReadOnlySpan allTiles, out int index, out PaletteTileRotation rot) + { + for (int i = 0; i < allTiles.Length; i += tileColors.Length) + { + var tile = allTiles.Slice(i, tileColors.Length); + var check = PaletteTile.GetRotationValue(tileColors, tile); + if (check == PaletteTileRotation.Invalid) + continue; + index = i / tileColors.Length; + rot = check; + return true; + } + index = 0; + rot = 0; + return false; + } + + private static void ReadTile(ReadOnlySpan data, int i, Span tileColors, Span colorChoices) + where T : ITiledImage + { + var tSize = T.TileSize; + int baseX = (i * tSize) % T.Width; + int baseY = tSize * ((i * tSize) / T.Width); + // Inflate to pixel offset + int ofsSrc = ((baseY * T.Width) + baseX) * bpp; + + var length = tSize * bpp; + for (int y = 0; y < tSize; y++) + { + int ofsDest = ofsSrc + (y * T.Width * bpp); + var tileRow = data.Slice(ofsDest, length); + var i32 = MemoryMarshal.Cast(tileRow); + + var slice = tileColors.Slice(y * tSize, T.TileSize); + PaletteTileOperations.Deflate(i32, colorChoices, slice); + } + } +} diff --git a/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/TiledImageStat.cs b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/TiledImageStat.cs new file mode 100644 index 000000000..0f4993965 --- /dev/null +++ b/PKHeX.Core/Saves/Substructures/Gen5/TiledImage/TiledImageStat.cs @@ -0,0 +1,69 @@ +using System.Runtime.InteropServices; +using System; + +namespace PKHeX.Core; + +/// +/// Simple record to store the unique tile and color count of a . +/// +/// Count of unique tiles in the data +/// Count of unique colors in the data +public readonly record struct TiledImageStat(int TileCount, int ColorCount); + +/// +/// Logic to calculate a from an . +/// +public static class TiledImageUtil +{ + /// + /// Get the unique tile and color count of the . + /// + /// Useful if you want to inspect a tiled image directly without rebuilding between image file. + public static TiledImageStat GetStats(this ITiledImage img) + { + var tileCount = GetUniqueTileCount(img.Tiles); + var colorCount = GetUniqueColorCount(img.Colors); + return new TiledImageStat(tileCount, colorCount); + } + + private static int GetUniqueColorCount(ReadOnlySpan imgColors) + { + var u16 = MemoryMarshal.Cast(imgColors); + int count = 1; // first color is always unique + for (int i = 1; i < u16.Length; i++) + { + var color = u16[i]; + if (!u16[..i].Contains(color)) + count++; + } + + return count; + } + + private static int GetUniqueTileCount(ReadOnlySpan data) + { + // Get count of unique tiles, based on unique sequences of bytes + int count = 1; // first tile is always unique + const int length = PaletteTile.SIZE; + for (int i = length; i < data.Length; i += length) + { + var tile = data.Slice(i, length); + if (IsUniqueTile(data[..i], tile)) + count++; + } + + return count; + } + + private static bool IsUniqueTile(ReadOnlySpan otherTiles, ReadOnlySpan tile) + { + const int length = PaletteTile.SIZE; + for (int i = 0; i < otherTiles.Length; i += length) + { + var other = otherTiles.Slice(i, length); + if (tile.SequenceEqual(other)) + return false; + } + return true; + } +} diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen5/CGearImage.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen5/CGearImage.cs index dbd6cf8c2..19270f6d0 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen5/CGearImage.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen5/CGearImage.cs @@ -28,7 +28,7 @@ public static Bitmap GetBitmap(CGearBackground bg) /// Converts a to a . /// /// - public static CGearBackground GetCGearBackground(Bitmap img) + public static TiledImageStat GetCGearBackground(Bitmap img, CGearBackground bg) { ArgumentOutOfRangeException.ThrowIfNotEqual(img.Width, Width); ArgumentOutOfRangeException.ThrowIfNotEqual(img.Height, Height); @@ -39,6 +39,6 @@ public static CGearBackground GetCGearBackground(Bitmap img) const int bpp = 4; Debug.Assert(data.Length == Width * Height * bpp); - return CGearBackground.GetBackground(data); + return bg.SetImageData(data); } } diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_CGearSkin.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_CGearSkin.cs index 529e7cd09..2f305cf8b 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_CGearSkin.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_CGearSkin.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; @@ -9,23 +10,23 @@ namespace PKHeX.WinForms; public partial class SAV_CGearSkin : Form { - private readonly SaveFile Origin; private readonly SAV5 SAV; + private readonly CGearBackground bg; + private const string FilterBW = $"PokeStock C-Gear Skin Background|*.{CGearBackgroundBW.Extension}"; + private const string FilterB2W2 = $"C-Gear Background|*.{CGearBackgroundB2W2.Extension}"; public SAV_CGearSkin(SAV5 sav) { InitializeComponent(); WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage); - SAV = (SAV5)(Origin = sav).Clone(); + SAV = sav; - byte[] data = SAV.CGearSkinData; - bg = new CGearBackground(data); + var data = SAV.CGearSkinData.ToArray(); + bg = sav is SAV5BW ? new CGearBackgroundBW(data) : new CGearBackgroundB2W2(data); PB_Background.Image = CGearImage.GetBitmap(bg); } - private CGearBackground bg; - private void B_ImportPNG_Click(object sender, EventArgs e) { using var ofd = new OpenFileDialog(); @@ -34,11 +35,20 @@ private void B_ImportPNG_Click(object sender, EventArgs e) if (ofd.ShowDialog() != DialogResult.OK) return; - using var img = (Bitmap)Image.FromFile(ofd.FileName); try { - bg = CGearImage.GetCGearBackground(img); - PB_Background.Image = CGearImage.GetBitmap(bg); + using var img = (Bitmap)Image.FromFile(ofd.FileName); + if (!IsInputCorrect(img, out var msg)) + { + WinFormsUtil.Alert(msg); + return; + } + var result = CGearImage.GetCGearBackground(img, bg); + PB_Background.Image = CGearImage.GetBitmap(bg); // regenerate rather than reuse input + if (CheckResult(result, out msg)) + System.Media.SystemSounds.Asterisk.Play(); + else + WinFormsUtil.Alert(msg); } catch (Exception ex) { @@ -46,6 +56,41 @@ private void B_ImportPNG_Click(object sender, EventArgs e) } } + private static bool IsInputCorrect(Bitmap img, [NotNullWhen(false)] out string? msg) where T : ITiledImage + { + if (img.Width != T.Width || img.Height != T.Height) + { + msg = $"Incorrect image dimensions. Expected {T.Width}x{T.Height}"; + return false; + } + if (img.PixelFormat != PixelFormat.Format32bppArgb) + { + msg = $"Incorrect image format. Expected {PixelFormat.Format32bppArgb}"; + return false; + } + msg = null; + return true; + } + + private static bool CheckResult(TiledImageStat result, [NotNullWhen(false)] out string? msg) + where T : ITiledImage + { + bool tooManyColors = result.ColorCount > T.ColorCount; + bool tooManyTiles = result.TileCount > T.TilePoolCount; + if (!tooManyColors && !tooManyTiles) + { + msg = null; + return true; // Success + } + + msg = ""; + if (tooManyColors) + msg += $"Too many colors. Expected: {T.ColorCount}, received {result.ColorCount}"; + if (tooManyTiles) + msg += (msg.Length != 0 ? Environment.NewLine : "") + $"Too many tiles. Expected {T.TilePoolCount}, received {result.TileCount}"; + return false; + } + private void B_ExportPNG_Click(object sender, EventArgs e) { using var sfd = new SaveFileDialog(); @@ -60,47 +105,57 @@ private void B_ExportPNG_Click(object sender, EventArgs e) private void B_ImportCGB_Click(object sender, EventArgs e) { using var ofd = new OpenFileDialog(); - ofd.Filter = CGearBackground.Filter + "|PokeStock C-Gear Skin|*.psk"; + bool isBW = SAV is SAV5BW; + ofd.Filter = isBW ? $"{FilterBW}|{FilterB2W2}" : $"{FilterB2W2}|{FilterBW}"; if (ofd.ShowDialog() != DialogResult.OK) return; var path = ofd.FileName; var len = new FileInfo(path).Length; - if (len != CGearBackground.SIZE_CGB) + if (len != CGearBackground.SIZE) { - WinFormsUtil.Error($"Incorrect size, got {len} bytes, expected {CGearBackground.SIZE_CGB} bytes."); + WinFormsUtil.Error($"Incorrect size, got {len} bytes, expected {CGearBackground.SIZE} bytes."); return; } byte[] data = File.ReadAllBytes(path); - bg = new CGearBackground(data); + + // Load the data and adjust it to the correct game format if not matching. + CGearBackground temp = isBW ? new CGearBackgroundBW(data) : new CGearBackgroundB2W2(data); + bool isPSK = PaletteTileSelection.IsPaletteShiftFormat(temp.Arrange); + + try + { + if (isBW && !isPSK) + PaletteTileSelection.ConvertToShiftFormat(temp.Arrange); + else if (!isBW && isPSK) + PaletteTileSelection.ConvertFromShiftFormat(temp.Arrange); + } + catch (Exception ex) + { + WinFormsUtil.Error(ex.Message); + return; + } + + temp.Data.CopyTo(bg.Data); PB_Background.Image = CGearImage.GetBitmap(bg); } private void B_ExportCGB_Click(object sender, EventArgs e) { using var sfd = new SaveFileDialog(); - sfd.Filter = CGearBackground.Filter; + sfd.Filter = SAV is SAV5BW ? FilterBW : FilterB2W2; if (sfd.ShowDialog() != DialogResult.OK) return; - - var data = new byte[CGearBackground.SIZE_CGB]; - bg.Write(data, true); - File.WriteAllBytes(sfd.FileName, data); + File.WriteAllBytes(sfd.FileName, bg.Data.ToArray()); } private void B_Save_Click(object sender, EventArgs e) { - var data = new byte[CGearBackground.SIZE_CGB]; - bool cgb = SAV is SAV5B2W2; - bg.Write(data, cgb); - if (data.AsSpan().ContainsAnyExcept(0)) - { - SAV.CGearSkinData = data; - Origin.CopyChangesFrom(SAV); - } + if (bg.Data.ContainsAnyExcept(0)) + SAV.SetCGearSkin(bg.Data); Close(); }