Rewrite C-Gear Skin conversion code (#4351)

#4337

On form open, sav->cgb->png
On file import, data->cgb->png
On image import, img->cgb->png
On form save, cgb->sav

Original data is retained when opening/importing file, rather than rebuilding png->cgb every time on form save.

Changes:
- can now adapt logic easily for pokedex skins
- importing image with too many colors/tiles will instead use color/tile 0 instead of throwing an exception, then will alert after importing.
- importing image with wrong dimensions/format will alert rather than display exception message
This commit is contained in:
Kurt 2024-08-28 19:04:25 -05:00 committed by GitHub
parent ba7646e7a2
commit 0e4d30fd21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 969 additions and 642 deletions

View file

@ -136,38 +136,31 @@ public abstract class SAV5 : SaveFile, ISaveBlock5BW, IEventFlagProvider37, IBox
private static ReadOnlySpan<byte> DLCFooter => [ 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x14, 0x27, 0x00, 0x00, 0x27, 0x35, 0x05, 0x31, 0x00, 0x00 ];
public byte[] CGearSkinData
public Memory<byte> CGearSkinData => Data.AsMemory(CGearDataOffset, CGearBackground.SIZE);
public void SetCGearSkin(ReadOnlySpan<byte> 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<BlockInfo> AllBlocks { get; }

View file

@ -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;
/// <summary>
/// Generation 5 C-Gear Background Image
/// </summary>
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<byte> 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;
}
/// <summary>
/// Writes the <see cref="CGearBackground"/> data to a binary form.
/// </summary>
/// <param name="data">Destination buffer to write the skin to</param>
/// <param name="cgb">True if the destination game is <see cref="GameVersion.B2W2"/>, otherwise false for <see cref="GameVersion.BW"/>.</param>
/// <returns>Serialized skin data for writing to the save file</returns>
public void Write(Span<byte> 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<byte> 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<byte> data, ReadOnlySpan<Tile> 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<byte> data)
{
var colors = new int[data.Length / 2]; // u16->rgb32
ReadColorPalette(data, colors);
return colors;
}
private static void ReadColorPalette(ReadOnlySpan<byte> data, Span<int> colors)
{
var buffer = MemoryMarshal.Cast<byte, ushort>(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<byte> data, ReadOnlySpan<int> colors)
{
var buffer = MemoryMarshal.Cast<byte, ushort>(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;
}
}
/// <summary>
/// Creates a new C-Gear Background object from an input image data byte array, with 32 bits per pixel.
/// </summary>
/// <param name="data">Image data</param>
/// <returns>new C-Gear Background object</returns>
public static CGearBackground GetBackground(ReadOnlySpan<byte> 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<Tile> 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<byte> data)
{
var pixels = MemoryMarshal.Cast<byte, int>(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<int> colors, ReadOnlySpan<int> 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<int> colors, ReadOnlySpan<int> 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<Tile> tiles, out List<Tile> 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<Tile> 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<byte> 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..]);
}
}
}
}
/// <summary>
/// Generation 5 image tile composed of 8x8 pixels.
/// </summary>
/// <remarks>Each pixel's color choice is a nibble (4 bits).</remarks>
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<byte> 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<int> palette) => PixelData = GetTileData(palette, ColorChoices);
private static byte[] GetTileData(ReadOnlySpan<int> Palette, ReadOnlySpan<byte> choices)
{
byte[] data = new byte[choices.Length * 4];
SetTileData(data, Palette, choices);
return data;
}
private static void SetTileData(Span<byte> result, ReadOnlySpan<int> palette, ReadOnlySpan<byte> 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<byte> data) => Write(data, ColorChoices);
private static void Write(Span<byte> data, ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> data, Span<byte> result, [ConstantExpected(Min = 0)] int width, [ConstantExpected(Min = 4, Max = 4)] int bpp)
{
int pixels = data.Length / bpp;
var resultInt = MemoryMarshal.Cast<byte, int>(result);
var dataInt = MemoryMarshal.Cast<byte, int>(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<byte> data, Span<byte> 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<byte, int>(result);
var dataInt = MemoryMarshal.Cast<byte, int>(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<byte> 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<byte> 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<byte> 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<byte> 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<byte> data) : this(data.Length) => LoadData(data, TileChoices, Rotations);
private static void LoadData(ReadOnlySpan<byte> data, Span<byte> tiles, Span<byte> rotations)
{
bool isCGB = IsCGB(data);
if (!isCGB)
LoadDataPSK(data, tiles, rotations);
else
LoadDataCGB(data, tiles, rotations);
}
private static void LoadDataCGB(ReadOnlySpan<byte> data, Span<byte> tiles, Span<byte> 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<byte> data, Span<byte> tiles, Span<byte> 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<byte> 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<byte> data, bool cgb) => Write(data, TileChoices, Rotations, cgb);
private static void Write(Span<byte> data, ReadOnlySpan<byte> choices, ReadOnlySpan<byte> 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<byte> data, ReadOnlySpan<byte> choices, ReadOnlySpan<byte> 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<byte> data, ReadOnlySpan<byte> choices, ReadOnlySpan<byte> 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;
}
}

View file

@ -0,0 +1,50 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Lower screen tiled image data for the C-Gear.
/// </summary>
/// <param name="Raw">Data buffer storing the skin.</param>
/// <remarks>Image tiles, color palette, and tile arrangement map.</remarks>
public abstract class CGearBackground(Memory<byte> Raw) : ITiledImage
{
/// <summary> Pixel width of the background image. </summary>
public const int Width = 256; // px
/// <summary> Pixel height of the background image. </summary>
public const int Height = 192; // px
/// <summary> Byte size of the C-Gear background image. </summary>
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<byte> Data => Raw.Span;
/* 0x0000 - 0x1FDF*/ public Span<byte> Tiles => Data[..LengthTilePool];
/* 0x1FE0 - 0x1FFF*/ public Span<byte> Colors => Data.Slice(LengthTilePool, LengthColorData);
/* 0x2000 - 0x25FF*/ public Span<byte> Arrange => Data.Slice(OffsetTileMap, LengthTileMap);
public Span<byte> GetTileData(int tile) => Tiles.Slice(tile * PaletteTile.SIZE, PaletteTile.SIZE);
public Span<byte> 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);
}

View file

@ -0,0 +1,14 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Generation 5 C-Gear Background Image, tile positions formatted for <see cref="GameVersion.B2W2"/>.
/// </summary>
public sealed class CGearBackgroundB2W2(Memory<byte> Raw) : CGearBackground(Raw)
{
/// <summary>
/// Standard format, no oddities compared to the format for B/W.
/// </summary>
public const string Extension = "cgb";
}

View file

@ -0,0 +1,34 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Generation 5 C-Gear Background Image, tile positions formatted for <see cref="GameVersion.BW"/>.
/// </summary>
public sealed class CGearBackgroundBW(Memory<byte> Raw) : CGearBackground(Raw)
{
/// <summary>
/// Originally supported by PokeStock, a PokeSkin file.
/// </summary>
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.
/// <summary> Gets the 0-based index of the tile from an offset layout index. </summary>
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;
}
/// <summary> Gets the offset layout index of the tile from a 0-based index. </summary>
public static ushort GetLayoutIndex(int tile) => (ushort)((TileBWHeight * (tile / TileBWWidth)) + tile + TileBWStart);
}

View file

@ -0,0 +1,32 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Tiled Image interface for Generation 5 C-Gear Backgrounds and PokéDex Skins.
/// </summary>
public interface ITiledImage
{
/// <summary> Section of the data buffer storing all the tiles. </summary>
Span<byte> Tiles { get; }
/// <summary> Section of the data buffer storing the color palette. </summary>
Span<byte> Colors { get; }
/// <summary> Section of the data buffer storing the tile arrangement map. </summary>
Span<byte> Arrange { get; }
/// <summary> Get the tile color choice data for a specific tile index. </summary>
Span<byte> GetTileData(int tile);
/// <summary> Count of pixels in the image. </summary>
static abstract int PixelCount { get; }
/// <summary> Count of unique colors in the image. </summary>
static abstract int ColorCount { get; }
/// <summary> Count of tiles in the tile pool. </summary>
static abstract int TilePoolCount { get; }
/// <summary> Count of pixels for a side (width/height) of a tile. </summary>
static abstract int TileSize { get; }
/// <summary> Pixel width of the image. </summary>
static abstract int Width { get; }
/// <summary> Pixel height of the image. </summary>
static abstract int Height { get; }
}

View file

@ -0,0 +1,62 @@
using System;
using System.Runtime.InteropServices;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Logic for interacting with a palette color set.
/// </summary>
/// <see cref="ITiledImage"/>
public static class PaletteColorSet
{
/// <summary>
/// Converts a 32-bit ARGB pixel list to a 15-bit color.
/// </summary>
public static int GetUniqueColorsFromPixelData(ReadOnlySpan<byte> data, Span<int> result)
{
int count = 0;
var pixels = MemoryMarshal.Cast<byte, int>(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<byte> data, Span<int> colors)
{
var buffer = MemoryMarshal.Cast<byte, ushort>(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<int> colors, Span<byte> data)
{
var buffer = MemoryMarshal.Cast<byte, ushort>(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;
}
}
}

View file

@ -0,0 +1,68 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Generation 5 image tile composed of 8x8 pixels.
/// </summary>
/// <remarks>Each pixel's color choice is a nibble (4 bits) from a color palette.</remarks>
/// <see cref="ITiledImage"/>
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<byte> tileColors, ReadOnlySpan<byte> 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<byte> tileColors, ReadOnlySpan<byte> 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<byte> tileColors, ReadOnlySpan<byte> 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<byte> tileColors, ReadOnlySpan<byte> 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;
}
}

