using System.Buffers.Binary; using System.Globalization; using System.Text; using Enums; namespace SwitchGiftDataManager.Core; public class BCATManager { public const string Version = "1.5.0"; private const int FileNameOffset = 0x00; private const int UnkOffset = 0x20; private const int FileSizeOffset = 0x28; private const int ChecksumOffset = 0x30; private const int MaxFileNameLength = 0x1F; private const int WCMetaInfoLength = 0x80; private const int MetaHeaderLength = 0x04; private Games Game { get; set; } private List? WCList { get; set; } public BCATManager(Games game) { Game = game; WCList = new(); } private Wondercard CreateWondercard(ReadOnlySpan data) { return Game switch { Games.LGPE => new WB7(data), Games.SWSH => new WC8(data), Games.BDSP => new WB8(data), Games.PLA => new WA8(data), Games.SCVI => new WC9(data), _ => throw new ArgumentOutOfRangeException(nameof(Game)), }; } public bool TryAddWondercards(ReadOnlySpan data, bool sort = false) { var success = false; if (Game is not Games.None && WCList is not null) if (Game is not Games.LGPE || (Game is Games.LGPE && WCList.Count == 0)) { var game = GetCompatibleGamesFromWC(data); if (game == Game) { var size = (int)Wondercard.GetSize(game); var qty = data.Length / size; for (int i = 0; i < qty; i++) { var wc = CreateWondercard(data[(i * size)..((i + 1) * size)]); if (wc.IsValid()) { WCList.Add(wc); success = true; } } } if (sort) Sort(); } return success; } public void Sort() => WCList!.Sort((x, y) => x.WCID.CompareTo(y.WCID)); public int Count() { if (WCList is not null) return WCList.Count; return 0; } public void RemoveWC(int index) { if (WCList is not null && WCList.Count > 0 && WCList.Count >= index - 1) WCList.RemoveAt(index); } public void Reset() { if(WCList is not null) WCList.Clear(); else WCList = new List(); } public void SetWCID(int index, ushort wcid) { if (WCList is not null && WCList.Count > 0 && WCList.Count >= index - 1 && index >= 0) { var wc = WCList.ElementAt(index); wc.SetID(wcid); } } public ushort GetWCID(int index) { if (WCList is not null && WCList.Count > 0 && WCList.Count >= index - 1 && index >= 0) return WCList.ElementAt(index).WCID; return 0; } public void SetIsRepeatable(int index, bool repeatable) { if (WCList is not null && WCList.Count > 0 && WCList.Count >= index - 1 && index >= 0) { var wc = WCList.ElementAt(index); wc.SetRepeatable(repeatable); } } public bool GetIsRepeatable(int index) { if (WCList is not null && WCList.Count > 0 && WCList.Count >= index - 1 && index >= 0) return WCList.ElementAt(index).IsRepeatable; return false; } public int GetIndex(ushort wcid) { if (WCList is not null && WCList.Count > 0) return WCList.FindIndex((x) => x.WCID == wcid); else throw new IndexOutOfRangeException(); } public List GetListNames() { List list = new(); if (WCList is not null && WCList.Count > 0) foreach (var wc in WCList) { var str = wc.Type!.ToString()!.Equals("Pokemon") ? ((PokemonGift)wc.Content!).GetSpeciesName().Replace("\r","") : wc.Type!.ToString()!; list.Add($"{Game} #{wc.WCID}: {str}"); } return list; } public List GetContentToString(int index) { var wcid = ""; var el1 = ""; var el2 = ""; var el3 = ""; var el4 = ""; var el5 = ""; var el6 = ""; var el7 = ""; if (WCList is not null && WCList.Count > 0 && WCList.Count >= index) { var wc = WCList.ElementAt(index); wcid = wc.WCID.ToString(); var content = wc.Content; if (content!.GetType() == typeof(PokemonGift)) { var c = (PokemonGift)content; el1 = GetShinyString(c.ShinyType); el2 = c.GetSpeciesName(); el3 = GetPIDString(c.PIDType); } else if (content!.GetType() == typeof(List)) { foreach (var c in (List)content) { var item = c.GetItemName().Replace("\r",""); var qty = c.Quantity; var str = $"{item.Replace("BP", "Battle Points").Replace("LP", "League Points")}"; if (qty != 0x0 && qty != ushort.MaxValue) str = $"{str} x{qty}"; if (el1.Equals("")) el1 = str; else if (el2.Equals("")) el2 = str; else if (el3.Equals("")) el3 = str; else if (el4.Equals("")) el4 = str; else if (el5.Equals("")) el5 = str; else if (el6.Equals("")) el6 = str; else if (el7.Equals("")) el7 = str; } } } return new List { wcid, el1, el2, el3, el4, el5, el6, el7 }; } public List? GetDuplicatedWCID() { if (WCList is not null && WCList.Count > 1) { var l = WCList.GroupBy(x => x.WCID) .Where(g => g.Count() > 1) .Select(y => y.Key) .ToList(); return l; } else return new List(); } public string GetDefaultBcatFolderName() => GetDefaultBcatFolderName(this.Game); public string GetDefaultBcatFileName() => GetDefaultBcatFileName(this.Game); public bool TrySaveAllWondercards(string path) { if (WCList is null || WCList.Count == 0) return false; for (int i = 0; i < WCList.Count; i++) if (!TrySaveWondercard(i, path)) return false; return true; } public bool TrySaveWondercard(int index, string path) { try { var wc = WCList!.ElementAt(index); File.WriteAllBytes(Path.Combine(path, ForgeWcFileName(wc)), wc.Data!); return true; } catch (Exception) { return false; } } public ReadOnlySpan ConcatenateFiles() { var size = (int)Wondercard.GetSize(Game); var data = new byte[size * WCList!.Count]; foreach (var el in WCList!.Select((wc, i) => new { i, wc })) el.wc.Data!.CopyTo(data, el.i * size); return data.AsSpan(); } public ReadOnlySpan ForgeMetaInfo(object? data = null) { if (data is not null && data.GetType() == typeof(byte[])) { var fileName = System.Text.Encoding.UTF8.GetBytes(GetDefaultBcatFileName()); var fileSize = (uint)((byte[])data).Length; var checksum = ChecksumCalculator.CalcReverseMD5((byte[])data); var metainfo = new byte[WCMetaInfoLength]; fileName.CopyTo(metainfo, FileNameOffset); BinaryPrimitives.WriteUInt32LittleEndian(metainfo.AsSpan(UnkOffset), uint.MaxValue); BinaryPrimitives.WriteUInt32LittleEndian(metainfo.AsSpan(FileSizeOffset), fileSize); checksum.ToArray().CopyTo(metainfo, ChecksumOffset); return ForgeMetaInfoHeader(metainfo); } else { var metainfo = new byte[WCMetaInfoLength * WCList!.Count]; foreach (var el in WCList!.Select((wc, i) => new { i, wc })) ForgeWcMetaInfo(el.wc).ToArray().CopyTo(metainfo, el.i * WCMetaInfoLength); return ForgeMetaInfoHeader(metainfo); } } private static ReadOnlySpan ForgeWcMetaInfo(Wondercard wc) { var fileName = System.Text.Encoding.UTF8.GetBytes(ForgeWcFileName(wc)); var fileSize = (uint)Wondercard.GetSize(wc.Game); var checksum = wc.CalcMetaChecksum(); var data = new byte[WCMetaInfoLength]; fileName.CopyTo(data, FileNameOffset); BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(UnkOffset), uint.MaxValue); BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(FileSizeOffset), fileSize); checksum.ToArray().CopyTo(data, ChecksumOffset); return data.AsSpan(); } private static string ForgeWcFileName(Wondercard wc) { var type = wc.Type!.ToString()!; var content = type.Equals("Pokemon") ? $"{((PokemonGift)wc.Content!).GetSpeciesName().Replace("\r", "")}" : type; var str = $"{wc.WCID:0000}_{content}"; var sb = new System.Text.StringBuilder(); foreach (char c in str) if (c != '\\' && c != '/' && c != ':' && c != '*' && c != '?' && c != '"' && c != '<' && c != '>' && c != '|') sb.Append(c); var _sb = new System.Text.StringBuilder(); foreach (char c in sb.ToString().Normalize(NormalizationForm.FormD)) if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) _sb.Append(c); return _sb.ToString().ToLower(); } private static ReadOnlySpan ForgeMetaInfoHeader(ReadOnlySpan data) { uint header = 0x1; byte[] filesDotMeta = new byte[data.Length + MetaHeaderLength]; BinaryPrimitives.WriteUInt32LittleEndian(filesDotMeta.AsSpan(), header); data.ToArray().CopyTo(filesDotMeta, MetaHeaderLength); return filesDotMeta.AsSpan(); } public static Games GetCompatibleGamesFromWC(ReadOnlySpan data) { if (data.Length % (int)Wondercard.GetSize(Games.LGPE) == 0) return Games.LGPE; else if (data.Length % (int)Wondercard.GetSize(Games.SWSH) == 0) return Games.SWSH; else if (data.Length % (int)Wondercard.GetSize(Games.BDSP) == 0) return Games.BDSP; else if (data.Length % (int)Wondercard.GetSize(Games.PLA) == 0 && data[Wondercard.GenOffset] != 0) return Games.PLA; else if (data.Length % (int)Wondercard.GetSize(Games.SCVI) == 0 && data[Wondercard.GenOffset] == 0) return Games.SCVI; else return Games.None; } public static string GetDefaultBcatFolderName(Games game) { return game switch { Games.LGPE => "normal", Games.SWSH => "normal", Games.BDSP => "99", Games.PLA => "normal", Games.SCVI => "normal", _ => throw new ArgumentOutOfRangeException(nameof(game)), }; } public static string GetDefaultBcatFileName(Games game) { return game switch { Games.SWSH => "distribution_internet", Games.BDSP => "99", Games.PLA => "distribution_internet", Games.SCVI => "distribution_internet", _ => throw new ArgumentOutOfRangeException(nameof(game)), }; } private static string GetShinyString(ShinyType type) { return type switch { ShinyType.ShinyLocked => "Shiny Locked", ShinyType.ShinyPossible => "Shiny Standard Odds", ShinyType.ShinyHighOdds => "Shiny High Odds", ShinyType.ShinyForced => "Shiny Forced", ShinyType.ShinyTIDAbuse => "Shiny Possible (TID Abuse)", _ => throw new IndexOutOfRangeException() }; } private static string GetPIDString(PIDType type) { return type switch { PIDType.FixedPID => "Fixed PID", PIDType.RandomPID => "Random PID", _ => throw new IndexOutOfRangeException() }; } }