PKHeX/PKHeX.WinForms/Subforms/SAV_Encounters.cs
Kurt 88830e0d00
Update from .NET Framework 4.6 to .NET 7 (#3729)
Updates from net46->net7, dropping support for mono in favor of using the latest runtime (along with the performance/API improvements). Releases will be posted as 64bit only for now.

Refactors a good amount of internal API methods to be more performant and more customizable for future updates & fixes.

Adds functionality for Batch Editor commands to `>`, `<` and <=/>=

TID/SID properties renamed to TID16/SID16 for clarity; other properties exposed for Gen7 / display variants.

Main window has a new layout to account for DPI scaling (8 point grid)

Fixed: Tatsugiri and Paldean Tauros now output Showdown form names as Showdown expects
Changed: Gen9 species now interact based on the confirmed National Dex IDs (closes #3724)
Fixed: Pokedex set all no longer clears species with unavailable non-base forms (closes #3720)
Changed: Hyper Training suggestions now apply for level 50 in SV. (closes #3714)
Fixed: B2/W2 hatched egg met locations exclusive to specific versions are now explicitly checked (closes #3691)
Added: Properties for ribbon/mark count (closes #3659)
Fixed: Traded SV eggs are now checked correctly (closes #3692)
2023-01-21 20:02:33 -08:00

490 lines
17 KiB
C#

using PKHeX.Core;
using PKHeX.Core.Searching;
using PKHeX.WinForms.Controls;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Drawing.PokeSprite;
using PKHeX.WinForms.Properties;
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;
public SAV_Encounters(PKMEditor f1, TrainerDatabase db)
{
InitializeComponent();
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(6, 11, 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.ToArray();
// Enable Scrolling when hovered over
foreach (var slot in PKXBOXES)
{
// Enable Click
slot.MouseClick += (sender, e) =>
{
if (sender == null)
return;
if (ModifierKeys == Keys.Control)
ClickView(sender, e);
};
slot.ContextMenuStrip = mnu;
if (Main.Settings.Hover.HoverSlotShowText)
slot.MouseEnter += (o, args) => ShowHoverTextForSlot(slot, args);
}
Counter = L_Count.Text;
L_Viewed.Text = string.Empty; // invis for now
L_Viewed.MouseEnter += (sender, e) => hover.SetToolTip(L_Viewed, L_Viewed.Text);
PopulateComboBoxes();
WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage);
GetTypeFilters();
// Load Data
L_Count.Text = "Ready...";
CenterToParent();
}
private void GetTypeFilters()
{
var types = (EncounterTypeGroup[])Enum.GetValues(typeof(EncounterTypeGroup));
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);
}
}
private EncounterTypeGroup[] GetTypes()
{
return TypeFilters.Controls.OfType<CheckBox>().Where(z => z.Checked).Select(z => z.Name)
.Select(z => (EncounterTypeGroup)Enum.Parse(typeof(EncounterTypeGroup), z)).ToArray();
}
private readonly PictureBox[] PKXBOXES;
private List<IEncounterInfo> Results = new();
private int slotSelected = -1; // = null;
private Image? slotColor;
private const int RES_MAX = 66;
private const int RES_MIN = 6;
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)
{
var pb = WinFormsUtil.GetUnderlyingControl<PictureBox>(sender);
int index = Array.IndexOf(PKXBOXES, 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 pk = enc.ConvertToPKM(trainer, criteria);
pk.RefreshChecksum();
PKME_Tabs.PopulateFields(pk, false);
slotSelected = index;
slotColor = SpriteUtil.Spriter.View;
FillPKXBoxes(SCR_Box.Value);
}
private EncounterCriteria GetCriteria(ISpeciesForm 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 set = new ShowdownSet(editor);
var criteria = EncounterCriteria.GetCriteria(set, editor.PersonalInfo);
if (!isInChain)
criteria = criteria with {Gender = -1}; // 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 DS_Species = new List<ComboItem>(GameInfo.SpeciesDataSource);
DS_Species.RemoveAt(0); DS_Species.Insert(0, Any); CB_Species.DataSource = DS_Species;
// Set the Move ComboBoxes too..
var DS_Move = new List<ComboItem>(GameInfo.MoveDataSource);
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, null);
}
}
var DS_Version = new List<ComboItem>(GameInfo.VersionDataSource);
DS_Version.Insert(0, Any);
DS_Version.RemoveAt(DS_Version.Count - 1);
CB_GameOrigin.DataSource = DS_Version;
// Trigger a Reset
ResetFilters(this, EventArgs.Empty);
}
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();
if (sender == this)
return; // still starting up
foreach (var chk in TypeFilters.Controls.OfType<CheckBox>())
chk.Checked = true;
System.Media.SystemSounds.Asterisk.Play();
}
// View Updates
private IEnumerable<IEncounterInfo> SearchDatabase(CancellationToken token)
{
var settings = GetSearchSettings();
// If nothing is specified, instead of just returning all possible encounters, just return nothing.
if (settings is { Species: 0, Moves.Count: 0 } && Main.Settings.EncounterDb.ReturnNoneIfEmptySearch)
return Array.Empty<IEncounterInfo>();
var pk = SAV.BlankPKM;
var moves = settings.Moves.ToArray();
var versions = settings.GetVersions(SAV);
var species = settings.Species == 0 ? GetFullRange(SAV.MaxSpeciesID) : new[] { settings.Species };
var results = GetAllSpeciesFormEncounters(species, SAV.Personal, versions, moves, pk, token);
if (settings.SearchEgg != null)
results = results.Where(z => z.EggEncounter == settings.SearchEgg);
if (settings.SearchShiny != null)
results = results.Where(z => z.IsShiny == settings.SearchShiny);
// return filtered results
var comparer = new ReferenceComparer<IEncounterInfo>();
results = results.Distinct(comparer); // only distinct objects
if (Main.Settings.EncounterDb.FilterUnavailableSpecies)
{
static bool IsPresentInGameSV (ISpeciesForm pk) => PersonalTable.SV .IsPresentInGame(pk.Species, pk.Form);
static bool IsPresentInGameSWSH(ISpeciesForm pk) => PersonalTable.SWSH.IsPresentInGame(pk.Species, pk.Form);
static bool IsPresentInGameBDSP(ISpeciesForm pk) => PersonalTable.BDSP.IsPresentInGame(pk.Species, pk.Form);
static bool IsPresentInGameLA (ISpeciesForm pk) => PersonalTable.LA .IsPresentInGame(pk.Species, pk.Form);
results = SAV switch
{
SAV9SV => results.Where(IsPresentInGameSV),
SAV8SWSH => results.Where(IsPresentInGameSWSH),
SAV8BS => results.Where(IsPresentInGameBDSP),
SAV8LA => results.Where(IsPresentInGameLA),
_ => results.Where(z => z.Generation <= 7),
};
}
if (token.IsCancellationRequested)
return results;
ReadOnlySpan<char> batchText = RTB_Instructions.Text;
if (batchText.Length > 0 && !StringInstructionSet.HasEmptyLine(batchText))
{
var filters = StringInstruction.GetFilters(batchText);
BatchEditing.ScreenStrings(filters);
results = results.Where(enc => BatchEditing.IsFilterMatch(filters, enc)); // Compare across all filters
}
return results;
}
private static IEnumerable<ushort> GetFullRange(int max)
{
for (ushort i = 1; i <= max; i++)
yield return i;
}
private static IEnumerable<IEncounterInfo> GetAllSpeciesFormEncounters(IEnumerable<ushort> species, IPersonalTable pt, IReadOnlyList<GameVersion> versions, ushort[] 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<T> : IEqualityComparer<T> where T : class
{
public bool Equals(T? x, T? y)
{
if (x == null)
return false;
if (y == null)
return false;
return RuntimeHelpers.GetHashCode(x).Equals(RuntimeHelpers.GetHashCode(y));
}
public int GetHashCode(T obj)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
return RuntimeHelpers.GetHashCode(obj);
}
}
private static IEnumerable<IEncounterInfo> GetEncounters(ushort species, byte form, ushort[] moves, PKM pk, IReadOnlyList<GameVersion> vers)
{
pk.Species = species;
pk.Form = form;
pk.SetGender(pk.GetSaneGender());
return EncounterMovesetGenerator.GenerateEncounters(pk, moves, vers);
}
private SearchSettings GetSearchSettings()
{
var settings = new SearchSettings
{
Format = SAV.Generation, // 0->(n-1) => 1->n
Generation = SAV.Generation,
Species = GetU16(CB_Species),
BatchInstructions = RTB_Instructions.Text,
Version = 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;
}
private async void B_Search_Click(object sender, EventArgs e)
{
B_Search.Enabled = false;
EncounterMovesetGenerator.PriorityList = GetTypes();
var token = TokenSource.Token;
var search = SearchDatabase(token);
if (token.IsCancellationRequested)
return;
var results = await Task.Run(() => search.ToList(), token).ConfigureAwait(true);
if (token.IsCancellationRequested)
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();
}
private void UpdateScroll(object sender, ScrollEventArgs e)
{
if (e.OldValue != e.NewValue)
FillPKXBoxes(e.NewValue);
}
private void SetResults(List<IEncounterInfo> 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 enc = Results[i + begin];
boxes[i].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(object sender, EventArgs e)
{
var pb = (PictureBox)sender;
int index = Array.IndexOf(PKXBOXES, 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);
RTB_Instructions.AppendText(s);
}
}