mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-23 20:43:07 +00:00
47071b41f3
Existing `get`/`set` logic is flawed in that it doesn't work on Big Endian operating systems, and it allocates heap objects when it doesn't need to. `System.Buffers.Binary.BinaryPrimitives` in the `System.Memory` NuGet package provides both Little Endian and Big Endian methods to read and write data; all the `get`/`set` operations have been reworked to use this new API. This removes the need for PKHeX's manual `BigEndian` class, as all functions are already covered by the BinaryPrimitives API. The `StringConverter` has now been rewritten to accept a Span to read from & write to, no longer requiring a temporary StringBuilder. Other Fixes included: - The Super Training UI for Gen6 has been reworked according to the latest block structure additions. - Cloning a Stadium2 Save File now works correctly (opening from the Folder browser list). - Checksum & Sanity properties removed from parent PKM class, and is now implemented via interface.
530 lines
18 KiB
C#
530 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
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|*.cgb";
|
|
public const int Width = 256; // px
|
|
public const int Height = 192; // px
|
|
public const int SIZE_CGB = 0x2600;
|
|
private const int ColorCount = 0x10;
|
|
private const int TileSize = 8;
|
|
private const int TileCount = (Width / TileSize) * (Height / TileSize); // 0x300
|
|
|
|
/* 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 BW) employs some obfuscation.
|
|
* BW 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 / B2W2.
|
|
* Due to BW and B2W2 using different obfuscation adds, PSK files are incompatible between the versions.
|
|
*/
|
|
|
|
public CGearBackground(byte[] data)
|
|
{
|
|
if (data.Length != SIZE_CGB)
|
|
throw new ArgumentException(nameof(data));
|
|
|
|
// decode for easy handling
|
|
if (!IsCGB(data))
|
|
{
|
|
_psk = data;
|
|
data = PSKtoCGB(data);
|
|
}
|
|
else
|
|
{
|
|
_cgb = data;
|
|
}
|
|
|
|
var Region1 = data.AsSpan(0, 0x1FE0);
|
|
var ColorData = data.Slice(0x1FE0, 0x20);
|
|
var Region2 = data.Slice(0x2000, 0x600);
|
|
|
|
ColorPalette = new int[ColorCount];
|
|
for (int i = 0; i < ColorPalette.Length; i++)
|
|
ColorPalette[i] = GetRGB555_16(ReadUInt16LittleEndian(ColorData.AsSpan(i * 2)));
|
|
|
|
Tiles = new Tile[0xFF];
|
|
for (int i = 0; i < Tiles.Length; i++)
|
|
{
|
|
byte[] tiledata = Region1.Slice(i * Tile.SIZE_TILE, Tile.SIZE_TILE).ToArray();
|
|
Tiles[i] = new Tile(tiledata);
|
|
Tiles[i].SetTile(ColorPalette);
|
|
}
|
|
|
|
Map = new TileMap(Region2);
|
|
}
|
|
|
|
public readonly int[] ColorPalette;
|
|
public readonly Tile[] Tiles;
|
|
public readonly TileMap Map;
|
|
|
|
// Track the original data
|
|
private readonly byte[]? _cgb;
|
|
private readonly byte[]? _psk;
|
|
|
|
/// <summary>
|
|
/// Writes the <see cref="CGearBackground"/> data to a binary form.
|
|
/// </summary>
|
|
/// <param name="B2W2">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 byte[] GetSkin(bool B2W2) => B2W2 ? GetCGB() : GetPSK();
|
|
|
|
private byte[] GetCGB() => _cgb ?? Write();
|
|
private byte[] GetPSK() => _psk ?? CGBtoPSK(Write());
|
|
|
|
private byte[] Write()
|
|
{
|
|
byte[] data = new byte[SIZE_CGB];
|
|
for (int i = 0; i < Tiles.Length; i++)
|
|
Array.Copy(Tiles[i].Write(), 0, data, i*Tile.SIZE_TILE, Tile.SIZE_TILE);
|
|
|
|
for (int i = 0; i < ColorPalette.Length; i++)
|
|
{
|
|
var value = GetRGB555(ColorPalette[i]);
|
|
var span = data.AsSpan(0x1FE0 + (i * 2));
|
|
WriteUInt16LittleEndian(span, value);
|
|
}
|
|
|
|
Array.Copy(Map.Write(), 0, data, 0x2000, 0x600);
|
|
|
|
return data;
|
|
}
|
|
|
|
private static bool IsCGB(IReadOnlyList<byte> data)
|
|
{
|
|
if (data.Count != SIZE_CGB)
|
|
return false;
|
|
|
|
// check odd bytes for anything not rotation flag
|
|
for (int i = 0x2000; i < 0x2600; i += 2)
|
|
{
|
|
if ((data[i + 1] & ~0b1100) != 0)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static byte[] CGBtoPSK(ReadOnlySpan<byte> cgb)
|
|
{
|
|
byte[] psk = cgb.ToArray();
|
|
for (int i = 0x2000; i < 0x2600; i += 2)
|
|
{
|
|
var span = psk.AsSpan(i);
|
|
var tileVal = ReadUInt16LittleEndian(span);
|
|
int val = GetPSKValue(tileVal);
|
|
WriteUInt16LittleEndian(span, (ushort)val);
|
|
}
|
|
return psk;
|
|
}
|
|
|
|
private static int GetPSKValue(ushort val)
|
|
{
|
|
int rot = val & 0xFF00;
|
|
int tile = val & 0x00FF;
|
|
if (tile == 0xFF) // invalid tile?
|
|
tile = 0;
|
|
|
|
return tile + (15 * (tile / 17)) + 0xA0A0 + rot;
|
|
}
|
|
|
|
private static byte[] PSKtoCGB(ReadOnlySpan<byte> psk)
|
|
{
|
|
byte[] cgb = psk.ToArray();
|
|
for (int i = 0x2000; i < 0x2600; i += 2)
|
|
{
|
|
int val = ReadUInt16LittleEndian(psk[i..]);
|
|
int index = ValToIndex(val);
|
|
|
|
byte tile = (byte)index;
|
|
byte rot = (byte)(index >> 8);
|
|
if (tile == 0xFF)
|
|
tile = 0;
|
|
cgb[i] = tile;
|
|
cgb[i + 1] = rot;
|
|
}
|
|
return cgb;
|
|
}
|
|
|
|
private static int ValToIndex(int val)
|
|
{
|
|
var trunc = (val & 0x3FF);
|
|
if (trunc is < 0xA0 or > 0x280)
|
|
return (val & 0x5C00) | 0xFF;
|
|
return ((val % 0x20) + (17 * ((trunc - 0xA0) / 0x20))) | (val & 0x5C00);
|
|
}
|
|
|
|
private static byte Convert8to5(int colorval)
|
|
{
|
|
byte i = 0;
|
|
while (colorval > Convert5To8[i]) i++;
|
|
return i;
|
|
}
|
|
|
|
private static int GetRGB555_32(int val)
|
|
{
|
|
var R = (val >> 00) & 0xFF;
|
|
var G = (val >> 08) & 0xFF;
|
|
var B = (val >> 16) & 0xFF;
|
|
return 0xFF << 24 | B << 16 | G << 8 | R;
|
|
}
|
|
|
|
private static int GetRGB555_16(ushort val)
|
|
{
|
|
int R = (val >> 0) & 0x1F;
|
|
int G = (val >> 5) & 0x1F;
|
|
int B = (val >> 10) & 0x1F;
|
|
|
|
R = Convert5To8[R];
|
|
G = Convert5To8[G];
|
|
B = Convert5To8[B];
|
|
|
|
return 0xFF << 24 | R << 16 | G << 8 | B;
|
|
}
|
|
|
|
private static ushort GetRGB555(int v)
|
|
{
|
|
var R = (byte)(v >> 16);
|
|
var G = (byte)(v >> 8);
|
|
var B = (byte)(v >> 0);
|
|
|
|
int val = 0;
|
|
val |= Convert8to5(R) << 0;
|
|
val |= Convert8to5(G) << 5;
|
|
val |= Convert8to5(B) << 10;
|
|
return (ushort)val;
|
|
}
|
|
|
|
private static readonly int[] Convert5To8 = { 0x00,0x08,0x10,0x18,0x20,0x29,0x31,0x39,
|
|
0x41,0x4A,0x52,0x5A,0x62,0x6A,0x73,0x7B,
|
|
0x83,0x8B,0x94,0x9C,0xA4,0xAC,0xB4,0xBD,
|
|
0xC5,0xCD,0xD5,0xDE,0xE6,0xEE,0xF6,0xFF };
|
|
|
|
/// <summary>
|
|
/// Creates a new C-Gear Background object from an input image data byte array
|
|
/// </summary>
|
|
/// <param name="data">Image data</param>
|
|
/// <param name="bpp">Bytes per pixel</param>
|
|
/// <returns>new C-Gear Background object</returns>
|
|
public static CGearBackground GetBackground(byte[] data, int bpp = 4)
|
|
{
|
|
int[] pixels = new int[data.Length / bpp];
|
|
Buffer.BlockCopy(data, 0, pixels, 0, data.Length);
|
|
var colors = GetColorData(pixels);
|
|
|
|
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 >= 0xFF)
|
|
throw new ArgumentException($"Too many unique tiles. Expected < 256, received {tilelist.Count}.");
|
|
|
|
// Finished!
|
|
return new CGearBackground(Palette, tilelist.ToArray(), tm);
|
|
}
|
|
|
|
private static int[] GetColorData(IReadOnlyList<int> pixels)
|
|
{
|
|
int[] colors = new int[pixels.Count];
|
|
for (int i = 0; i < pixels.Count; i++)
|
|
colors[i] = GetRGB555_32(pixels[i]);
|
|
return colors;
|
|
}
|
|
|
|
private static Tile[] GetTiles(IReadOnlyList<int> colors, 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(IReadOnlyList<int> colors, int[] palette, int tileIndex)
|
|
{
|
|
int x = (tileIndex * 8) % Width;
|
|
int y = 8 * ((tileIndex * 8) / Width);
|
|
|
|
var t = new Tile();
|
|
for (uint ix = 0; ix < 8; ix++)
|
|
{
|
|
for (uint iy = 0; iy < 8; iy++)
|
|
{
|
|
int index = ((int) (y + iy) * Width) + (int) (x + ix);
|
|
int c = colors[index];
|
|
|
|
t.ColorChoices[(ix % 8) + (iy * 8)] = Array.IndexOf(palette, c);
|
|
}
|
|
}
|
|
|
|
t.SetTile(palette);
|
|
return t;
|
|
}
|
|
|
|
private static void GetTileList(IReadOnlyList<Tile> tiles, out List<Tile> tilelist, out TileMap tm)
|
|
{
|
|
tilelist = new List<Tile> { tiles[0] };
|
|
tm = new TileMap(new byte[2 * Width * Height / 64]);
|
|
|
|
// 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, IList<Tile> tilelist, TileMap tm, int tileIndex)
|
|
{
|
|
// Test all tiles currently in the list
|
|
for (int j = 0; j < tilelist.Count; j++)
|
|
{
|
|
int rotVal = t.GetRotationValue(tilelist[j].ColorChoices);
|
|
if (rotVal <= -1)
|
|
continue;
|
|
tm.TileChoices[tileIndex] = j;
|
|
tm.Rotations[tileIndex] = rotVal;
|
|
return;
|
|
}
|
|
|
|
// No tile found, add to list
|
|
tilelist.Add(t);
|
|
tm.TileChoices[tileIndex] = tilelist.Count - 1;
|
|
tm.Rotations[tileIndex] = 0;
|
|
}
|
|
|
|
private CGearBackground(int[] Palette, Tile[] tilelist, TileMap tm)
|
|
{
|
|
Map = tm;
|
|
ColorPalette = Palette;
|
|
Tiles = tilelist;
|
|
}
|
|
|
|
public byte[] GetImageData()
|
|
{
|
|
byte[] data = new byte[4 * Width * Height];
|
|
for (int i = 0; i < Map.TileChoices.Length; i++)
|
|
{
|
|
int x = (i * 8) % Width;
|
|
int y = 8 * ((i * 8) / Width);
|
|
var choice = Map.TileChoices[i] % (Tiles.Length + 1);
|
|
var tile = Tiles[choice];
|
|
var tileData = tile.Rotate(Map.Rotations[i]);
|
|
for (int iy = 0; iy < 8; iy++)
|
|
{
|
|
int src = iy * (4 * 8);
|
|
int dest = (((y+iy) * Width) + x) * 4;
|
|
Array.Copy(tileData, src, data, dest, 4*8);
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
}
|
|
|
|
public sealed class Tile
|
|
{
|
|
internal const int SIZE_TILE = 0x20;
|
|
private const int TileWidth = 8;
|
|
private const int TileHeight = 8;
|
|
internal readonly int[] ColorChoices;
|
|
private byte[] PixelData;
|
|
private byte[]? PixelDataX;
|
|
private byte[]? PixelDataY;
|
|
|
|
internal Tile() : this(new byte[SIZE_TILE]) { }
|
|
|
|
internal Tile(byte[] data)
|
|
{
|
|
if (data.Length != SIZE_TILE)
|
|
throw new ArgumentException(nameof(data));
|
|
|
|
ColorChoices = new int[TileWidth * TileHeight];
|
|
for (int i = 0; i < data.Length; i++)
|
|
{
|
|
var ofs = i * 2;
|
|
ColorChoices[ofs + 0] = data[i] & 0xF;
|
|
ColorChoices[ofs + 1] = data[i] >> 4;
|
|
}
|
|
PixelData = Array.Empty<byte>();
|
|
}
|
|
|
|
internal void SetTile(int[] Palette) => PixelData = GetTileData(Palette);
|
|
|
|
private byte[] GetTileData(IReadOnlyList<int> Palette)
|
|
{
|
|
const int pixels = TileWidth * TileHeight;
|
|
byte[] data = new byte[pixels * 4];
|
|
for (int i = 0; i < pixels; i++)
|
|
{
|
|
var choice = ColorChoices[i];
|
|
var value = Palette[choice];
|
|
var span = data.AsSpan(4 * i, 4);
|
|
WriteInt32LittleEndian(span, value);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
internal byte[] Write()
|
|
{
|
|
byte[] data = new byte[SIZE_TILE];
|
|
for (int i = 0; i < data.Length; i++)
|
|
{
|
|
var span = ColorChoices.AsSpan(i * 2, 2);
|
|
data[i] = (byte)((span[0] & 0xF) | ((span[1] & 0xF) << 4));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
public byte[] Rotate(int rotFlip)
|
|
{
|
|
if (rotFlip == 0)
|
|
return PixelData;
|
|
if ((rotFlip & 4) > 0)
|
|
return PixelDataX ??= FlipX(PixelData, TileWidth);
|
|
if ((rotFlip & 8) > 0)
|
|
return PixelDataY ??= FlipY(PixelData, TileHeight);
|
|
return PixelData;
|
|
}
|
|
|
|
private static byte[] FlipX(IReadOnlyList<byte> data, int width, int bpp = 4)
|
|
{
|
|
byte[] result = new byte[data.Count];
|
|
int pixels = data.Count / bpp;
|
|
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) * bpp;
|
|
|
|
var o = 4 * i;
|
|
result[dest + 0] = data[o + 0];
|
|
result[dest + 1] = data[o + 1];
|
|
result[dest + 2] = data[o + 2];
|
|
result[dest + 3] = data[o + 3];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static byte[] FlipY(IReadOnlyList<byte> data, int height, int bpp = 4)
|
|
{
|
|
byte[] result = new byte[data.Count];
|
|
int pixels = data.Count / bpp;
|
|
int width = pixels / height;
|
|
for (int i = 0; i < pixels; i++)
|
|
{
|
|
int x = i % width;
|
|
int y = i / width;
|
|
|
|
y = height - y - 1; // flip x
|
|
int dest = ((y * width) + x) * bpp;
|
|
|
|
var o = 4 * i;
|
|
result[dest + 0] = data[o + 0];
|
|
result[dest + 1] = data[o + 1];
|
|
result[dest + 2] = data[o + 2];
|
|
result[dest + 3] = data[o + 3];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
internal int GetRotationValue(ReadOnlySpan<int> tileColors)
|
|
{
|
|
// Check all rotation types
|
|
if (tileColors.SequenceEqual(ColorChoices))
|
|
return 0;
|
|
|
|
if (IsMirrorX(tileColors))
|
|
return 4;
|
|
if (IsMirrorY(tileColors))
|
|
return 8;
|
|
if (IsMirrorXY(tileColors))
|
|
return 12;
|
|
|
|
return -1;
|
|
}
|
|
|
|
private bool IsMirrorX(ReadOnlySpan<int> tileColors)
|
|
{
|
|
for (int i = 0; i < 64; i++)
|
|
{
|
|
if (ColorChoices[(7 - (i & 7)) + (8 * (i / 8))] != tileColors[i])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsMirrorY(ReadOnlySpan<int> tileColors)
|
|
{
|
|
for (int i = 0; i < 64; i++)
|
|
{
|
|
if (ColorChoices[64 - (8 * (1 + (i / 8))) + (i & 7)] != tileColors[i])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsMirrorXY(ReadOnlySpan<int> tileColors)
|
|
{
|
|
for (int i = 0; i < 64; i++)
|
|
{
|
|
if (ColorChoices[63 - i] != tileColors[i])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public sealed class TileMap
|
|
{
|
|
public readonly int[] TileChoices;
|
|
public readonly int[] Rotations;
|
|
|
|
internal TileMap(byte[] data)
|
|
{
|
|
TileChoices = new int[data.Length / 2];
|
|
Rotations = new int[data.Length / 2];
|
|
for (int i = 0; i < data.Length; i += 2)
|
|
{
|
|
TileChoices[i / 2] = data[i];
|
|
Rotations[i / 2] = data[i + 1];
|
|
}
|
|
}
|
|
|
|
internal byte[] Write()
|
|
{
|
|
byte[] data = new byte[TileChoices.Length * 2];
|
|
for (int i = 0; i < data.Length; i += 2)
|
|
{
|
|
data[i] = (byte)TileChoices[i / 2];
|
|
data[i + 1] = (byte)Rotations[i / 2];
|
|
}
|
|
return data;
|
|
}
|
|
}
|
|
}
|