View file

@ -0,0 +1,163 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// 32-bit ARGB pixel converter for interacting with square pixel tiles.
/// </summary>
/// <see cref="ITiledImage"/>
public static class PaletteTileOperations
{
/// <summary>
/// Reads the color choices from the <see cref="data"/> into the <see cref="colorChoices"/>.
/// </summary>
public static void SplitNibbles(ReadOnlySpan<byte> data, Span<byte> 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);
}
}
/// <summary>
/// Writes the <see cref="colorChoices"/> into the <see cref="data"/>.
/// </summary>
public static void MergeNibbles(ReadOnlySpan<byte> colorChoices, Span<byte> 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));
}
}
/// <summary>
/// Inflates the <see cref="colorChoices"/> into the <see cref="result"/> using the <see cref="palette"/>.
/// </summary>
/// <param name="colorChoices">Color choices for each pixel</param>
/// <param name="palette">32-bit ARGB palette</param>
/// <param name="result">Resulting pixel data</param>
public static void Inflate(ReadOnlySpan<byte> colorChoices, ReadOnlySpan<int> palette, Span<byte> 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);
}
}
/// <summary>
/// Deflates the <see cref="pixels"/> into the <see cref="tileColors"/> using the <see cref="palette"/>.
/// </summary>
/// <param name="pixels">Resulting pixel data</param>
/// <param name="palette">32-bit ARGB palette</param>
/// <param name="tileColors">Color choices for each pixel</param>
public static void Deflate(ReadOnlySpan<int> pixels, ReadOnlySpan<int> palette, Span<byte> 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);
}
}
/// <summary>
/// Flips the pixel data across the X axis.
/// </summary>
public static void FlipX(ReadOnlySpan<byte> data, Span<byte> result, int width, [ConstantExpected(Min = 4, Max = 4)] int bpp)
{
int pixels = data.Length / bpp;
var resultInt = MemoryMarshal.Cast<byte, int>(result);
var dataInt = MemoryMarshal.Cast<byte, int>(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];
}
}
/// <summary>
/// Flips the pixel data across the Y axis.
/// </summary>
public static void FlipY(ReadOnlySpan<byte> data, Span<byte> result, int height, [ConstantExpected(Min = 4, Max = 4)] int bpp)
{
int pixels = data.Length / bpp;
int width = pixels / height;
var resultInt = MemoryMarshal.Cast<byte, int>(result);
var dataInt = MemoryMarshal.Cast<byte, int>(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];
}
}
/// <summary>
/// Flips the pixel data across both the X and Y axis.
/// </summary>
public static void FlipXY(ReadOnlySpan<byte> data, Span<byte> result, int height, [ConstantExpected(Min = 4, Max = 4)] int bpp)
{
int pixels = data.Length / bpp;
int width = pixels / height;
var resultInt = MemoryMarshal.Cast<byte, int>(result);
var dataInt = MemoryMarshal.Cast<byte, int>(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];
}
}
/// <summary>
/// Flips the 32-bit pixel data across the requested <see cref="rot"/>.
/// </summary>
/// <remarks>If no rotation is requested, the data is only copied.</remarks>
/// <param name="tile">Unpacked (32-bit pixel) tile data to process</param>
/// <param name="result">Resulting tile data</param>
/// <param name="tileSize">Size of the tile (width/height)</param>
/// <param name="rot">Rotation type to apply</param>
/// <param name="bpp">Bits per pixel, requires 4.</param>
public static void Flip(ReadOnlySpan<byte> tile, Span<byte> 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);
}
}

View file

@ -0,0 +1,26 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// Rotation flags for a palette tile.
/// </summary>
/// <see cref="PaletteTileSelection"/>
[Flags]
public enum PaletteTileRotation
{
/// <summary> No rotation. </summary>
None = 0,
/// <summary> Flip the tile horizontally. </summary>
FlipX = 1 << 0,
/// <summary> Flip the tile vertically. </summary>
FlipY = 1 << 1,
/// <summary> Flip the tile horizontally and vertically. </summary>
FlipXY = FlipX | FlipY,
/// <summary> Invalid rotation. </summary>
Invalid = 1 << 2,
}

