using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using PKHeX.Core; using PKHeX.Core.Searching; using PKHeX.Drawing.PokeSprite; using PKHeX.WinForms.Controls; using PKHeX.WinForms.Properties; using static PKHeX.Core.MessageStrings; namespace PKHeX.WinForms; public partial class SAV_Database : Form { private readonly SaveFile SAV; private readonly SAVEditor BoxView; private readonly PKMEditor PKME_Tabs; private readonly EntityInstructionBuilder UC_Builder; private const int GridWidth = 6; private const int GridHeight = 11; private readonly PictureBox[] PKXBOXES; private readonly string DatabasePath = Main.DatabasePath; private List Results = []; private List RawDB = []; private int slotSelected = -1; // = null; private Image? slotColor; private const int RES_MIN = GridWidth * 1; private const int RES_MAX = GridWidth * GridHeight; private readonly string Counter; private readonly string Viewed; private const int MAXFORMAT = Latest.Generation; private readonly SummaryPreviewer ShowSet = new(); private readonly CancellationTokenSource cts = new(); public SAV_Database(PKMEditor f1, SAVEditor saveditor) { InitializeComponent(); FormClosing += (_, _) => cts.Cancel(); var settings = new TabPage { Text = "Settings", Name = "Tab_Settings" }; settings.Controls.Add(new PropertyGrid { Dock = DockStyle.Fill, SelectedObject = Main.Settings.EntityDb }); TC_SearchSettings.Controls.Add(settings); WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage); UC_Builder = new EntityInstructionBuilder(() => f1.PreparePKM()) { Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, Width = Tab_Advanced.Width, Dock = DockStyle.Top, ReadOnly = true, }; Tab_Advanced.Controls.Add(UC_Builder); UC_Builder.SendToBack(); if (!Directory.Exists(DatabasePath)) Menu_OpenDB.Visible = false; SAV = saveditor.SAV; BoxView = saveditor; PKME_Tabs = f1; // Preset Filters to only show PKM available for loaded save UC_EntitySearch.InitializeSelections(SAV); var grid = DatabasePokeGrid; var smallWidth = grid.Width; var smallHeight = grid.Height; grid.InitializeGrid(GridWidth, GridHeight, SpriteUtil.Spriter); grid.SetBackground(Resources.box_wp_clean); var newWidth = grid.Width; var newHeight = grid.Height; var wdelta = newWidth - smallWidth; if (wdelta != 0) Width += wdelta; var hdelta = newHeight - smallHeight; if (hdelta != 0) Height += hdelta; PKXBOXES = [.. grid.Entries]; // Enable Scrolling when hovered over foreach (var slot in PKXBOXES) { // Enable Click slot.MouseClick += (_, e) => { switch (ModifierKeys) { case Keys.Control: ClickView(slot, e); break; case Keys.Alt: ClickDelete(slot, e); break; case Keys.Shift: ClickSet(slot, e); break; } }; slot.ContextMenuStrip = mnu; if (Main.Settings.Hover.HoverSlotShowText) { slot.MouseMove += (_, args) => ShowSet.UpdatePreviewPosition(args.Location); slot.MouseEnter += (_, _) => ShowHoverTextForSlot(slot); slot.MouseLeave += (_, _) => ShowSet.Clear(); } slot.Enter += (_, _) => { var index = PKXBOXES.IndexOf(slot); if (index < 0) return; index += (SCR_Box.Value * RES_MIN); if (index >= Results.Count) return; var pk = Results[index]; var x = Main.Settings; var programLanguage = Language.GetLanguageValue(x.Startup.Language); var settings = x.BattleTemplate.Hover.GetSettings(programLanguage, pk.Entity.Context); slot.AccessibleDescription = ShowdownParsing.GetLocalizedPreviewText(pk.Entity, settings); }; } Counter = L_Count.Text; Viewed = L_Viewed.Text; L_Viewed.Text = string.Empty; // invisible for now UC_EntitySearch.PopulateComboBoxes(GameInfo.FilteredSources); // Load Data B_Search.Enabled = false; L_Count.Text = "Loading..."; var token = cts.Token; var task = new Task(() => LoadDatabase(token), cts.Token); task.ContinueWith(z => { if (token.IsCancellationRequested || !z.IsFaulted) return; Invoke((MethodInvoker)(() => L_Count.Text = "Failed.")); if (z.Exception is null) return; WinFormsUtil.Error("Loading database failed.", z.Exception.InnerException ?? z.Exception.GetBaseException()); }); task.Start(); Menu_SearchSettings.DropDown.Closing += (_, e) => { if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked) e.Cancel = true; }; UC_EntitySearch.SetFormatAnyText(MsgAny); CenterToParent(); FormClosing += (_, _) => ShowSet.Clear(); if (Application.IsDarkModeEnabled) { WinFormsUtil.InvertToolStripIcons(menuStrip1.Items); WinFormsUtil.InvertToolStripIcons(mnu.Items); } } protected override void OnShown(EventArgs e) { base.OnShown(e); UC_EntitySearch.ResetComboBoxSelections(); } private void ClickView(object sender, EventArgs e) { if (!WinFormsUtil.TryGetUnderlying(sender, out var pb)) ArgumentNullException.ThrowIfNull(pb); int index = PKXBOXES.IndexOf(pb); if (!GetShiftedIndex(ref index)) { System.Media.SystemSounds.Exclamation.Play(); return; } if (sender == mnu) mnu.Hide(); var slot = Results[index]; var temp = slot.Entity; var pk = EntityConverter.ConvertToType(temp, SAV.PKMType, out var c); if (pk is null) { WinFormsUtil.Error(c.GetDisplayString(temp, SAV.PKMType)); return; } SAV.AdaptToSaveFile(pk); pk.RefreshChecksum(); PKME_Tabs.PopulateFields(pk, false); slotSelected = index; slotColor = SpriteUtil.Spriter.View; FillPKXBoxes(SCR_Box.Value); L_Viewed.Text = string.Format(Viewed, slot.Identify()); } private void ClickDelete(object sender, EventArgs e) { if (!WinFormsUtil.TryGetUnderlying(sender, out var pb)) ArgumentNullException.ThrowIfNull(pb); int index = PKXBOXES.IndexOf(pb); if (!GetShiftedIndex(ref index)) { System.Media.SystemSounds.Exclamation.Play(); return; } var entry = Results[index]; var pk = entry.Entity; if (entry.Source is SlotInfoFileSingle(var path)) { // Data from Database: Delete file from disk if (File.Exists(path)) File.Delete(path); } else if (entry.Source is SlotInfoBox b && entry.SAV == SAV) { // Data from Box: Delete from save file var exist = b.Read(SAV); if (!exist.EqualsStored(pk)) // data modified already? { WinFormsUtil.Error(MsgDBDeleteFailModified, MsgDBDeleteFailWarning); return; } BoxView.EditEnv.Slots.Delete(b); } else { WinFormsUtil.Error(MsgDBDeleteFailBackup, MsgDBDeleteFailWarning); return; } // Remove from database. RawDB.Remove(entry); Results.Remove(entry); // Refresh database view. L_Count.Text = string.Format(Counter, Results.Count); slotSelected = -1; FillPKXBoxes(SCR_Box.Value); System.Media.SystemSounds.Asterisk.Play(); } private void ClickSet(object sender, EventArgs e) { // Don't care what slot was clicked, just add it to the database if (!PKME_Tabs.EditsComplete) return; PKM pk = PKME_Tabs.PreparePKM(); Directory.CreateDirectory(DatabasePath); string path = Path.Combine(DatabasePath, PathUtil.CleanFileName(pk.FileName)); if (File.Exists(path)) { WinFormsUtil.Alert(MsgDBAddFailExistsFile); return; } Span data = stackalloc byte[pk.SIZE_STORED]; pk.WriteDecryptedDataStored(data); File.WriteAllBytes(path, data); var info = new SlotInfoFileSingle(path); var entry = new SlotCache(info, pk); Results.Add(entry); // Refresh database view. L_Count.Text = string.Format(Counter, Results.Count); slotSelected = Results.Count - 1; slotColor = SpriteUtil.Spriter.Set; if ((SCR_Box.Maximum + 1) * GridWidth < Results.Count) SCR_Box.Maximum++; SCR_Box.Value = Math.Max(0, SCR_Box.Maximum - (PKXBOXES.Length / GridWidth) + 1); FillPKXBoxes(SCR_Box.Value); WinFormsUtil.Alert(MsgDBAddFromTabsSuccess); } private bool GetShiftedIndex(ref int index) { if ((uint)index >= RES_MAX) return false; index += SCR_Box.Value * RES_MIN; return index < Results.Count; } private void ResetFilters(object sender, EventArgs e) { UC_EntitySearch.ResetFilters(); RTB_Instructions.Clear(); if (sender != this) System.Media.SystemSounds.Asterisk.Play(); } private void GenerateDBReport(object sender, EventArgs e) { if (WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDBCreateReportPrompt, MsgDBCreateReportWarning) != DialogResult.Yes) return; if (this.OpenWindowExists()) return; ReportGrid reportGrid = new(); reportGrid.Show(); var settings = Main.Settings.Report; var extra = CollectionsMarshal.AsSpan(settings.ExtraProperties); var hide = CollectionsMarshal.AsSpan(settings.HiddenProperties); reportGrid.PopulateData(Results, extra, hide); } private sealed class SearchFolderDetail(string path, bool ignoreBackupFiles) { public string Path { get; } = path; public bool IgnoreBackupFiles { get; } = ignoreBackupFiles; } private void LoadDatabase(CancellationToken token) { var settings = Main.Settings; var otherPaths = new List(); if (settings.EntityDb.SearchExtraSaves) otherPaths.AddRange(settings.Backup.OtherBackupPaths.Where(Directory.Exists).Select(z => new SearchFolderDetail(z, true))); if (settings.EntityDb.SearchBackups) otherPaths.Add(new SearchFolderDetail(Main.BackupPath, false)); RawDB = LoadEntitiesFromFolder(DatabasePath, SAV, otherPaths, settings.EntityDb.SearchExtraSavesDeep, token); if (token.IsCancellationRequested) return; // Load stats for pk who do not have any foreach (var entry in RawDB) { var pk = entry.Entity; pk.ForcePartyData(); } try { while (!IsHandleCreated) { } if (cts.Token.IsCancellationRequested) return; BeginInvoke(new MethodInvoker(() => SetResults(RawDB))); } catch { /* Window Closed? */ } } private static List LoadEntitiesFromFolder(string databaseFolder, SaveFile sav, List otherPaths, bool otherDeep, CancellationToken token) { var dbTemp = new ConcurrentBag(); var extensions = new HashSet(EntityFileExtension.GetExtensionsAll().Select(z => $".{z}")); var files = Directory.EnumerateFiles(databaseFolder, "*", SearchOption.AllDirectories); Parallel.ForEach(files, file => SlotInfoLoader.AddFromLocalFile(file, dbTemp, sav, extensions)); foreach (var folder in otherPaths) { if (!SaveUtil.GetSavesFromFolder(folder.Path, otherDeep, token, out var paths, folder.IgnoreBackupFiles)) continue; Parallel.ForEach(paths, file => TryAddPKMsFromSaveFilePath(dbTemp, file)); } // Fetch from save file SlotInfoLoader.AddFromSaveFile(sav, dbTemp); var result = new List(dbTemp); result.RemoveAll(z => !z.IsDataValid()); if (Main.Settings.EntityDb.FilterUnavailableSpecies) { var filter = EntityPresenceFilters.GetFilterEntity(sav.Context); if (filter is not null) result.RemoveAll(z => !filter(z.Entity)); } var sort = Main.Settings.EntityDb.InitialSortMode; if (sort is DatabaseSortMode.SlotIdentity) result.Sort(); else if (sort is DatabaseSortMode.SpeciesForm) result.Sort((first, second) => first.CompareToSpeciesForm(second)); // Finalize the Database return result; } private static void TryAddPKMsFromSaveFilePath(ConcurrentBag dbTemp, string file) { if (SaveUtil.TryGetSaveFile(file, out var sav)) { SlotInfoLoader.AddFromSaveFile(sav, dbTemp); return; } if (FileUtil.TryGetMemoryCard(file, out var mc)) TryAddPKMsFromMemoryCard(dbTemp, mc, file); else Debug.WriteLine($"Unable to load SaveFile: {file}"); } private static void TryAddPKMsFromMemoryCard(ConcurrentBag dbTemp, SAV3GCMemoryCard mc, string file) { var state = mc.GetMemoryCardState(); if (state == MemoryCardSaveStatus.Invalid) return; if (mc.HasCOLO) TryAdd(dbTemp, mc, file, SaveFileType.Colosseum); if (mc.HasXD) TryAdd(dbTemp, mc, file, SaveFileType.XD); if (mc.HasRSBOX) TryAdd(dbTemp, mc, file, SaveFileType.RSBox); static void TryAdd(ConcurrentBag dbTemp, SAV3GCMemoryCard mc, string path, SaveFileType game) { mc.SelectSaveGame(game); if (!SaveUtil.TryGetSaveFile(mc, out var sav)) return; sav.Metadata.SetExtraInfo(path); SlotInfoLoader.AddFromSaveFile(sav, dbTemp); } } // IO Usage private void OpenDB(object sender, EventArgs e) { if (Directory.Exists(DatabasePath)) Process.Start("explorer.exe", DatabasePath); } private void Menu_Export_Click(object sender, EventArgs e) { if (Results.Count == 0) { WinFormsUtil.Alert(MsgDBCreateReportFail); return; } if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDBExportResultsPrompt)) return; using var fbd = new FolderBrowserDialog(); if (DialogResult.OK != fbd.ShowDialog()) return; string path = fbd.SelectedPath; Directory.CreateDirectory(path); Span data = stackalloc byte[SAV.SIZE_PARTY]; foreach (var pk in Results.Select(z => z.Entity)) { var fileName = Path.Combine(path, PathUtil.CleanFileName(pk.FileName)); pk.ForcePartyData(); pk.WriteDecryptedDataParty(data); File.WriteAllBytes(fileName, data); } } private void Menu_Import_Click(object sender, EventArgs e) { if (!BoxView.GetBulkImportSettings(out var clearAll, out var overwrite, out var settings)) return; int box = BoxView.Box.CurrentBox; int ctr = SAV.LoadBoxes(Results.Select(z => z.Entity), out var result, box, clearAll, overwrite, settings); if (ctr <= 0) return; BoxView.SetPKMBoxes(); BoxView.UpdateBoxViewers(); WinFormsUtil.Alert(result); } // View Updates private IEnumerable SearchDatabase() { var settings = GetSearchSettings(); IEnumerable res = RawDB; // pre-filter based on the file path (if specified) if (!Menu_SearchBoxes.Checked) res = res.Where(z => z.SAV != SAV); if (!Menu_SearchDatabase.Checked) res = res.Where(z => !IsIndividualFilePKMDB(z)); if (!Menu_SearchBackups.Checked) res = res.Where(z => !IsBackupSaveFile(z)); // return filtered results return settings.Search(res); } private SearchSettings GetSearchSettings() { var settings = UC_EntitySearch.CreateSearchSettings(RTB_Instructions.Text); if (Menu_SearchLegal.Checked != Menu_SearchIllegal.Checked) settings.SearchLegal = Menu_SearchLegal.Checked; if (Menu_SearchClones.Checked) { settings.SearchClones = ModifierKeys switch { Keys.Control => CloneDetectionMethod.HashPID, _ => CloneDetectionMethod.HashDetails, }; } return settings; } // ReSharper disable once AsyncVoidMethod private async void B_Search_Click(object sender, EventArgs e) { try { B_Search.Enabled = false; var search = SearchDatabase(); bool legalSearch = Menu_SearchLegal.Checked ^ Menu_SearchIllegal.Checked; bool wordFilter = ParseSettings.Settings.WordFilter.CheckWordFilter; if (wordFilter && legalSearch && WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDBSearchLegalityWordfilter) == DialogResult.No) ParseSettings.Settings.WordFilter.CheckWordFilter = false; var results = await Task.Run(search.ToList).ConfigureAwait(true); ParseSettings.Settings.WordFilter.CheckWordFilter = wordFilter; if (results.Count == 0) { if (!Menu_SearchBoxes.Checked && !Menu_SearchDatabase.Checked && !Menu_SearchBackups.Checked) WinFormsUtil.Alert(MsgDBSearchFail, MsgDBSearchNone); else WinFormsUtil.Alert(MsgDBSearchNone); } SetResults(results); // updates Count Label as well. System.Media.SystemSounds.Asterisk.Play(); B_Search.Enabled = true; } catch { // Ignore. } } private void UpdateScroll(object sender, ScrollEventArgs e) { if (e.OldValue == e.NewValue) return; FillPKXBoxes(e.NewValue); ShowSet.Clear(); } private void SetResults(List res) { Results = res; ShowSet.Clear(); SCR_Box.Maximum = (int)Math.Ceiling((decimal)Results.Count / RES_MIN); if (SCR_Box.Maximum > 0) SCR_Box.Maximum--; slotSelected = -1; // reset the slot last viewed SCR_Box.Value = 0; FillPKXBoxes(0); L_Count.Text = string.Format(Counter, Results.Count); B_Search.Enabled = true; } private void FillPKXBoxes(int start) { if (Results.Count == 0) { for (int i = 0; i < RES_MAX; i++) { PKXBOXES[i].Image = null; PKXBOXES[i].BackgroundImage = null; } return; } int begin = start * RES_MIN; int end = Math.Min(RES_MAX, Results.Count - begin); for (int i = 0; i < end; i++) { var slot = Results[i + begin]; var pk = Results[i + begin].Entity; PKXBOXES[i].Image = pk.Sprite(SAV, visibility: GetFlags(pk), storage: slot.Source.Type); } for (int i = end; i < RES_MAX; i++) PKXBOXES[i].Image = null; for (int i = 0; i < RES_MAX; i++) PKXBOXES[i].BackgroundImage = SpriteUtil.Spriter.Transparent; if (slotSelected != -1 && slotSelected >= begin && slotSelected < begin + RES_MAX) PKXBOXES[slotSelected - begin].BackgroundImage = slotColor ?? SpriteUtil.Spriter.View; } private SlotVisibilityType GetFlags(PKM pk, bool ignoreLegality = false) { var result = SlotVisibilityType.None; if (!ignoreLegality) result |= SlotVisibilityType.CheckLegalityIndicate; return result; } // Misc Update Methods private void Menu_Exit_Click(object sender, EventArgs e) => Close(); protected override void OnMouseWheel(MouseEventArgs e) { if (!DatabasePokeGrid.RectangleToScreen(DatabasePokeGrid.ClientRectangle).Contains(MousePosition)) return; int oldval = SCR_Box.Value; int newval = oldval + (e.Delta < 0 ? 1 : -1); if (newval < SCR_Box.Minimum || SCR_Box.Maximum < newval) return; FillPKXBoxes(SCR_Box.Value = newval); ShowSet.Clear(); } private void Menu_DeleteClones_Click(object sender, EventArgs e) { var dr = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDBDeleteCloneWarning + Environment.NewLine + MsgDBDeleteCloneAdvice, MsgContinue); if (dr != DialogResult.Yes) return; var deleted = 0; var db = RawDB.Where(IsIndividualFilePKMDB) .OrderByDescending(GetRevisedTime); const CloneDetectionMethod method = CloneDetectionMethod.HashDetails; var hasher = SearchUtil.GetCloneDetectMethod(method); var duplicates = SearchUtil.GetExtraClones(db, z => hasher(z.Entity)); foreach (var entry in duplicates) { var src = entry.Source; if (src is not SlotInfoFileSingle(var path) || !File.Exists(path)) continue; try { File.Delete(path); ++deleted; } catch (Exception ex) { WinFormsUtil.Error(MsgDBDeleteCloneFail + Environment.NewLine + ex.Message + Environment.NewLine + path); } } var boxClear = new BoxManipClearDuplicate(BoxManipType.DeleteClones, pk => SearchUtil.GetCloneDetectMethod(method)(pk)); var param = new BoxManipParam(0, SAV.BoxCount - 1); int count = boxClear.Execute(SAV, param); deleted += count; if (deleted == 0) { WinFormsUtil.Alert(MsgDBDeleteCloneNone); return; } WinFormsUtil.Alert(string.Format(MsgFileDeleteCount, deleted), MsgWindowClose); BoxView.ReloadSlots(); Close(); } private static DateTime GetRevisedTime(SlotCache arg) { // This isn't displayed to the user, so just return the quickest -- Utc (not local time). var src = arg.Source; if (src is not SlotInfoFileSingle(var path)) return DateTime.UtcNow; return File.GetLastWriteTimeUtc(path); } private bool IsBackupSaveFile(SlotCache pk) => pk.SAV is not FakeSaveFile && pk.SAV != SAV; private bool IsIndividualFilePKMDB(SlotCache pk) => pk.Source is SlotInfoFileSingle(var path) && path.StartsWith(DatabasePath + Path.DirectorySeparatorChar, StringComparison.Ordinal); private void L_Viewed_MouseEnter(object sender, EventArgs e) => hover.SetToolTip(L_Viewed, L_Viewed.Text); private void ShowHoverTextForSlot(PictureBox pb) { int index = PKXBOXES.IndexOf(pb); if (!GetShiftedIndex(ref index)) return; var ent = Results[index]; ShowSet.Show(pb, ent.Entity, ent.Source.Type); } private void B_Add_Click(object sender, EventArgs e) { var s = UC_Builder.Create(); if (s.Length == 0) { WinFormsUtil.Alert(MsgBEPropertyInvalid); return; } // If we already have text, add a new line (except if the last line is blank). var tb = RTB_Instructions; var batchText = tb.Text; if (batchText.Length != 0 && !batchText.EndsWith('\n')) tb.AppendText(Environment.NewLine); tb.AppendText(s); } }