using PKHeX.Core; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; using PKHeX.Drawing.Misc; using PKHeX.Drawing.PokeSprite; using PKHeX.WinForms.Controls; using static PKHeX.Core.MessageStrings; namespace PKHeX.WinForms; public partial class SAV_Wondercard : Form { private readonly SaveFile Origin; private readonly SaveFile SAV; private readonly SummaryPreviewer Summary = new(); public SAV_Wondercard(SaveFile sav, DataMysteryGift? g = null) { InitializeComponent(); WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage); SAV = (Origin = sav).Clone(); mga = SAV.GiftAlbum; pba = SAV.Generation switch { 4 => PopulateViewGiftsG4().ToArray(), 5 or 6 or 7 => PopulateViewGiftsG567().ToArray(), _ => throw new ArgumentOutOfRangeException(nameof(SAV.Generation), "Game not supported."), }; foreach (var pb in pba) { pb.AllowDrop = true; pb.DragDrop += BoxSlot_DragDrop; pb.DragEnter += BoxSlot_DragEnter; pb.MouseDown += BoxSlot_MouseDown; pb.ContextMenuStrip = mnuVSD; pb.MouseHover += (_, _) => Summary.Show(pb, mga.Gifts[pba.IndexOf(pb)]); pb.Enter += (sender, e) => { var index = pba.IndexOf(pb); if (index < 0) return; var enc = mga.Gifts[index]; pb.AccessibleDescription = string.Join(Environment.NewLine, SummaryPreviewer.GetTextLines(enc)); }; } SetGiftBoxes(); GetReceivedFlags(); if (LB_Received.Items.Count > 0) LB_Received.SelectedIndex = 0; if (mga.Gifts[0] is WR7) // giftused is not a valid prop B_UnusedAll.Visible = B_UsedAll.Visible = L_QR.Visible = false; DragEnter += Main_DragEnter; DragDrop += Main_DragDrop; if (g == null) ClickView(pba[0], EventArgs.Empty); else ViewGiftData(g); } private readonly MysteryGiftAlbum mga; private DataMysteryGift? mg; private readonly IList pba; // Repopulation Functions private void SetBackground(int index, Image bg) { for (int i = 0; i < mga.Gifts.Length; i++) pba[i].BackgroundImage = index == i ? bg : null; } private void SetGiftBoxes() { for (int i = 0; i < mga.Gifts.Length; i++) { MysteryGift m = mga.Gifts[i]; pba[i].Image = m.Sprite(); } } private void ViewGiftData(DataMysteryGift g) { try { // only check if the form is visible (not opening) if (Visible && g.GiftUsed) { var prompt = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgMsyteryGiftUsedAlert, MsgMysteryGiftUsedFix); if (prompt == DialogResult.Yes) g.GiftUsed = false; } RTB.Lines = g.GetDescription().ToArray(); PB_Preview.Image = g.Sprite(); mg = g; } // Some user input mystery gifts can have out-of-bounds values. Just swallow any exception. catch (Exception e) { RTB.Clear(); WinFormsUtil.Error(MsgMysteryGiftParseTypeUnknown, e); } } private void GetReceivedFlags() { LB_Received.Items.Clear(); for (int i = 1; i < mga.Flags.Length; i++) { if (mga.Flags[i]) LB_Received.Items.Add(i.ToString("0000")); } if (LB_Received.Items.Count > 0) LB_Received.SelectedIndex = 0; } private void SetCardID(int cardID) { if (cardID is <= 0 or >= 0x100 * 8) return; string card = cardID.ToString("0000"); if (!LB_Received.Items.Contains(card)) LB_Received.Items.Add(card); LB_Received.SelectedIndex = LB_Received.Items.IndexOf(card); } // Mystery Gift IO (.file<->window) private void B_Import_Click(object sender, EventArgs e) { var fileFilter = WinFormsUtil.GetMysterGiftFilter(SAV.Context); using var import = new OpenFileDialog { Filter = fileFilter }; if (import.ShowDialog() != DialogResult.OK) return; var path = import.FileName; var data = File.ReadAllBytes(path); var ext = Path.GetExtension(path); var gift = MysteryGift.GetMysteryGift(data, ext); if (gift == null) { WinFormsUtil.Error(MsgMysteryGiftInvalid, path); return; } ViewGiftData(gift); } private void B_Output_Click(object sender, EventArgs e) { if (mg == null) return; WinFormsUtil.ExportMGDialog(mg); } private static int GetLastUnfilledByType(MysteryGift gift, MysteryGiftAlbum album) { var gifts = album.Gifts; for (int i = 0; i < gifts.Length; i++) { var exist = gifts[i]; if (!exist.Empty) continue; if (exist.Type != gift.Type) continue; return i; } return -1; } // Mystery Gift RW (window<->sav) private void ClickView(object sender, EventArgs e) { var pb = WinFormsUtil.GetUnderlyingControl(sender); if (pb == null) return; int index = pba.IndexOf(pb); SetBackground(index, Drawing.PokeSprite.Properties.Resources.slotView); ViewGiftData(mga.Gifts[index]); } private void ClickSet(object sender, EventArgs e) { if (mg is not { } gift) return; if (!gift.IsCardCompatible(SAV, out var msg)) { WinFormsUtil.Alert(MsgMysteryGiftSlotFail, msg); return; } var pb = WinFormsUtil.GetUnderlyingControl(sender); if (pb == null) return; int index = pba.IndexOf(pb); // Hijack to the latest unfilled slot if index creates interstitial empty slots. int lastUnfilled = GetLastUnfilledByType(gift, mga); if (lastUnfilled > -1 && lastUnfilled < index) index = lastUnfilled; if (gift is PCD { IsLockCapsule: true }) index = 11; var gifts = mga.Gifts; var other = gifts[index]; if (gift is PCD { CanConvertToPGT: true } pcd && other is PGT) { gift = pcd.Gift; } else if (gift.Type != other.Type) { WinFormsUtil.Alert(MsgMysteryGiftSlotFail, $"{gift.Type} != {other.Type}"); return; } else if (gift is PCD { IsLockCapsule: true } != (index == 11)) { WinFormsUtil.Alert(MsgMysteryGiftSlotFail, $"{GameInfo.Strings.Item[533]} slot not valid."); return; } gifts[index] = (DataMysteryGift)gift.Clone(); SetBackground(index, Drawing.PokeSprite.Properties.Resources.slotSet); SetGiftBoxes(); SetCardID(gift.CardID); } private void ClickDelete(object sender, EventArgs e) { var pb = WinFormsUtil.GetUnderlyingControl(sender); if (pb == null) return; int index = pba.IndexOf(pb); var arr = mga.Gifts[index].Data; Array.Clear(arr, 0, arr.Length); // Shuffle blank card down int i = index; while (i < mga.Gifts.Length - 1) { if (mga.Gifts[i+1].Empty) break; if (mga.Gifts[i+1].Type != mga.Gifts[i].Type) break; i++; var mg1 = mga.Gifts[i]; var mg2 = mga.Gifts[i-1]; mga.Gifts[i-1] = mg1; mga.Gifts[i] = mg2; } SetBackground(i, Drawing.PokeSprite.Properties.Resources.slotDel); SetGiftBoxes(); } // Close Window private void B_Cancel_Click(object sender, EventArgs e) { Close(); } private void B_Save_Click(object sender, EventArgs e) { // Make sure all of the Received Flags are flipped! bool[] flags = new bool[mga.Flags.Length]; foreach (var o in LB_Received.Items) { var value = o?.ToString(); if (value == null) continue; var flag = Util.ToUInt32(value); flags[flag] = true; } flags.CopyTo(mga.Flags, 0); SAV.GiftAlbum = mga; Origin.CopyChangesFrom(SAV); Close(); } // Delete Received Flag private void ClearReceivedFlag(object sender, EventArgs e) { if (LB_Received.SelectedIndex < 0) return; if (LB_Received.SelectedIndices.Count > 1) { for (int i = LB_Received.SelectedIndices.Count - 1; i >= 0; i--) LB_Received.Items.RemoveAt(LB_Received.SelectedIndices[i]); } else if (LB_Received.SelectedIndices.Count == 1) { int lastIndex = LB_Received.SelectedIndex; LB_Received.Items.RemoveAt(lastIndex); if (LB_Received.Items.Count == 0) return; if (lastIndex == LB_Received.Items.Count) lastIndex--; LB_Received.SelectedIndex = lastIndex; } } // Drag & Drop Wonder Cards private static void Main_DragEnter(object? sender, DragEventArgs? e) { if (e?.Data is null) return; if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy; } private void Main_DragDrop(object? sender, DragEventArgs? e) { if (e?.Data?.GetData(DataFormats.FileDrop) is not string[] { Length: not 0 } files) return; var first = files[0]; // Check for multiple wondercards if (Directory.Exists(first)) files = Directory.GetFiles(first, "*", SearchOption.AllDirectories); if (files.Length == 1 && !Directory.Exists(files[0])) { string path = files[0]; // open first D&D if (!MysteryGift.IsMysteryGift(new FileInfo(path).Length)) // arbitrary { WinFormsUtil.Alert(MsgMysteryGiftInvalid, path); return; } var gift = MysteryGift.GetMysteryGift(File.ReadAllBytes(path), Path.GetExtension(path)); if (gift == null) { WinFormsUtil.Error(MsgMysteryGiftInvalid, path); return; } ViewGiftData(gift); return; } SetGiftBoxes(); } private void ClickQR(object sender, EventArgs e) { if (ModifierKeys == Keys.Alt) { string url = Clipboard.GetText(); if (!string.IsNullOrWhiteSpace(url)) { ImportQRToView(url); return; } } ExportQRFromView(); } private void ExportQRFromView() { if (mg == null) return; if (mg.Empty) { WinFormsUtil.Alert(MsgMysteryGiftSlotNone); return; } if (SAV.Generation == 6 && mg.ItemID == 726 && mg.IsItem) { WinFormsUtil.Alert(MsgMysteryGiftQREonTicket, MsgMysteryGiftQREonTicketAdvice); return; } Image qr = QREncode.GenerateQRCode(mg); string desc = $"({mg.Type}) {string.Join(Environment.NewLine, mg.GetDescription())}"; using var form = new QR(qr, PB_Preview.Image, desc + Environment.NewLine + "PKHeX Wonder Card @ ProjectPokemon.org"); form.ShowDialog(); } private void ImportQRToView(string url) { var msg = QRDecode.GetQRData(url, out var data); if (msg != 0) { WinFormsUtil.Alert(msg.ConvertMsg()); return; } if (data.Length == 0) return; string[] types = mga.Gifts.Select(g => g.Type).Distinct().ToArray(); var gift = MysteryGift.GetMysteryGift(data); if (gift == null) return; string giftType = gift.Type; if (mga.Gifts.All(card => card.Data.Length != data.Length)) WinFormsUtil.Alert(MsgMysteryGiftQRTypeLength, string.Format(MsgQRDecodeSize, $"0x{data.Length:X}")); else if (types.All(type => type != giftType)) WinFormsUtil.Alert(MsgMysteryGiftTypeIncompatible, $"{MsgMysteryGiftQRReceived} {gift.Type}{Environment.NewLine}{MsgMysteryGiftTypeUnexpected} {string.Join(", ", types)}"); else if (!SAV.CanReceiveGift(gift)) WinFormsUtil.Alert(MsgMysteryGiftTypeDetails); else ViewGiftData(gift); } private async void BoxSlot_MouseDown(object? sender, MouseEventArgs e) { if (sender == null) return; switch (ModifierKeys) { case Keys.Control: ClickView(sender, e); return; case Keys.Shift: ClickSet(sender, e); return; case Keys.Alt: ClickDelete(sender, e); return; } var pb = sender as PictureBox; if (pb?.Image == null) return; if (e.Button != MouseButtons.Left || e.Clicks != 1) return; int index = pba.IndexOf(pb); var gift = mga.Gifts[index]; if (gift.Empty) return; // Create Temp File to Drag wc_slot = index; Cursor.Current = Cursors.Hand; string newfile = Path.Combine(Path.GetTempPath(), Util.CleanFileName(gift.FileName)); try { await File.WriteAllBytesAsync(newfile, gift.Write()).ConfigureAwait(true); DoDragDrop(new DataObject(DataFormats.FileDrop, new[] { newfile }), DragDropEffects.Copy | DragDropEffects.Move); } // Sometimes the drag-drop is canceled or ends up at a bad location. Don't bother recovering from an exception; just display a safe error message. catch (Exception x) { WinFormsUtil.Error("Drag & Drop Error", x); } wc_slot = -1; await DeleteAsync(newfile, 20_000).ConfigureAwait(false); } private static async Task DeleteAsync(string path, int delay) { await Task.Delay(delay).ConfigureAwait(true); if (!File.Exists(path)) return; try { File.Delete(path); } catch (Exception ex) { Debug.WriteLine(ex.Message); } } private void BoxSlot_DragDrop(object? sender, DragEventArgs? e) { if (mg == null || sender is not PictureBox pb) return; int index = pba.IndexOf(pb); // Hijack to the latest unfilled slot if index creates interstitial empty slots. int lastUnfilled = GetLastUnfilledByType(mg, mga); if (lastUnfilled > -1 && lastUnfilled < index && mga.Gifts[lastUnfilled].Type == mga.Gifts[index].Type) index = lastUnfilled; if (mg is PCD { IsLockCapsule: true }) index = 11; if (wc_slot == -1) // dropped { if (e?.Data?.GetData(DataFormats.FileDrop) is not string[] {Length: not 0} files) return; var first = files[0]; var fi = new FileInfo(first); if (!MysteryGift.IsMysteryGift(fi.Length)) { WinFormsUtil.Alert(MsgFileUnsupported, first); return; } byte[] data = File.ReadAllBytes(first); var gift = MysteryGift.GetMysteryGift(data, fi.Extension); if (gift == null) { WinFormsUtil.Alert(MsgFileUnsupported, first); return; } ref var dest = ref mga.Gifts[index]; if (gift is PCD { CanConvertToPGT: true } pcd && dest is PGT) { gift = pcd.Gift; } else if (gift.Type != dest.Type) { WinFormsUtil.Alert(MsgMysteryGiftSlotFail, $"{gift.Type} != {dest.Type}"); return; } SetBackground(index, Drawing.PokeSprite.Properties.Resources.slotSet); dest = (DataMysteryGift)gift.Clone(); SetCardID(dest.CardID); ViewGiftData(dest); } else // Swap Data { index = SwapSlots(index, wc_slot); if (index == -1) return; } SetBackground(index, Drawing.PokeSprite.Properties.Resources.slotView); SetGiftBoxes(); } private int SwapSlots(int dest, int src) { var gifts = mga.Gifts; var s1 = gifts[dest]; var s2 = gifts[src]; // Double check compatibility of slots if (s1.Type != s2.Type) { if (s2 is PCD { CanConvertToPGT: true } && s1 is PGT) { // Get first empty slot var firstEmpty = Array.FindIndex(gifts, static z => z.Empty); if ((uint)firstEmpty < dest) dest = firstEmpty; // set the PGT to the destination PGT slot instead ViewGiftData(s2); ClickSet(pba[dest], EventArgs.Empty); WinFormsUtil.Alert(string.Format(MsgMysteryGiftSlotAlternate, s2.Type, s1.Type)); } else { WinFormsUtil.Alert(string.Format(MsgMysteryGiftSlotFailSwap, s2.Type, s1.Type)); } return -1; } if ((s1 is PCD && dest == 11) || (s2 is PCD && src == 11)) { WinFormsUtil.Alert(MsgMysteryGiftSlotFail, $"{GameInfo.Strings.Item[533]} swap not valid."); return -1; } // If data is present in both slots, just swap. if (!s1.Empty) { // Swap (gifts[src], gifts[dest]) = (s1, s2); return dest; } // empty slot created, bubble this slot to the end of its list for (int i = src; i != dest; i++) { if (gifts[i + 1].Empty) return i; // done bubbling (gifts[i + 1], gifts[i]) = (gifts[i], gifts[i + 1]); } throw new InvalidOperationException(); // shouldn't ever hit here. } private static void BoxSlot_DragEnter(object? sender, DragEventArgs e) { if (e.AllowedEffect == (DragDropEffects.Copy | DragDropEffects.Link)) // external file e.Effect = DragDropEffects.Copy; else if (e.Data != null) // within e.Effect = DragDropEffects.Move; Debug.WriteLine(e.Effect); } private int wc_slot = -1; // UI Generation private List PopulateViewGiftsG4() { List pb = new(); var spriter = SpriteUtil.Spriter; // Row 1 var f1 = GetFlowLayoutPanel(); f1.Controls.Add(GetLabel($"{nameof(PGT)} 1-6")); for (int i = 0; i < 6; i++) { var p = GetPictureBox(spriter.Width, spriter.Height, $"PGT {i + 1}"); f1.Controls.Add(p); pb.Add(p); } // Row 2 var f2 = GetFlowLayoutPanel(); f2.Controls.Add(GetLabel($"{nameof(PGT)} 7-8")); for (int i = 6; i < 8; i++) { var p = GetPictureBox(spriter.Width, spriter.Height, $"PGT {i + 1}"); f2.Controls.Add(p); pb.Add(p); } // Row 3 var f3 = GetFlowLayoutPanel(); f3.Margin = new Padding(0, 12, 0, 0); f3.Controls.Add(GetLabel($"{nameof(PCD)} 1-3")); for (int i = 8; i < 11; i++) { var p = GetPictureBox(spriter.Width, spriter.Height, $"PCD {i - 7}"); f3.Controls.Add(p); pb.Add(p); } FLP_Gifts.Controls.Add(f1); FLP_Gifts.Controls.Add(f2); FLP_Gifts.Controls.Add(f3); if (mga.Gifts.Length == 12) // lock capsule { // Row 4 var f4 = GetFlowLayoutPanel(); f4.Controls.Add(GetLabel(GameInfo.Strings.Item[533])); // Lock Capsule { var p = GetPictureBox(spriter.Width, spriter.Height, "PCD Lock Capsule"); f4.Controls.Add(p); pb.Add(p); } FLP_Gifts.Controls.Add(f4); } return pb; } private List PopulateViewGiftsG567() { var pb = new List(); const int cellsPerRow = 6; int rows = (int)Math.Ceiling(mga.Gifts.Length / (decimal)cellsPerRow); int countRemaining = mga.Gifts.Length; var spriter = SpriteUtil.Spriter; for (int i = 0; i < rows; i++) { var row = GetFlowLayoutPanel(); int count = cellsPerRow >= countRemaining ? countRemaining : cellsPerRow; countRemaining -= count; int start = (i * cellsPerRow) + 1; row.Controls.Add(GetLabel($"{start}-{start + count - 1}")); for (int j = 0; j < count; j++) { var p = GetPictureBox(spriter.Width, spriter.Height, $"Row {i} Slot {start + j}"); row.Controls.Add(p); pb.Add(p); } FLP_Gifts.Controls.Add(row); } return pb; } private static FlowLayoutPanel GetFlowLayoutPanel() => new() { Width = 490, Height = 60, Padding = new Padding(0), Margin = new Padding(0), }; private static Label GetLabel(string text) => new() { Size = new Size(50, 60), AutoSize = false, TextAlign = ContentAlignment.MiddleRight, Text = text, Padding = new Padding(0), Margin = new Padding(0), }; private static SelectablePictureBox GetPictureBox(int width, int height, string name) => new() { Size = new Size(width + 2, height + 2), // +1 to each side for the FixedSingle border SizeMode = PictureBoxSizeMode.CenterImage, BorderStyle = BorderStyle.FixedSingle, BackColor = SlotUtil.GoodDataColor, Padding = new Padding(0), Margin = new Padding(1), Name = name, AccessibleName = name, AccessibleRole = AccessibleRole.Graphic, }; private void B_ModifyAll_Click(object sender, EventArgs e) { foreach (var g in mga.Gifts) g.GiftUsed = sender == B_UsedAll; SetGiftBoxes(); System.Media.SystemSounds.Asterisk.Play(); } private void LB_Received_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Delete) { if (LB_Received.SelectedIndices.Count > 1) { for (int i = LB_Received.SelectedIndices.Count - 1; i >= 0; i--) LB_Received.Items.RemoveAt(LB_Received.SelectedIndices[i]); } else if (LB_Received.SelectedIndices.Count == 1) { int lastIndex = LB_Received.SelectedIndex; LB_Received.Items.RemoveAt(lastIndex); if (LB_Received.Items.Count == 0) return; if (lastIndex == LB_Received.Items.Count) lastIndex--; LB_Received.SelectedIndex = lastIndex; } } } }