View file

@ -0,0 +1,149 @@
using System;
using System.Runtime.InteropServices;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Logic for reading and writing the palette tile selection format.
/// </summary>
/// <remarks>
/// The structure is a 16-bit packed value with the following format:
/// <code>
/// 6 bits tile index
/// 2 bits flip y|x
/// 4 bits palette type
/// </code>
/// </remarks>
public static class PaletteTileSelection
{
/// <summary>
/// Breaks down the selection value into its components.
/// </summary>
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);
}
/// <summary>
/// Combines the components into a selection value.
/// </summary>
public static ushort ComposeSelection(ushort choice, byte flip, byte palette)
{
return (ushort)((palette << 12) | ((flip & 3) << 10) | (choice & 0x3FF));
}
/// <summary>
/// Checks if any of the selection values are in the shifted tile arrangement format.
/// </summary>
public static bool IsPaletteShiftFormat(ReadOnlySpan<byte> data)
{
var u16 = MemoryMarshal.Cast<byte, ushort>(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;
}
/// <summary>
/// Converts the selection values to the shifted tile arrangement format.
/// </summary>
/// <remarks>Used to convert from B2/W2 to B/W.</remarks>
public static void ConvertToShiftFormat<T>(Span<byte> data) where T : ITiledImage
{
var u16 = MemoryMarshal.Cast<byte, ushort>(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);
}
}
/// <summary>
/// Converts the selection values from the shifted tile arrangement format.
/// </summary>
/// <remarks>Used to convert from B/W to B2/W2.</remarks>
public static void ConvertFromShiftFormat(Span<byte> data)
{
var u16 = MemoryMarshal.Cast<byte, ushort>(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);
}
}
/// <summary>
/// Reads all the layout details from the arrangement list.
/// </summary>
/// <param name="data">Buffer containing the arrangement list.</param>
/// <param name="choice">Result storage for tile choice at index i</param>
/// <param name="flip">Result storage for flip type at index i</param>
/// <param name="palette">Result storage for palette type at index i</param>
public static void ReadLayoutDetails(ReadOnlySpan<byte> data, Span<ushort> choice, Span<byte> flip, Span<byte> palette)
{
var u16 = MemoryMarshal.Cast<byte, ushort>(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]);
}
}
/// <summary>
/// Writes all the layout details to the arrangement list.
/// </summary>
/// <param name="data">Buffer containing the arrangement list.</param>
/// <param name="choice">Input storage for tile choice at index i</param>
/// <param name="flip">Input storage for flip type at index i</param>
/// <param name="palette">Input storage for palette type at index i</param>
public static void WriteLayoutDetails(Span<byte> data, ReadOnlySpan<ushort> choice, ReadOnlySpan<byte> flip, ReadOnlySpan<byte> palette)
{
var u16 = MemoryMarshal.Cast<byte, ushort>(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;
}
}
/// <inheritdoc cref="WriteLayoutDetails(Span{byte}, ReadOnlySpan{ushort}, ReadOnlySpan{byte}, ReadOnlySpan{byte})"/>
public static void WriteLayoutDetails(Span<byte> data, ReadOnlySpan<ushort> choice, ReadOnlySpan<byte> flip)
{
var u16 = MemoryMarshal.Cast<byte, ushort>(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;
}
}
}

View file

