PKHeX/PKHeX.WinForms/MainWindow/Main.cs
Kurt 8b08f263e5 Minor tweaks
Remove GameSync/SecureValue from SAV tab (still lives in Block Data)
Remove inaccessible items from FRLG/E key items
2026-03-08 23:40:56 -05:00

1394 lines
47 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Media;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Core;
using PKHeX.Drawing;
using PKHeX.Drawing.Misc;
using PKHeX.Drawing.PokeSprite;
using PKHeX.WinForms.Controls;
using static PKHeX.Core.MessageStrings;
namespace PKHeX.WinForms;
public partial class Main : Form
{
public Main()
{
InitializeComponent();
if (Settings.Display.DisableScalingDpi)
AutoScaleMode = AutoScaleMode.Font;
C_SAV.SetEditEnvironment(new SaveDataEditor<PictureBox>(FakeSaveFile.Default, PKME_Tabs));
FormLoadAddEvents();
#if DEBUG // translation updater -- all controls are added at this point -- call translate now
if (DevUtil.IsUpdatingTranslations)
{
WinFormsUtil.TranslateInterface(this, CurrentLanguage); // Translate the UI to language.
return;
}
#endif
FormInitializeSecond();
}
#region Important Variables
public static string CurrentLanguage
{
get => GameInfo.CurrentLanguage;
private set => GameInfo.CurrentLanguage = value;
}
public static bool Unicode
{
get;
private set
{
field = value;
GenderSymbols = value ? GameInfo.GenderSymbolUnicode : GameInfo.GenderSymbolASCII;
}
}
public static IReadOnlyList<string> GenderSymbols { get; private set; } = GameInfo.GenderSymbolUnicode;
public static bool HaX => Program.HaX;
private static List<IPlugin> Plugins { get; } = [];
private static PluginLoadResult? PluginLoadResult { get; set; } // keep alive so that plugins may load their external dependencies if needed
#endregion
#region Path Variables
public static string DatabasePath => Settings.LocalResources.GetDatabasePath();
public static string MGDatabasePath => Settings.LocalResources.GetMGDatabasePath();
public static string BackupPath => Settings.LocalResources.GetBackupPath();
public static string CryPath => Settings.LocalResources.GetCryPath();
private static string TemplatePath => Settings.LocalResources.GetTemplatePath();
private static string TrainerPath => Settings.LocalResources.GetTrainerPath();
private const string ThreadPath = "https://projectpokemon.org/pkhex/";
public static PKHeXSettings Settings => Program.Settings;
#endregion
#region //// MAIN MENU FUNCTIONS ////
private void FormLoadAddEvents()
{
C_SAV.Menu_Redo = Menu_Redo;
C_SAV.Menu_Undo = Menu_Undo;
dragout.GiveFeedback += (_, e) => e.UseDefaultCursors = false;
GiveFeedback += (_, e) => e.UseDefaultCursors = false;
PKME_Tabs.EnableDragDrop(Main_DragEnter, Main_DragDrop);
C_SAV.EnableDragDrop(Main_DragEnter, Main_DragDrop);
menuStrip1.AllowDrop = true;
menuStrip1.DragEnter += Main_DragEnter;
menuStrip1.DragDrop += Main_DragDrop;
PB_Legal.AllowDrop = true;
PB_Legal.DragEnter += Main_DragEnter;
PB_Legal.DragDrop += Main_DragDrop;
// ToolTips for Drag&Drop
toolTip.SetToolTip(dragout, "Drag to Save");
// Box to Tabs D&D
dragout.AllowDrop = true;
// Add ContextMenus
var mnu = new ContextMenuPKM();
mnu.RequestEditorLegality += ClickLegality;
mnu.RequestEditorQR += ClickQR;
mnu.RequestEditorSaveAs += MainMenuSave;
dragout.ContextMenuStrip = mnu.mnuL;
C_SAV.menu.RequestEditorLegality = DisplayLegalityReport;
}
public void LoadInitialFiles(StartupArguments args)
{
var sav = args.SAV!;
var path = sav.Metadata.FilePath ?? string.Empty;
OpenSAV(sav, path);
var pk = args.Entity!;
OpenPKM(pk);
if (args.Error is { } ex)
ErrorWindow.ShowErrorDialog(MsgFileLoadFailAuto, ex, true);
}
private void LoadBlankSaveFile(GameVersion version)
{
if (!version.IsValidSavedVersion())
version = Latest.Version;
var current = C_SAV?.SAV;
var sav = BlankSaveFile.Get(version, current);
OpenSAV(sav, string.Empty);
C_SAV!.SAV.State.Edited = false; // Prevents form close warning from showing until changes are made
}
public async Task CheckForUpdates()
{
Version? latestVersion;
// User might not be connected to the internet or with a flaky connection.
try { latestVersion = UpdateUtil.GetLatestPKHeXVersion(); }
catch (Exception ex)
{
Debug.WriteLine($"Exception while checking for latest version: {ex}");
return;
}
if (latestVersion is null || latestVersion <= Program.CurrentVersion)
return;
while (!IsHandleCreated) // Wait for form to be ready
await Task.Delay(2_000).ConfigureAwait(false);
await InvokeAsync(() => NotifyNewVersionAvailable(latestVersion)).ConfigureAwait(false); // invoke on GUI thread
}
private void NotifyNewVersionAvailable(Version version)
{
var date = $"{2000 + version.Major:00}{version.Minor:00}{version.Build:00}";
var lbl = L_UpdateAvailable;
lbl.Text = $"{MsgProgramUpdateAvailable} {date}";
lbl.Click += (_, _) => Process.Start(new ProcessStartInfo(ThreadPath) { UseShellExecute = true });
lbl.Visible = lbl.TabStop = lbl.Enabled = true;
}
public static DrawConfig Draw { get; private set; } = new();
private void FormInitializeSecond()
{
var settings = Settings;
Draw = C_SAV.M.Hover.Draw = PKME_Tabs.Draw = settings.Draw;
ReloadProgramSettings(settings, true);
CB_MainLanguage.Items.AddRange(Enum.GetNames<ProgramLanguage>());
PB_Legal.Visible = !HaX;
C_SAV.HaX = PKME_Tabs.HaX = HaX;
#if DEBUG
DevUtil.AddDeveloperControls(Menu_Tools, Plugins);
#endif
// Select Language
CB_MainLanguage.SelectedIndex = GameLanguage.GetLanguageIndex(settings.Startup.Language);
if (Application.IsDarkModeEnabled)
WinFormsUtil.InvertToolStripIcons(menuStrip1.Items);
}
public void AttachPlugins()
{
var folder = Settings.LocalResources.GetPluginPath();
if (Plugins.Count != 0)
return; // already loaded
if (!Directory.Exists(folder))
return;
try
{
PluginLoadResult = PluginLoader.LoadPlugins(folder, Plugins, Settings.Startup.PluginLoadMerged);
}
catch (InvalidCastException c)
{
WinFormsUtil.Error(MsgPluginFailLoad, c);
return;
}
var list = Plugins.OrderBy(z => z.Priority);
foreach (var p in list)
{
try
{
p.Initialize(C_SAV, PKME_Tabs, menuStrip1, Program.CurrentVersion);
}
catch (Exception ex)
{
WinFormsUtil.Error(MsgPluginFailLoad, ex);
Plugins.Remove(p);
}
}
}
// Main Menu Strip UI Functions
private void MainMenuOpen(object sender, EventArgs e)
{
if (WinFormsUtil.OpenSAVPKMDialog(C_SAV.SAV.PKMExtensions, out var path))
OpenQuick(path);
}
private void MainMenuSave(object? sender, EventArgs e)
{
if (!PKME_Tabs.EditsComplete)
return;
PKM pk = PreparePKM();
WinFormsUtil.SavePKMDialog(pk);
}
private void MainMenuExit(object sender, EventArgs e)
{
if (ModifierKeys == Keys.Control) // triggered via hotkey
{
if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgConfirmQuitProgram))
return;
}
Close();
}
private void MainMenuAbout(object sender, EventArgs e) => ShowAboutDialog(AboutPage.Shortcuts);
public void ShowAboutDialog(AboutPage index)
{
using var form = new About(index);
form.ShowDialog();
}
// Sub Menu Options
private void MainMenuBoxReport(object sender, EventArgs e)
{
if (this.OpenWindowExists<ReportGrid>())
return;
var report = new ReportGrid();
report.Show();
var list = new List<SlotCache>();
SlotInfoLoader.AddFromSaveFile(C_SAV.SAV, list);
var settings = Settings.Report;
var extra = CollectionsMarshal.AsSpan(settings.ExtraProperties);
var hide = CollectionsMarshal.AsSpan(settings.HiddenProperties);
report.PopulateData(list, extra, hide);
}
private void MainMenuDatabase(object sender, EventArgs e)
{
if (ModifierKeys == Keys.Shift)
{
if (!this.OpenWindowExists<KChart>())
new KChart(C_SAV.SAV).Show();
return;
}
if (!Directory.Exists(DatabasePath))
{
WinFormsUtil.Alert(MsgDatabase, string.Format(MsgDatabaseAdvice, DatabasePath));
return;
}
if (!this.OpenWindowExists<SAV_Database>())
new SAV_Database(PKME_Tabs, C_SAV).Show();
}
private void Menu_EncDatabase_Click(object sender, EventArgs e)
{
if (this.OpenWindowExists<SAV_Encounters>())
return;
var db = new TrainerDatabase();
var sav = C_SAV.SAV;
Task.Run(() =>
{
var dir = TrainerPath;
if (!Directory.Exists(dir))
return;
var files = Directory.EnumerateFiles(TrainerPath, "*.*", SearchOption.AllDirectories);
var pk = BoxUtil.GetPKMsFromPaths(files, sav.Context);
foreach (var f in pk)
db.RegisterCopy(f);
});
new SAV_Encounters(PKME_Tabs, db).Show();
}
private void MainMenuMysteryDB(object sender, EventArgs e)
{
if (!this.OpenWindowExists<SAV_MysteryGiftDB>())
new SAV_MysteryGiftDB(PKME_Tabs, C_SAV).Show();
}
private static void ClosePopups()
{
var forms = Application.OpenForms;
for (int i = forms.Count - 1; i >= 0; i--)
{
var f = forms[i];
if (f is null)
continue;
if (!IsPopupFormType(f))
continue;
if (f.InvokeRequired)
continue; // from another thread, not our scope.
f.Close();
}
}
private static bool IsPopupFormType(Form z) => z is not (Main or SplashScreen or SAV_FolderList or PokePreview);
private void MainMenuSettings(object sender, EventArgs e)
{
var settings = Settings;
using var form = new SettingsEditor(settings);
form.ShowDialog();
// Reload text (if OT details hidden)
Text = GetProgramTitle(C_SAV.SAV);
// Update final settings
ReloadProgramSettings(Settings);
if (form.BlankChanged) // changed by user
{
LoadBlankSaveFile(Settings.Startup.DefaultSaveVersion);
return;
}
PKME_Tabs_UpdatePreviewSprite(sender, e);
if (C_SAV.SAV.HasBox)
C_SAV.ReloadSlots();
}
private void ReloadProgramSettings(PKHeXSettings settings, bool skipCore = false)
{
if (!skipCore)
StartupUtil.ReloadSettings(settings);
PKME_Tabs.Unicode = Unicode = settings.Display.Unicode;
PKME_Tabs.UpdateUnicode(GenderSymbols);
SpriteName.AllowShinySprite = settings.Sprite.ShinySprites;
SpriteBuilderUtil.SpriterPreference = settings.Sprite.SpritePreference;
C_SAV.ModifyPKM = PKME_Tabs.ModifyPKM = settings.SlotWrite.SetUpdatePKM;
C_SAV.FlagIllegal = settings.Display.FlagIllegal;
C_SAV.M.Hover.GlowHover = settings.Hover.HoverSlotGlowEdges;
PKME_Tabs.HideSecretValues = settings.Privacy.HideSecretDetails;
WinFormsUtil.DetectSaveFileOnFileOpen = settings.Startup.TryDetectRecentSave;
SelectablePictureBox.FocusBorderDeflate = GenderToggle.FocusBorderDeflate = settings.Display.FocusBorderDeflate;
if (HaX)
{
EntityConverter.AllowIncompatibleConversion = EntityCompatibilitySetting.AllowIncompatibleAll;
}
SpriteBuilder.LoadSettings(settings.Sprite);
WinFormsUtil.AddSaveFileExtensions(settings.Backup.OtherSaveFileExtensions);
}
private void MainMenuBoxLoad(object sender, EventArgs e)
{
string? path = null;
if (Directory.Exists(DatabasePath))
{
var dr = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDatabaseLoad);
if (dr == DialogResult.Yes)
path = DatabasePath;
}
if (C_SAV.LoadBoxes(out string result, path))
WinFormsUtil.Alert(result);
}
/// <summary>
/// Dumps all Entity content stored in the SaveFile's boxes to disk.
/// </summary>
private void MainMenuBoxDump(object sender, EventArgs e)
{
if (Application.OpenForms.OfType<BoxExporter>().FirstOrDefault() is { } open)
{
open.Focus();
return;
}
DialogResult ld = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDatabaseExport);
if (ld == DialogResult.Yes)
{
BoxExport.Export(C_SAV.SAV, DatabasePath, BoxExportSettings.Default);
return;
}
if (ld != DialogResult.No)
return;
var dumper = new BoxExporter(C_SAV.SAV, BoxExporter.ExportOverride.All) { Owner = this };
dumper.Show();
}
private void MainMenuBoxDumpSingle(object sender, EventArgs e)
{
if (Application.OpenForms.OfType<BoxExporter>().FirstOrDefault() is { } open)
{
open.Focus();
return;
}
C_SAV.SAV.CurrentBox = C_SAV.CurrentBox; // double check
var dumper = new BoxExporter(C_SAV.SAV, BoxExporter.ExportOverride.Current) { Owner = this };
dumper.Show();
}
private void MainMenuBatchEditor(object sender, EventArgs e)
{
using var form = new BatchEditor(PKME_Tabs.PreparePKM(), C_SAV.SAV);
form.ShowDialog();
C_SAV.SetPKMBoxes(); // refresh
C_SAV.UpdateBoxViewers();
}
private void MainMenuFolder(object sender, EventArgs e)
{
if (this.OpenWindowExists<SAV_FolderList>())
return;
var form = new SAV_FolderList(s => OpenSAV(s.Clone(), s.Metadata.FilePath!));
form.Show();
}
// Misc Options
private void ClickShowdownImportPKM(object? sender, EventArgs e)
{
if (!Clipboard.ContainsText())
{ WinFormsUtil.Alert(MsgClipboardFailRead); return; }
// Get Simulator Data
var text = Clipboard.GetText();
var sets = BattleTemplateTeams.TryGetSets(text);
var set = sets.FirstOrDefault() ?? new(string.Empty); // take only first set
if (set.Species == 0)
{ WinFormsUtil.Alert(MsgSimulatorFailClipboard); return; }
var programLanguage = Language.GetLanguageValue(Settings.Startup.Language);
var settings = Settings.BattleTemplate.Export.GetSettings(programLanguage, set.Context);
var reformatted = set.GetText(settings);
if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgSimulatorLoad, reformatted))
return;
var invalid = set.InvalidLines;
if (invalid.Count != 0)
{
var localization = BattleTemplateParseErrorLocalization.Get(CurrentLanguage);
var sb = new System.Text.StringBuilder();
foreach (var line in invalid)
{
var error = line.Humanize(localization);
sb.AppendLine(error);
}
WinFormsUtil.Alert(MsgSimulatorInvalid, sb.ToString());
}
PKME_Tabs.LoadShowdownSet(set);
}
private void ClickShowdownExportPKM(object sender, EventArgs e)
{
if (!PKME_Tabs.EditsComplete)
{
WinFormsUtil.Alert(MsgSimulatorExportBadFields);
return;
}
var pk = PreparePKM();
var programLanguage = Language.GetLanguageValue(Settings.Startup.Language);
var settings = Settings.BattleTemplate.Export.GetSettings(programLanguage, pk.Context);
var text = ShowdownParsing.GetShowdownText(pk, settings);
bool success = WinFormsUtil.SetClipboardText(text);
if (!success || !Clipboard.GetText().Equals(text))
WinFormsUtil.Alert(MsgClipboardFailWrite, MsgSimulatorExportFail);
else
WinFormsUtil.Alert(MsgSimulatorExportSuccess, text);
}
private void ClickShowdownExportParty(object sender, EventArgs e) => C_SAV.ClickShowdownExportParty(sender, e);
private void ClickShowdownExportCurrentBox(object sender, EventArgs e) => C_SAV.ClickShowdownExportCurrentBox(sender, e);
// Main Menu Subfunctions
private void OpenQuick(string path)
{
if (!CanFocus)
{
SystemSounds.Asterisk.Play();
return;
}
OpenFromPath(path);
}
private void OpenFromPath(string path)
{
if (Plugins.Any(p => p.TryLoadFile(path)))
return; // handled by plugin
// detect if it is a folder (load into boxes or not)
if (Directory.Exists(path))
{ C_SAV.LoadBoxes(out string _, path); return; }
var fi = new FileInfo(path);
if (!fi.Exists)
return;
if (FileUtil.IsFileTooBig(fi.Length))
{
WinFormsUtil.Error(MsgFileSizeLarge + Environment.NewLine + string.Format(MsgFileSize, fi.Length), path);
return;
}
if (FileUtil.IsFileTooSmall(fi.Length))
{
WinFormsUtil.Error(MsgFileSizeSmall + Environment.NewLine + string.Format(MsgFileSize, fi.Length), path);
return;
}
byte[] input; try { input = File.ReadAllBytes(path); }
catch (Exception e) { WinFormsUtil.Error(MsgFileInUse + path, e); return; }
string ext = fi.Extension;
#if DEBUG
OpenFile(input, path, ext);
#else
try { OpenFile(input, path, ext); }
catch (Exception e) { WinFormsUtil.Error(MsgFileLoadFail + "\nPath: " + path, e); }
#endif
}
internal void OpenFile(Memory<byte> input, string path, string ext)
{
var obj = FileUtil.GetSupportedFile(input, ext, C_SAV.SAV);
if (obj is not null && LoadFile(obj, path))
return;
WinFormsUtil.Error(GetHintInvalidFile(input.Span, path),
$"{MsgFileLoad}{Environment.NewLine}{path}",
$"{string.Format(MsgFileSize, input.Length)}{Environment.NewLine}{input.Length} bytes (0x{input.Length:X4})");
}
private static string GetHintInvalidFile(ReadOnlySpan<byte> input, string path)
{
bool isSAV = WinFormsUtil.IsFileExtensionSAV(path);
if (!isSAV)
return MsgPKMUnsupported;
// Include a hint for the user to check if the file is all 00 or all FF
bool allZero = !input.ContainsAnyExcept<byte>(0x00);
if (allZero)
return MsgFileLoadAllZero;
bool allFF = !input.ContainsAnyExcept<byte>(0xFF);
if (allFF)
return MsgFileLoadAllFFFF;
return MsgFileUnsupported;
}
private bool LoadFile(object? input, string path)
{
if (input is null)
return false;
switch (input)
{
case PKM pk: return OpenPKM(pk);
case SaveFile s: return OpenSAV(s, path);
case IPokeGroup b: return OpenGroup(b);
case MysteryGift g: return OpenMysteryGift(g, path);
case ConcatenatedEntitySet pkms: return OpenPCBoxBin(pkms);
case IEncounterConvertible enc: return OpenPKM(enc.ConvertToPKM(C_SAV.SAV));
case SAV3GCMemoryCard gc:
if (!CheckGCMemoryCard(gc, path))
return true;
if (!SaveUtil.TryGetSaveFile(gc, out var mcsav))
return false;
mcsav.Metadata.SetExtraInfo(path);
return OpenSAV(mcsav, path);
}
return false;
}
private bool OpenPKM(PKM pk)
{
var sav = C_SAV.SAV;
var destType = sav.PKMType;
var tmp = EntityConverter.ConvertToType(pk, destType, out var c);
Debug.WriteLine(c.GetDisplayString(pk, destType));
if (tmp is null)
return false;
var unconverted = ReferenceEquals(pk, tmp);
if (unconverted && sav is { State.Exportable: true })
sav.AdaptToSaveFile(tmp);
PKME_Tabs.PopulateFields(tmp);
return true;
}
private bool OpenGroup(IPokeGroup b)
{
bool result = C_SAV.OpenGroup(b, out var msg);
if (!string.IsNullOrWhiteSpace(msg))
WinFormsUtil.Alert(msg);
Debug.WriteLine(msg);
return result;
}
private bool OpenMysteryGift(MysteryGift tg, string path)
{
if (!tg.IsEntity)
{
WinFormsUtil.Alert(MsgPKMMysteryGiftFail, path);
return true;
}
var temp = tg.ConvertToPKM(C_SAV.SAV);
var destType = C_SAV.SAV.PKMType;
var pk = EntityConverter.ConvertToType(temp, destType, out var c);
if (pk is null)
{
WinFormsUtil.Alert(c.GetDisplayString(temp, destType));
return true;
}
C_SAV.SAV.AdaptToSaveFile(pk);
PKME_Tabs.PopulateFields(pk);
Debug.WriteLine(c);
return true;
}
private bool OpenPCBoxBin(ConcatenatedEntitySet pkms)
{
if (C_SAV.IsBoxDragActive)
return true;
Cursor = Cursors.Default;
if (!C_SAV.OpenPCBoxBin(pkms.Data.Span, out var msg))
{
WinFormsUtil.Alert(MsgFileLoadIncompatible, msg);
return true;
}
WinFormsUtil.Alert(msg);
return true;
}
private SaveFileType SelectMemoryCardSaveGame(SAV3GCMemoryCard memCard)
{
if (memCard.SaveGameCount == 1)
return memCard.SelectedGameVersion;
string[] games =
[
MsgGameColosseum,
MsgGameXD,
MsgGameRSBOX,
];
if (!this.TrySelectIndex(MsgFileLoadSaveMultiple, MsgFileLoadSaveSelectGame, games, out var index))
return SaveFileType.None;
return index switch
{
0 => SaveFileType.Colosseum,
1 => SaveFileType.XD,
2 => SaveFileType.RSBox,
_ => SaveFileType.None,
};
}
private bool CheckGCMemoryCard(SAV3GCMemoryCard memCard, string path)
{
var state = memCard.GetMemoryCardState();
switch (state)
{
case MemoryCardSaveStatus.NoPkmSaveGame:
WinFormsUtil.Error(MsgFileGameCubeNoGames, path);
return false;
case MemoryCardSaveStatus.DuplicateCOLO:
case MemoryCardSaveStatus.DuplicateXD:
case MemoryCardSaveStatus.DuplicateRSBOX:
WinFormsUtil.Error(MsgFileGameCubeDuplicate, path);
return false;
case MemoryCardSaveStatus.MultipleSaveGame:
var game = SelectMemoryCardSaveGame(memCard);
if (game == 0) // Cancel
return false;
memCard.SelectSaveGame(game);
break;
case MemoryCardSaveStatus.SaveGameCOLO: memCard.SelectSaveGame(SaveFileType.Colosseum); break;
case MemoryCardSaveStatus.SaveGameXD: memCard.SelectSaveGame(SaveFileType.XD); break;
case MemoryCardSaveStatus.SaveGameRSBOX: memCard.SelectSaveGame(SaveFileType.RSBox); break;
default:
WinFormsUtil.Error(!SAV3GCMemoryCard.IsMemoryCardSize(memCard.Data.Length) ? MsgFileGameCubeBad : GetHintInvalidFile(memCard.Data, path), path);
return false;
}
return true;
}
private static void StoreLegalSaveGameData(SaveFile sav)
{
if (sav is SAV3 sav3)
EReaderBerrySettings.LoadFrom(sav3);
}
private bool OpenSAV(SaveFile sav, string path)
{
if (ModifierKeys == Keys.Alt)
{
SaveTypeInfo other = default;
if (SaveUtil.TryOverride(sav, other, out var replace))
sav = replace;
}
if (!sav.IsVersionValid())
{
WinFormsUtil.Error(MsgFileLoadSaveLoadFail, path);
return true;
}
sav.Metadata.SetExtraInfo(path);
if (!SanityCheckSAV(ref sav))
return true;
if (C_SAV.SAV.State.Edited && Settings.SlotWrite.ModifyUnset)
{
var prompt = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgProgramCloseUnsaved, MsgProgramSaveFileConfirm);
if (prompt != DialogResult.Yes)
return true;
}
ClosePopups();
PKME_Tabs.Focus(); // flush any pending changes
StoreLegalSaveGameData(sav);
ParseSettings.InitFromSaveFileData(sav); // physical GB, no longer used in logic
RecentTrainerCache.SetRecentTrainer(sav);
SpriteUtil.Initialize(sav); // refresh sprite generator
dragout.Size = new Size(SpriteUtil.Spriter.Width, SpriteUtil.Spriter.Height);
// clean fields
Menu_ExportSAV.Enabled = sav.State.Exportable;
// No changes made yet
Menu_Undo.Enabled = false;
Menu_Redo.Enabled = false;
GameInfo.FilteredSources = new FilteredGameDataSource(sav, GameInfo.Sources, HaX);
ResetSAVPKMEditors(sav);
C_SAV.M.Reset();
Text = GetProgramTitle(sav);
TryBackupExportCheck(sav, path);
CheckLoadPath(path);
Menu_ShowdownExportParty.Visible = sav.HasParty;
Menu_ShowdownExportCurrentBox.Visible = sav.HasBox;
Settings.Startup.LoadSaveFile(path);
if (Settings.Sounds.PlaySoundSAVLoad)
SystemSounds.Asterisk.Play();
return true;
}
private void ResetSAVPKMEditors(SaveFile sav)
{
C_SAV.SetEditEnvironment(new SaveDataEditor<PictureBox>(sav, PKME_Tabs));
var pk = sav.LoadTemplate(TemplatePath);
PKME_Tabs.CurrentPKM = pk;
bool init = PKME_Tabs.IsInitialized;
if (!init)
{
PKME_Tabs.InitializeBinding();
PKME_Tabs.SetPKMFormatMode(pk);
PKME_Tabs.ChangeLanguage(sav);
}
else
{
PKME_Tabs.SetPKMFormatMode(pk);
}
PKME_Tabs.PopulateFields(pk);
// Initialize Overall Info
Menu_LoadBoxes.Enabled = Menu_DumpBoxes.Enabled = Menu_DumpBox.Enabled = Menu_Report.Enabled = C_SAV.SAV.HasBox;
// Initialize Subviews
bool WindowTranslationRequired = false;
WindowTranslationRequired |= PKME_Tabs.ToggleInterface(sav, pk);
WindowTranslationRequired |= C_SAV.ToggleInterface();
if (WindowTranslationRequired) // force update -- re-added controls may be untranslated
WinFormsUtil.TranslateInterface(this, CurrentLanguage);
PKME_Tabs.PopulateFields(pk);
sav.State.Edited = false;
foreach (var p in Plugins)
p.NotifySaveLoaded();
}
private static string GetProgramTitle()
{
#if DEBUG
// Get the file path that started this exe.
var path = Environment.ProcessPath;
var date = path is null ? DateTime.Now : File.GetLastWriteTime(path);
string version = $"d-{date:yyyyMMdd}";
#else
var v = Program.CurrentVersion;
string version = $"{2000+v.Major:00}{v.Minor:00}{v.Build:00}";
#endif
return $"PKH{(HaX ? "a" : "e")}X ({version})";
}
private static string GetProgramTitle(SaveFile sav)
{
string title = GetProgramTitle() + $" - {sav.GetType().Name}: ";
if (sav is ISaveFileRevision rev)
title = title.Insert(title.Length - 2, rev.SaveRevisionString);
var version = GameInfo.GetVersionName(sav.Version);
if (Settings.Privacy.HideSAVDetails)
return title + $"[{version}]";
if (!sav.State.Exportable) // Blank save file
return title + $"{sav.Metadata.FileName} [{sav.OT} ({version})]";
return title + Path.GetFileNameWithoutExtension(PathUtil.CleanFileName(sav.Metadata.BAKName)); // more descriptive
}
private static bool TryBackupExportCheck(SaveFile sav, string path)
{
// If backup folder exists, save a backup.
if (string.IsNullOrWhiteSpace(path))
return false; // not actual save
if (!Settings.Backup.BAKEnabled)
return false;
if (!sav.State.Exportable)
return false; // not actual save
var dir = BackupPath;
if (!Directory.Exists(dir))
return false;
var meta = sav.Metadata;
var backupName = meta.GetBackupFileName(dir);
if (File.Exists(backupName))
return false; // Already backed up.
// Ensure the file we are copying exists.
var src = meta.FilePath;
if (src is null || !File.Exists(src))
return false;
try
{
// Don't need to force overwrite, but on the off-chance it was written externally, we force ours.
File.Copy(src, backupName, true);
return true;
}
catch (Exception ex)
{
WinFormsUtil.Error(MsgBackupUnable, ex);
return false;
}
}
private static bool CheckLoadPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
return false; // not actual save
if (!FileUtil.IsFileLocked(path))
return true;
WinFormsUtil.Alert(MsgFileWriteProtected + Environment.NewLine + path, MsgFileWriteProtectedAdvice);
return false;
}
private bool SanityCheckSAV(ref SaveFile sav)
{
if (sav.Generation <= 3)
SaveLanguage.TryRevise(sav);
if (sav.State.Exportable && sav is SAV3 s3)
{
if (ModifierKeys == Keys.Control || s3.IsCorruptPokedexFF())
{
ReadOnlySpan<GameVersion> choices = [GameVersion.R, GameVersion.S, GameVersion.E, GameVersion.FR, GameVersion.LG];
var options = new string[choices.Length];
for (int i = 0; i < options.Length; i++)
options[i] = GameInfo.Strings.gamelist[(int)choices[i]];
var msg = string.Format(MsgFileLoadVersionDetect, $"3 ({s3.Version})");
var text = MsgFileLoadSaveSelectVersion;
if (sav.Metadata.FileName is { } fn)
text += Environment.NewLine + fn;
if (!this.TrySelectIndex(msg, text, options, out var index, choices.IndexOf(s3.Version)))
return false;
var game = choices[index];
var s = s3.ForceLoad(game);
if (s is SAV3FRLG frlg)
{
// Try to give the correct Deoxys form stats (different in R/S, E, FR and LG)
bool result = frlg.ResetPersonal(game);
if (!result)
return false;
}
var origin = sav.Metadata.FilePath;
if (origin is not null)
s.Metadata.SetExtraInfo(origin);
sav = s;
}
else if (s3 is SAV3FRLG frlg && !frlg.Version.IsValidSavedVersion()) // IndeterminateSubVersion
{
ReadOnlySpan<GameVersion> choices = [GameVersion.FR, GameVersion.LG];
var options = new string[choices.Length];
for (int i = 0; i < options.Length; i++)
options[i] = GameInfo.Strings.gamelist[(int)choices[i]];
string dual = "{1}/{2} " + MsgFileLoadVersionDetect;
var msg = string.Format(dual, "3", options[0], options[1]);
if (!this.TrySelectIndex(msg, MsgFileLoadSaveSelectVersion, options, out var index))
return false;
var game = choices[index];
bool result = frlg.ResetPersonal(game);
if (!result)
return false;
}
}
return true;
}
public static void SetCountrySubRegion(ComboBox cb, string type)
{
// Try to retain previous selection index. If triggered by language change, the list will be reloaded.
int index = cb.SelectedIndex;
string cl = GameInfo.CurrentLanguage;
cb.DataSource = Util.GetCountryRegionList(type, cl);
if (index > 0 && index < cb.Items.Count)
cb.SelectedIndex = index;
}
// Language Translation
private void ChangeMainLanguage(object sender, EventArgs e)
{
var index = CB_MainLanguage.SelectedIndex;
if ((uint)index < CB_MainLanguage.Items.Count)
CurrentLanguage = GameLanguage.LanguageCode(index);
var lang = CurrentLanguage;
Settings.Startup.Language = lang;
WinFormsUtil.SetCultureLanguage(lang);
Menu_Options.DropDown.Close();
var sav = C_SAV.SAV;
LocalizeUtil.InitializeStrings(lang, sav, HaX);
WinFormsUtil.TranslateInterface(this, lang); // Translate the UI to language.
LocalizedDescriptionAttribute.Localizer = WinFormsTranslator.GetDictionary(lang);
SizeCP.ResetSizeLocalizations(lang);
PKME_Tabs.SizeCP.TryResetStats();
C_SAV.SL_Extra.ForceTranslation(lang);
if (sav is not FakeSaveFile)
{
var pk = PKME_Tabs.CurrentPKM.Clone();
PKME_Tabs.ChangeLanguage(sav);
PKME_Tabs.PopulateFields(pk); // put data back in form
Text = GetProgramTitle(sav);
}
foreach (var plugin in Plugins)
plugin.NotifyDisplayLanguageChanged(lang);
}
#endregion
#region //// PKX WINDOW FUNCTIONS ////
private bool QR6Notified;
private void ClickQR(object? sender, EventArgs e)
{
if (ModifierKeys == Keys.Alt)
{
string url = Clipboard.GetText();
if (!string.IsNullOrWhiteSpace(url))
{
if (url.StartsWith("http") && !url.Contains('\n')) // qr payload
ImportQRToTabs(url);
else
ClickShowdownImportPKM(sender, e);
return;
}
}
ExportQRFromTabs();
}
private void ImportQRToTabs(string url)
{
var msg = QRDecode.GetQRData(url, out var input);
if (msg != 0)
{
WinFormsUtil.Alert(msg.ConvertMsg());
return;
}
if (input.Length == 0)
return;
var sav = C_SAV.SAV;
if (FileUtil.TryGetPKM(input, out var pk, sav.Generation.ToString(), sav))
{
OpenPKM(pk);
return;
}
if (FileUtil.TryGetMysteryGift(input, out var mg, url))
{
OpenMysteryGift(mg, url);
return;
}
WinFormsUtil.Alert(MsgQRDecodeFail, string.Format(MsgQRDecodeSize, input.Length));
}
private void ExportQRFromTabs()
{
if (!PKME_Tabs.EditsComplete)
return;
PKM pk = PreparePKM();
if (pk.Format == 6 && !QR6Notified) // hint that the user should not be using QR6 injection
{
WinFormsUtil.Alert(MsgQRDeprecated, MsgQRAlternative);
QR6Notified = true;
}
var qr = QREncode.GenerateQRCode(pk);
if (dragout.Image is not Bitmap sprite)
return;
var la = new LegalityAnalysis(pk, C_SAV.SAV.Personal);
if (la.Parsed && pk.Species != 0)
{
var img = SpriteUtil.GetLegalIndicator(la.Valid);
sprite = ImageUtil.LayerImage(sprite, img, sprite.Width - img.Width, 0);
}
string[] r = pk.GetQRLines();
string refer = GetProgramTitle();
using var form = new QR(qr, sprite, pk, r[0], r[1], r[2], $"{refer} ({pk.GetType().Name})");
form.ShowDialog();
}
private void ClickLegality(object? sender, EventArgs e)
{
if (!PKME_Tabs.EditsComplete)
{ SystemSounds.Hand.Play(); return; }
var pk = PreparePKM();
if (pk.Species == 0 || !pk.ChecksumValid)
{ SystemSounds.Hand.Play(); return; }
var la = new LegalityAnalysis(pk, C_SAV.SAV.Personal);
PKME_Tabs.UpdateLegality(la);
DisplayLegalityReport(la);
}
private void DisplayLegalityReport(LegalityAnalysis la)
{
if (Settings.Sounds.PlaySoundLegalityCheck)
SystemSounds.Asterisk.Play();
if (Settings.Display.IgnoreLegalPopup && la.Valid)
return;
var verbose = ModifierKeys == Keys.Control ^ Settings.Display.ExportLegalityAlwaysVerbose;
var localizer = LegalityLocalizationContext.Create(la, CurrentLanguage);
var simpleReport = localizer.Report(false);
var verboseReport = localizer.Report(true);
var settings = localizer.Settings;
var intro = simpleReport + Environment.NewLine;
if (la.Valid)
intro += Environment.NewLine;
var expandText = verboseReport.Replace(intro, "");
var page = new TaskDialogPage
{
Caption = MsgLegalityPopupCaption,
Heading = la.Valid ? settings.Lines.Legal : settings.Lines.SInvalid,
Icon = la.Valid ? TaskDialogIcon.ShieldSuccessGreenBar : TaskDialogIcon.ShieldErrorRedBar,
Text = la.Valid ? "" : simpleReport,
Expander = new TaskDialogExpander
{
CollapsedButtonText = MsgLegalityPopupCollapsed,
ExpandedButtonText = MsgLegalityPopupExpanded,
Expanded = verbose,
Text = expandText,
},
Buttons = [TaskDialogButton.OK],
DefaultButton = TaskDialogButton.OK,
AllowCancel = true,
SizeToContent = true,
};
if (!Settings.Display.ExportLegalityNeverClipboard)
{
var clipboard = new TaskDialogButton(MsgLegalityPopupCopyClipboard) { AllowCloseDialog = true };
clipboard.Click += (_, _) =>
{
var enc = la.EncounterOriginal.GetTextLines(Settings.Display.ExportLegalityVerboseProperties);
var msg = verboseReport + Environment.NewLine + Environment.NewLine + string.Join(Environment.NewLine, enc);
WinFormsUtil.SetClipboardText(msg);
SystemSounds.Asterisk.Play();
};
page.Buttons.Add(clipboard);
}
TaskDialog.ShowDialog(this, page);
}
private void ClickClone(object sender, EventArgs e)
{
if (!PKME_Tabs.EditsComplete)
return; // don't copy garbage to the box
PKM pk = PKME_Tabs.PreparePKM();
C_SAV.SetClonesToBox(pk);
}
private void GetPreview(PictureBox pb, PKM? pk = null)
{
pk ??= PreparePKM(false); // don't perform control loss click
var menu = dragout.ContextMenuStrip;
menu?.Enabled = pk.Species != 0 || HaX; // Species
var img = pk.Sprite(C_SAV.SAV);
if (Application.IsDarkModeEnabled)
{
var avg = img.GetAverageColor();
var c = Color.FromArgb(avg);
SpriteUtil.GetSpriteGlow(img, c.B, c.G, c.R, out var pixels, true);
var layer = ImageUtil.GetBitmap(pixels, img.Width, img.Height, img.PixelFormat);
img = ImageUtil.LayerImage(img, layer, 0, 0);
}
pb.Image = img;
if (pb.BackColor == SlotUtil.BadDataColor)
pb.BackColor = SlotUtil.GoodDataColor;
}
private void PKME_Tabs_UpdatePreviewSprite(object sender, EventArgs e) => GetPreview(dragout);
private void PKME_Tabs_LegalityChanged(object sender, EventArgs e)
{
if (HaX)
{
PB_Legal.Visible = false;
return;
}
PB_Legal.Visible = true;
bool isValid = (sender as bool?) != false;
PB_Legal.Image = SpriteUtil.GetLegalIndicator(isValid);
toolTip.SetToolTip(PB_Legal, isValid ? MsgLegalityHoverValid : MsgLegalityHoverInvalid);
}
private void PKME_Tabs_RequestShowdownExport(object sender, EventArgs e) => ClickShowdownExportPKM(sender, e);
private void PKME_Tabs_RequestShowdownImport(object sender, EventArgs e) => ClickShowdownImportPKM(sender, e);
private SaveFile PKME_Tabs_SaveFileRequested(object sender, EventArgs e) => C_SAV.SAV;
private PKM PreparePKM(bool click = true) => PKME_Tabs.PreparePKM(click);
// Drag & Drop Events
private static void Main_DragEnter(object? sender, DragEventArgs? e)
{
if (e is null)
return;
if (e.AllowedEffect == (DragDropEffects.Copy | DragDropEffects.Link)) // external file
e.Effect = DragDropEffects.Copy;
else if (e.Data is not null) // within
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;
OpenQuick(files[0]);
e.Effect = DragDropEffects.Copy;
}
// ReSharper disable once AsyncVoidMethod
private async void Dragout_MouseDown(object sender, MouseEventArgs e)
{
try
{
if (e.Button != MouseButtons.Left)
return;
if (ModifierKeys is Keys.Alt or Keys.Shift)
{
ClickQR(sender, e);
return;
}
if (!PKME_Tabs.EditsComplete)
return;
// Gather data
var pk = PreparePKM();
var preModify = pk.Clone();
var encrypt = ModifierKeys == Keys.Control;
var data = encrypt ? pk.EncryptedPartyData : pk.DecryptedPartyData;
// Create Temp File to Drag
var newfile = FileUtil.GetPKMTempFileName(pk, encrypt);
try
{
await File.WriteAllBytesAsync(newfile, data).ConfigureAwait(true);
var pb = (PictureBox)sender;
if (pb.Image is Bitmap img)
C_SAV.M.Drag.Info.Cursor = Cursor = new Cursor(img.GetHicon());
DoDragDrop(new DataObject(DataFormats.FileDrop, new[] { newfile }), DragDropEffects.Copy);
}
// Tons of things can happen with drag & drop; don't try to handle things, just indicate failure.
catch (Exception x)
{ WinFormsUtil.Error("Drag && Drop Error", x); }
finally
{
C_SAV.M.Drag.ResetCursor(this);
await DeleteAsync(newfile, 20_000).ConfigureAwait(false);
}
PKME_Tabs.NotifyWasExported(preModify); // restore pre-modify state, in case the user drags into the same program window
}
catch
{
// Ignore.
}
}
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 Dragout_DragOver(object sender, DragEventArgs e) => e.Effect = DragDropEffects.Copy;
private void DragoutEnter(object sender, EventArgs e)
{
dragout.BackgroundImage = PKME_Tabs.Entity.Species > 0 ? SpriteUtil.Spriter.Set : SpriteUtil.Spriter.Delete;
Cursor = Cursors.Hand;
}
private void DragoutLeave(object sender, EventArgs e)
{
dragout.BackgroundImage = SpriteUtil.Spriter.Transparent;
if (Cursor == Cursors.Hand)
Cursor = Cursors.Default;
}
private void DragoutDrop(object? sender, DragEventArgs? e)
{
if (e?.Data?.GetData(DataFormats.FileDrop) is not string[] { Length: not 0 } files)
return;
OpenQuick(files[0]);
e.Effect = DragDropEffects.Copy;
Cursor = DefaultCursor;
}
private async void Main_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
if (C_SAV.SAV.State.Edited || PKME_Tabs.PKMIsUnsaved)
{
var prompt = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgProgramCloseUnsaved, MsgProgramCloseConfirm);
if (prompt != DialogResult.Yes)
{
e.Cancel = true;
return;
}
}
await PKHeXSettings.SaveSettings(Program.PathConfig, Settings).ConfigureAwait(false);
}
catch
{
// Ignore; program is shutting down.
}
}
#endregion
#region //// SAVE FILE FUNCTIONS ////
private void ClickExportSAV(object sender, EventArgs e)
{
if (!Menu_ExportSAV.Enabled)
return; // hot-keys can't cheat the system!
if (Settings.Advanced.SaveExportCheckUnsavedEntity && PKME_Tabs.PKMIsUnsaved)
{
var prompt = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgProgramSaveUnsaved, MsgContinue);
if (prompt != DialogResult.Yes)
return;
}
C_SAV.ExportSaveFile();
Text = GetProgramTitle(C_SAV.SAV);
}
private void ClickSaveFileName(object sender, EventArgs e)
{
try
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
if (!SaveFinder.TryDetectSaveFile(cts.Token, out var sav))
return;
var path = sav.Metadata.FilePath!;
var time = new FileInfo(path).CreationTime;
var timeStamp = time.ToString(CultureInfo.CurrentCulture);
if (WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgFileLoadSaveDetectReload, path, timeStamp) == DialogResult.Yes)
LoadFile(sav, path); // load save
}
catch (Exception ex)
{
WinFormsUtil.Error(ex.Message); // `path` contains the error message
}
}
public void PromptBackup(string folder)
{
if (Directory.Exists(folder))
return;
if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, string.Format(MsgBackupCreateLocation, folder), MsgBackupCreateQuestion))
return;
try
{
Directory.CreateDirectory(folder);
WinFormsUtil.Alert(MsgBackupSuccess, string.Format(MsgBackupDelete, folder));
}
catch (Exception ex)
// Maybe they put their exe in a folder that we can't create files/folders to.
{ WinFormsUtil.Error($"{MsgBackupUnable} @ {folder}", ex); }
}
private void ClickUndo(object sender, EventArgs e) => C_SAV.ClickUndo();
private void ClickRedo(object sender, EventArgs e) => C_SAV.ClickRedo();
#endregion
public void WarnBehavior()
{
var page = new TaskDialogPage
{
Caption = MsgProgramIllegalModeActive,
Text = MsgProgramIllegalModeBehave,
Icon = TaskDialogIcon.Shield,
DefaultButton = TaskDialogButton.OK,
Buttons = [TaskDialogButton.OK],
AllowCancel = true,
};
TaskDialog.ShowDialog(this, page);
}
}