diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32becce --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# JetBrains +.idea/ + +# Visual Studio +.vs/ +bin/ +obj/ + +# Visual Studio Code +.vscode/ + diff --git a/LCRng.cs b/LCRng.cs new file mode 100644 index 0000000..a6b76cd --- /dev/null +++ b/LCRng.cs @@ -0,0 +1,16 @@ +namespace PCDWC4Converter; + +public class LCRng +{ + private uint _seed; + + public LCRng(uint seed = 0) => _seed = seed; + + public uint Next() => _seed = _seed * 0x41C64E6Du + 0x00006073u; + + public uint NextH() => Next() >> 0x10; + + public uint Prev() => _seed = _seed * 0xEEB9EB65u + 0xA3561A1u; + + public uint PrevH() => Prev() >> 0x10; +} diff --git a/PCDWC4Converter.csproj b/PCDWC4Converter.csproj new file mode 100644 index 0000000..b9de063 --- /dev/null +++ b/PCDWC4Converter.csproj @@ -0,0 +1,10 @@ + + + + Exe + net6.0 + enable + enable + + + diff --git a/PCDWC4Converter.sln b/PCDWC4Converter.sln new file mode 100644 index 0000000..ef38814 --- /dev/null +++ b/PCDWC4Converter.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCDWC4Converter", "PCDWC4Converter.csproj", "{CAED887B-C68C-4EF4-9897-BA1D25ECBC89}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CAED887B-C68C-4EF4-9897-BA1D25ECBC89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAED887B-C68C-4EF4-9897-BA1D25ECBC89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAED887B-C68C-4EF4-9897-BA1D25ECBC89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAED887B-C68C-4EF4-9897-BA1D25ECBC89}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/PokemonData.cs b/PokemonData.cs new file mode 100644 index 0000000..2e49269 --- /dev/null +++ b/PokemonData.cs @@ -0,0 +1,88 @@ +using System.Buffers.Binary; + +namespace PCDWC4Converter; + +public static class PokemonData +{ + private static readonly int[][] _blockPositions = + { + new[] { 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 2, 3, 1, 1, 2, 3, 2, 3, 1, 1, 2, 3, 2, 3 }, + new[] { 1, 1, 2, 3, 2, 3, 0, 0, 0, 0, 0, 0, 2, 3, 1, 1, 3, 2, 2, 3, 1, 1, 3, 2 }, + new[] { 2, 3, 1, 1, 3, 2, 2, 3, 1, 1, 3, 2, 0, 0, 0, 0, 0, 0, 3, 2, 3, 2, 1, 1 }, + new[] { 3, 2, 3, 2, 1, 1, 3, 2, 3, 2, 1, 1, 3, 2, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0 }, + }; + + private static void XorCrypt(Span data) + { + var pid = BinaryPrimitives.ReadUInt32LittleEndian(data[..4]); + var initialSeed = BinaryPrimitives.ReadUInt16LittleEndian(data[6..8]); + + var rng = new LCRng(initialSeed); + + for (var i = 8; i < 236; i += 2) + { + if (i == 136) + { + rng = new LCRng(pid); + } + + var dataBlock = data[i..]; + var value = BinaryPrimitives.ReadUInt16LittleEndian(dataBlock); + BinaryPrimitives.WriteUInt16LittleEndian(dataBlock, (ushort)(value ^ rng.NextH())); + } + } + + private static void Unshuffle(Span data) + { + var pid = BinaryPrimitives.ReadUInt32LittleEndian(data[..4]); + var shiftValue = ((pid & 0x3E000) >> 0xD) % 24; + + // Offset by first block + data = data.Slice(8, 32 * 4); + + Span originalData = stackalloc byte[32 * 4]; + data.CopyTo(originalData); + + for (var i = 0; i < 4; i++) + { + // Unshuffle the data + originalData.Slice(32 * _blockPositions[i][shiftValue], 32).CopyTo(data); + + // Increment to next block + data = data[32..]; + } + } + + private static void Shuffle(Span data) + { + var pid = BinaryPrimitives.ReadUInt32LittleEndian(data[..4]); + var shiftValue = ((pid & 0x3E000) >> 0xD) % 24; + + // Offset by first block + data = data.Slice(8, 32 * 4); + + Span originalData = stackalloc byte[32 * 4]; + data.CopyTo(originalData); + + for (var i = 0; i < 4; i++) + { + // Shuffle the data + originalData[..32].CopyTo(data.Slice(_blockPositions[i][shiftValue], 32)); + + // Increment to next block + originalData = originalData[32..]; + } + } + + public static void DecryptData(Span pkmData) + { + XorCrypt(pkmData); + Unshuffle(pkmData); + } + + public static void EncryptData(Span pkmData) + { + XorCrypt(pkmData); + Shuffle(pkmData); + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c164d9b --- /dev/null +++ b/Program.cs @@ -0,0 +1,86 @@ +using PCDWC4Converter; + +Console.WriteLine("PCDWC4Converter v1.0 by Sabresite"); + +if (args.Length == 0) +{ + Console.WriteLine("Usage: ./pcdwc4converter "); + return; +} + +var path = args[0]; +var fileAttr = File.GetAttributes(path); + +var extensions = new [] { ".pcd", ".wc4" }; +var files = Array.Empty(); + +if ((fileAttr & FileAttributes.Directory) != 0) +{ + files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories) + .Select(file => new FileInfo(file)) + .Where(file => extensions.Contains(file.Extension) && file.Length == 856) + .ToArray(); +} +else if (File.Exists(path)) +{ + var file = new FileInfo(path); + if (extensions.Contains(file.Extension) && file.Length == 856) + { + files = new[] { file }; + } +} + +if (files.Length == 0) +{ + Console.WriteLine("No files found to convert."); + return; +} + +// All PCD data +Span fileData = stackalloc byte[856]; + +for (var i = 0; i < files.Length; i++) +{ + var file = files[i]; + using (var fs = file.OpenRead()) + { + var bytesRead = fs.Read(fileData); + if (bytesRead != 856) + { + Console.WriteLine("Failed to read file {0}", file.FullName); + continue; + } + } + + var newExtension = ""; + + switch (file.Extension) + { + case ".pcd": + { + PokemonData.DecryptData(fileData[8..]); + newExtension = ".wc4"; + break; + } + case ".wc4": + { + PokemonData.EncryptData(fileData[8..]); + newExtension = ".pcd"; + break; + } + } + + var newFilePath = Path.Join(file.DirectoryName, $"{file.Name[..^file.Extension.Length]}{newExtension}"); + Console.WriteLine("New File {0}", newFilePath); + try + { + File.Delete(newFilePath); + using var writer = new FileStream(newFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + writer.Write(fileData); + } + catch (Exception e) + { + Console.WriteLine("Failed to write file {0}", newFilePath); + Console.WriteLine(e); + } +} diff --git a/README.md b/README.md index a7a3cc1..bc546fc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ -# PCDWC4Converter -Converter for PCD<->WC4 +# PCD/WC4 Converter for Pokemon Gen 4 Wondercards + +## Requirements +* .NET 6 + +### Usage +```bash +./pcdwc4converter +``` + +## Building Solution +* .NET 6 SDK +* Visual Studio 2022 or Rider 2021.3+