@ -0,0 +1,201 @@
using System;
using System.Runtime.InteropServices;
namespace PKHeX.Core;
/// <summary>
/// Conversion logic for <see cref="ITiledImage"/> objects.
/// </summary>
/// <remarks>Assumes 32-bit ARGB pixel data.</remarks>
public static class TiledImageConverter
{
private const int bpp = 4; // 32-bit ARGB
/// <summary>
/// Allocates a new byte array and fills it with the image data.
/// </summary>
/// <param name="bg">Input to create image pixel data for</param>
public static byte[] GetImageData<T>(this T bg)
where T : ITiledImage
{
var result = new byte[T.PixelCount * bpp];
GetImageData(bg, result);
return result;
}
/// <summary>
/// Fills the <see cref="result"/> buffer with the image data.
/// </summary>
/// <param name="bg">Input to create image pixel data for</param>
/// <param name="result">Pixel data buffer to fill</param>
/// <exception cref="ArgumentException"></exception>
public static void GetImageData<T>(this T bg, Span<byte> 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<int> palette = stackalloc int[T.ColorCount];
PaletteColorSet.Read(bg.Colors, palette);
// Get tile choices
var arrange = bg.Arrange;
Span<ushort> choiceTil = stackalloc ushort[arrange.Length / 2];
Span<byte> choiceRot = stackalloc byte[arrange.Length / 2];
Span<byte> choicePal = stackalloc byte[arrange.Length / 2];
PaletteTileSelection.ReadLayoutDetails(arrange, choiceTil, choiceRot, choicePal);
if (bg is CGearBackgroundBW)
{
if (!choiceTil.ContainsAnyExcept<ushort>(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>(T bg, Span<byte> result, Span<ushort> choiceTil, Span<byte> choiceRot, Span<int> palette)
where T : ITiledImage
{
// Allocate a working buffer to store the tile data
Span<byte> colors = stackalloc byte[PaletteTile.SIZE * 2]; // inflated, not nibbles. one pixel per index
Span<byte> tileData = stackalloc byte[colors.Length * bpp];
Span<byte> 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<T>(result, i, tileData);
}
}
private static void WriteTile<T>(Span<byte> result, int index, Span<byte> 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..]);
}
}
/// <summary>
/// Sets the image data from the <see cref="data"/> buffer, converting into the background tile object.
/// </summary>
/// <param name="bg">Result to update with the image data</param>
/// <param name="data">Pixel data buffer to read</param>
/// <exception cref="ArgumentException"></exception>
public static TiledImageStat SetImageData<T>(this T bg, ReadOnlySpan<byte> 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<int> 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<ushort> choiceTil = stackalloc ushort[arrange.Length / 2];
Span<byte> 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<byte> tileColors = stackalloc byte[T.TileSize * T.TileSize];
var maxTiles = bg.Tiles.Length / (tileColors.Length / 2);
Span<byte> rawTiles = stackalloc byte[tileColors.Length * maxTiles];
for (int tileIndex = 0; tileIndex < choiceTil.Length; tileIndex++)
{
ReadTile<T>(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<CGearBackgroundBW>(bg.Arrange);
return new TiledImageStat { TileCount = tileCount, ColorCount = colorCount };
}
private static bool TryFindRotation(ReadOnlySpan<byte> tileColors, ReadOnlySpan<byte> 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<T>(ReadOnlySpan<byte> data, int i, Span<byte> tileColors, Span<int> 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<byte, int>(tileRow);
var slice = tileColors.Slice(y * tSize, T.TileSize);
PaletteTileOperations.Deflate(i32, colorChoices, slice);
}
}
}

View file

@ -0,0 +1,69 @@
using System.Runtime.InteropServices;
using System;
namespace PKHeX.Core;
/// <summary>
/// Simple record to store the unique tile and color count of a <see cref="ITiledImage"/>.
/// </summary>
/// <param name="TileCount">Count of unique tiles in the data</param>
/// <param name="ColorCount">Count of unique colors in the data</param>
public readonly record struct TiledImageStat(int TileCount, int ColorCount);
/// <summary>
/// Logic to calculate a <see cref="TiledImageStat"/> from an <see cref="ITiledImage"/>.
/// </summary>
public static class TiledImageUtil
{
/// <summary>
/// Get the unique tile and color count of the <see cref="ITiledImage"/>.
/// </summary>
/// <remarks>Useful if you want to inspect a tiled image directly without rebuilding between image file.</remarks>
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<byte> imgColors)
{
var u16 = MemoryMarshal.Cast<byte, ushort>(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<byte> 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<byte> otherTiles, ReadOnlySpan<byte> 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;
}
}

View file

@ -28,7 +28,7 @@ public static class CGearImage
/// Converts a <see cref="Bitmap"/> to a <see cref="CGearBackground"/>.
/// </summary>
/// <exception cref="ArgumentException"></exception>
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 class CGearImage
const int bpp = 4;
Debug.Assert(data.Length == Width * Height * bpp);
return CGearBackground.GetBackground(data);
return bg.SetImageData(data);
}
}

View file

@ -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 @@ public partial class SAV_CGearSkin : Form
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<CGearBackground>(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<CGearBackground>(result, out msg))
System.Media.SystemSounds.Asterisk.Play();
else
WinFormsUtil.Alert(msg);
}
catch (Exception ex)
{
@ -46,6 +56,41 @@ public partial class SAV_CGearSkin : Form
}
}
private static bool IsInputCorrect<T>(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<T>(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 @@ public partial class SAV_CGearSkin : Form
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<CGearBackgroundBW>(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<byte>(0))
{
SAV.CGearSkinData = data;
Origin.CopyChangesFrom(SAV);
}
if (bg.Data.ContainsAnyExcept<byte>(0))
SAV.SetCGearSkin(bg.Data);
Close();
}