mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-10 06:34:19 +00:00
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:
parent
ba7646e7a2
commit
0e4d30fd21
15 changed files with 969 additions and 642 deletions
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue