using PKHeX.Core; using PKHeX.Core.Searching; using PKHeX.Drawing.PokeSprite; using PKHeX.WinForms.Controls; using PKHeX.WinForms.Properties; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using static PKHeX.Core.MessageStrings; namespace PKHeX.WinForms; public partial class SAV_Encounters : Form { private readonly PKMEditor PKME_Tabs; private SaveFile SAV => PKME_Tabs.RequestSaveFile; private readonly SummaryPreviewer ShowSet = new(); private readonly TrainerDatabase Trainers; private readonly CancellationTokenSource TokenSource = new(); private readonly EntityInstructionBuilder UC_Builder; private const int GridWidth = 6; private const int GridHeight = 11; // Criteria backing value (edited via PropertyGrid) private EncounterCriteria _criteriaValue = EncounterCriteria.Unrestricted; public SAV_Encounters(PKMEditor f1, TrainerDatabase db) { InitializeComponent(); var settings = new TabPage { Text = "Settings", Name = "Tab_Settings" }; settings.Controls.Add(new PropertyGrid { Dock = DockStyle.Fill, SelectedObject = Main.Settings.EncounterDb }); TC_SearchOptions.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(); PKME_Tabs = f1; Trainers = db; var grid = EncounterPokeGrid; 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) => { if (ModifierKeys == Keys.Control) ClickView(slot, e); }; slot.Enter += (_, _) => { var index = PKXBOXES.IndexOf(slot); if (index < 0) return; index += (SCR_Box.Value * RES_MIN); if (index >= Results.Count) return; var enc = Results[index]; slot.AccessibleDescription = string.Join(Environment.NewLine, enc.GetTextLines()); }; slot.ContextMenuStrip = mnu; if (Main.Settings.Hover.HoverSlotShowText) slot.MouseEnter += (_, _) => ShowHoverTextForSlot(slot); } Counter = L_Count.Text; L_Viewed.Text = string.Empty; // invisible for now L_Viewed.MouseEnter += (_, _) => hover.SetToolTip(L_Viewed, L_Viewed.Text); PopulateComboBoxes(); GetTypeFilters(); // Initialize criteria PropertyGrid with default value UpdateCriteriaPropertyGrid(BuildCriteriaFromTabs()); // Load Data L_Count.Text = "Ready..."; CenterToParent(); CheckIsSearchDisallowed(); if (Application.IsDarkModeEnabled) { WinFormsUtil.InvertToolStripIcons(menuStrip1.Items); WinFormsUtil.InvertToolStripIcons(mnu.Items); } } private void UpdateCriteriaPropertyGrid(EncounterCriteria value) { _criteriaValue = value; PG_Criteria.SelectedObject = _criteriaValue; // box the struct for PropertyGrid } private void PG_Criteria_PropertyValueChanged(object s, PropertyValueChangedEventArgs e) { if (PG_Criteria.SelectedObject is EncounterCriteria crit) _criteriaValue = crit; // unbox updated value back into our field } private void CriteriaReset_Click(object? sender, EventArgs e) { UpdateCriteriaPropertyGrid(EncounterCriteria.Unrestricted); System.Media.SystemSounds.Asterisk.Play(); } private void CriteriaFromTabs_Click(object? sender, EventArgs e) { UpdateCriteriaPropertyGrid(BuildCriteriaFromTabs()); System.Media.SystemSounds.Asterisk.Play(); } private EncounterCriteria BuildCriteriaFromTabs() { var editor = PKME_Tabs.Data; var set = new ShowdownSet(editor); var mutations = EncounterMutationUtil.GetSuggested(editor.Context, set.Level); var criteria = EncounterCriteria.GetCriteria(set, editor.PersonalInfo, mutations); if (editor.Context.IsHyperTrainingAvailable(100)) criteria = criteria.ReviseIVsHyperTrainAvailable(); return criteria; } private void GetTypeFilters() { var types = Enum.GetValues(); var checks = types.Select(z => new CheckBox { Name = z.ToString(), Text = z.ToString(), AutoSize = true, Checked = true, Padding = Padding.Empty, Margin = Padding.Empty, }).ToArray(); foreach (var chk in checks) { TypeFilters.Controls.Add(chk); TypeFilters.SetFlowBreak(chk, true); chk.Click += (_, _) => { if ((ModifierKeys & Keys.Shift) != 0) { foreach (var c in TypeFilters.Controls.OfType()) c.Checked = c == chk; } }; chk.CheckStateChanged += (_, _) => CheckIsSearchDisallowed(); } } private EncounterTypeGroup[] GetTypes() { return TypeFilters.Controls.OfType().Where(z => z.Checked).Select(z => z.Name) .Select(Enum.Parse).ToArray(); } private readonly PictureBox[] PKXBOXES; private List Results = []; 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 bool GetShiftedIndex(ref int index) { if (index >= RES_MAX) return false; index += SCR_Box.Value * RES_MIN; return index < Results.Count; } // Important Events private void ClickView(object sender, EventArgs e) { if (!WinFormsUtil.TryGetUnderlying(sender, out var pb)) ArgumentNullException.ThrowIfNull(pb); int index = PKXBOXES.IndexOf(pb); if (index >= RES_MAX) { System.Media.SystemSounds.Exclamation.Play(); return; } index += SCR_Box.Value * RES_MIN; if (index >= Results.Count) { System.Media.SystemSounds.Exclamation.Play(); return; } var enc = Results[index]; var criteria = GetCriteria(enc, Main.Settings.EncounterDb); var trainer = Trainers.GetTrainer(enc.Version, enc.Generation <= 2 ? (LanguageID)SAV.Language : null) ?? SAV; var temp = enc.ConvertToPKM(trainer, criteria); 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); } private EncounterCriteria GetCriteria(IEncounterTemplate enc, EncounterDatabaseSettings settings) { if (!settings.UseTabsAsCriteria) return EncounterCriteria.Unrestricted; var editor = PKME_Tabs.Data; var tree = EvolutionTree.GetEvolutionTree(editor.Context); bool isInChain = tree.IsSpeciesDerivedFrom(editor.Species, editor.Form, enc.Species, enc.Form); if (!settings.UseTabsAsCriteriaAnySpecies) { if (!isInChain) return EncounterCriteria.Unrestricted; } var criteria = _criteriaValue; if (!isInChain || EntityGender.IsSingleGender(enc.Species)) criteria = criteria with { Gender = Gender.Random }; // Genderless tabs and a gendered enc -> let's play safe. return criteria; } private void PopulateComboBoxes() { // Set the Text CB_Species.InitializeBinding(); CB_GameOrigin.InitializeBinding(); var Any = new ComboItem(MsgAny, 0); var filtered = GameInfo.FilteredSources; var source = filtered.Source; var species = new List(source.SpeciesDataSource) { [0] = Any // Replace (None) with "Any" }; CB_Species.DataSource = species; // Set the Move ComboBoxes too. var DS_Move = new List(filtered.Moves); DS_Move.RemoveAt(0); DS_Move.Insert(0, Any); { foreach (ComboBox cb in new[] { CB_Move1, CB_Move2, CB_Move3, CB_Move4 }) { cb.InitializeBinding(); cb.DataSource = new BindingSource(DS_Move, string.Empty); } } var DS_Version = new List(source.VersionDataSource); DS_Version.Insert(0, Any); DS_Version.RemoveAt(DS_Version.Count - 1); CB_GameOrigin.DataSource = DS_Version; } private void ResetFilters(object sender, EventArgs e) { CB_Species.SelectedIndex = 0; CB_Move1.SelectedIndex = CB_Move2.SelectedIndex = CB_Move3.SelectedIndex = CB_Move4.SelectedIndex = 0; CB_GameOrigin.SelectedIndex = 0; RTB_Instructions.Clear(); CHK_Shiny.CheckState = CHK_IsEgg.CheckState = CheckState.Indeterminate; foreach (var chk in TypeFilters.Controls.OfType()) chk.Checked = true; System.Media.SystemSounds.Asterisk.Play(); } protected override void OnShown(EventArgs e) { base.OnShown(e); foreach (var cb in TLP_Filters.Controls.OfType()) cb.SelectedIndex = cb.SelectionLength = 0; } // View Updates private IEnumerable SearchDatabase(CancellationToken token) { var settings = GetSearchSettings(); // If nothing is specified, instead of just returning all possible encounters, just return nothing. if (DisallowSearch(settings)) return []; var pk = SAV.BlankPKM; var moves = settings.Moves.ToArray(); var versions = settings.GetVersions(SAV); var species = settings.Species == 0 ? GetFullRange(SAV.MaxSpeciesID) : [settings.Species]; var results = GetAllSpeciesFormEncounters(species, SAV.Personal, versions, moves, pk, token); if (settings.SearchEgg is not null) results = results.Where(z => z.IsEgg == settings.SearchEgg); if (settings.SearchShiny is not null) results = results.Where(z => z.IsShiny == settings.SearchShiny); // return filtered results var comparer = new ReferenceComparer(); results = results.Distinct(comparer); // only distinct objects if (Main.Settings.EncounterDb.FilterUnavailableSpecies) { var filter = EntityPresenceFilters.GetFilterGeneric(SAV.Context); if (filter != null) results = results.Where(filter); } if (token.IsCancellationRequested) return results; ReadOnlySpan batchText = RTB_Instructions.Text; if (batchText.Length != 0 && !StringInstructionSet.HasEmptyLine(batchText)) { var filters = StringInstruction.GetFilters(batchText); EntityBatchEditor.ScreenStrings(filters); results = results.Where(enc => BatchEditingUtil.IsFilterMatch(filters, enc)); // Compare across all filters } return results; } private bool DisallowSearch(SearchSettings settings) { if (TypeFilters.Controls.OfType().All(z => !z.Checked)) return false; // no types selected return settings is { Species: 0, Moves.Count: 0 } && Main.Settings.EncounterDb.ReturnNoneIfEmptySearch; } private static IEnumerable GetFullRange(int max) { for (ushort i = 1; i <= max; i++) yield return i; } private IEnumerable GetAllSpeciesFormEncounters(IEnumerable species, IPersonalTable pt, ReadOnlyMemory versions, ReadOnlyMemory moves, PKM pk, CancellationToken token) { foreach (var s in species) { if (token.IsCancellationRequested) break; var pi = pt.GetFormEntry(s, 0); var fc = pi.FormCount; if (fc == 0 && !Main.Settings.EncounterDb.FilterUnavailableSpecies) // not present in game { // try again using past-gen table pi = PersonalTable.USUM.GetFormEntry(s, 0); fc = pi.FormCount; } for (byte f = 0; f < fc; f++) { if (FormInfo.IsBattleOnlyForm(s, f, pk.Format)) continue; var encs = GetEncounters(s, f, moves, pk, versions); foreach (var enc in encs) yield return enc; } } } private sealed class ReferenceComparer : IEqualityComparer where T : class { public bool Equals([NotNullWhen(true)] T? x, [NotNullWhen(true)] T? y) { if (x is null) return false; if (y is null) return false; return RuntimeHelpers.GetHashCode(x).Equals(RuntimeHelpers.GetHashCode(y)); } public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); } private IEnumerable GetEncounters(ushort species, byte form, ReadOnlyMemory moves, PKM pk, ReadOnlyMemory vers) { pk.Species = species; pk.Form = form; pk.SetGender(pk.GetSaneGender()); EncounterMovesetGenerator.OptimizeCriteria(pk, SAV); return EncounterMovesetGenerator.GenerateEncounters(pk, moves, vers); } private SearchSettings GetSearchSettings() { var settings = new SearchSettings { Context = SAV.Context, Generation = SAV.Generation, Species = GetU16(CB_Species), BatchInstructions = RTB_Instructions.Text, Version = (GameVersion)WinFormsUtil.GetIndex(CB_GameOrigin), }; static ushort GetU16(ListControl cb) { var val = WinFormsUtil.GetIndex(cb); if (val <= 0) return 0; return (ushort)val; } settings.AddMove(GetU16(CB_Move1)); settings.AddMove(GetU16(CB_Move2)); settings.AddMove(GetU16(CB_Move3)); settings.AddMove(GetU16(CB_Move4)); if (CHK_IsEgg.CheckState != CheckState.Indeterminate) settings.SearchEgg = CHK_IsEgg.CheckState == CheckState.Checked; if (CHK_Shiny.CheckState != CheckState.Indeterminate) settings.SearchShiny = CHK_Shiny.CheckState == CheckState.Checked; return settings; } // ReSharper disable once AsyncVoidMethod private async void B_Search_Click(object sender, EventArgs e) { try { B_Search.Enabled = false; EncounterMovesetGenerator.PriorityList = GetTypes(); var token = TokenSource.Token; var search = SearchDatabase(token); if (token.IsCancellationRequested) { EncounterMovesetGenerator.ResetFilters(); return; } var results = await Task.Run(search.ToList, token).ConfigureAwait(true); if (token.IsCancellationRequested) { EncounterMovesetGenerator.ResetFilters(); return; } if (results.Count == 0) WinFormsUtil.Alert(MsgDBSearchNone); SetResults(results); // updates Count Label as well. System.Media.SystemSounds.Asterisk.Play(); B_Search.Enabled = true; EncounterMovesetGenerator.ResetFilters(); } catch { // Ignore. } } private void UpdateScroll(object sender, ScrollEventArgs e) { if (e.OldValue != e.NewValue) FillPKXBoxes(e.NewValue); } 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) { var boxes = PKXBOXES; if (Results.Count == 0) { for (int i = 0; i < RES_MAX; i++) { boxes[i].Image = null; boxes[i].BackgroundImage = null; } return; } // Load new sprites int begin = start * RES_MIN; int end = Math.Min(RES_MAX, Results.Count - begin); for (int i = 0; i < end; i++) { var pb = boxes[i]; var enc = Results[i + begin]; pb.Image = enc.Sprite(); } // Clear empty slots for (int i = end; i < RES_MAX; i++) boxes[i].Image = null; // Reset backgrounds for all for (int i = 0; i < RES_MAX; i++) boxes[i].BackgroundImage = SpriteUtil.Spriter.Transparent; // Reload last viewed index's background if still within view if (slotSelected != -1 && slotSelected >= begin && slotSelected < begin + RES_MAX) boxes[slotSelected - begin].BackgroundImage = slotColor ?? SpriteUtil.Spriter.View; } private void Menu_Exit_Click(object sender, EventArgs e) => Close(); protected override void OnMouseWheel(MouseEventArgs e) { if (!EncounterPokeGrid.RectangleToScreen(EncounterPokeGrid.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) FillPKXBoxes(SCR_Box.Value = newval); } private void ShowHoverTextForSlot(PictureBox pb) { int index = PKXBOXES.IndexOf(pb); if (!GetShiftedIndex(ref index)) return; ShowSet.Show(pb, Results[index]); } private void SAV_Encounters_FormClosing(object sender, FormClosingEventArgs e) => TokenSource.Cancel(); 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); } private void CB_Species_SelectedIndexChanged(object sender, EventArgs e) => CheckIsSearchDisallowed(); private void CheckIsSearchDisallowed() { var settings = GetSearchSettings(); B_Search.Enabled = !DisallowSearch(settings); } }