PKHeX/PKHeX.WinForms/Program.cs
Kurt f0c8b86728 Minor startup tweaks
Allow settings to skip Splash Screen (and just launch the main form without fuss, cuz why not?)
Handle scenario where PKHeX.Core.dll fails to bind during Settings fetch -- handle via static constructor instead of Program.Main() so that errors pipe to the Release error handlers.

Run update check in another thread, after Main is shown, so that offline users don't have to wait 3 extra seconds for it to timeout and show.

Revise the startup animation to just show the Main form rather than minimize->restore. The previous "hack" was designed so that if users clicked anywhere after launching the program (thus losing focus) the Main form would re-capture it. Activate() works fine now (maybe it didn't in the past?)

Removes "dark" startup arg; do via settings. Users really won't have a separate launch config like they might for HaX via .bat
2025-08-14 23:57:20 -05:00

242 lines
8.1 KiB
C#

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Core;
#if !DEBUG
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
#endif
namespace PKHeX.WinForms;
internal static class Program
{
// Pipelines build can sometimes tack on text to the version code. Strip it out.
public static readonly Version CurrentVersion = Version.Parse(GetSaneVersionTag(Application.ProductVersion));
public static readonly string WorkingDirectory = Path.GetDirectoryName(Environment.ProcessPath)!;
public const string ConfigFileName = "cfg.json";
public static string PathConfig => Path.Combine(WorkingDirectory, ConfigFileName);
/// <summary>
/// Global settings instance, loaded before any forms are created.
/// </summary>
public static PKHeXSettings Settings { get; }
public static bool HaX { get; private set; }
static Program()
{
#if !DEBUG
Application.ThreadException += UIThreadException;
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
#endif
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Settings = PKHeXSettings.GetSettings(PathConfig);
if (Settings.Startup.DarkMode)
#pragma warning disable WFO5001
Application.SetColorMode(SystemColorMode.Dark);
#pragma warning restore WFO5001
}
[STAThread]
private static void Main()
{
// Load settings first
var settings = Settings;
settings.LocalResources.SetLocalPath(WorkingDirectory);
StartupUtil.ReloadSettings(settings);
SplashScreen? splash = null;
if (!settings.Startup.SkipSplashScreen)
{
// Show splash screen on a dedicated STA thread so it can pump its own message loop.
var splashThread = new Thread(() =>
{
splash = new SplashScreen();
Application.Run(splash);
})
{ IsBackground = true };
splashThread.SetApartmentState(ApartmentState.STA);
splashThread.Start();
}
var args = Environment.GetCommandLineArgs();
// Prepare initial values for the main form.
var startup = StartupUtil.GetStartup(args, settings);
var init = StartupUtil.FormLoadInitialActions(args, settings, CurrentVersion);
HaX = init.HaX;
var main = new Main();
// Close splash when Main is ready to display, then perform startup animation.
main.Shown += (_, _) =>
{
Task.Run(() => splash?.BeginInvoke(splash.ForceClose));
main.Activate();
// Follow-up: display popups if needed.
if (init.HaX)
main.WarnBehavior();
else if (init.ShowChangelog)
main.ShowAboutDialog(AboutPage.Changelog);
else if (init.BackupPrompt)
main.PromptBackup(settings.LocalResources.GetBackupPath());
Task.Run(main.CheckForUpdates);
};
// Setup complete.
if (Settings.Startup.PluginLoadEnable)
main.AttachPlugins();
main.LoadInitialFiles(startup);
Application.Run(main);
}
private static ReadOnlySpan<char> GetSaneVersionTag(ReadOnlySpan<char> productVersion)
{
for (int i = 0; i < productVersion.Length; i++)
{
char c = productVersion[i];
if (c == '.')
continue;
if (char.IsNumber(c))
continue;
return productVersion[..i];
}
return productVersion;
}
#if !DEBUG
private static void Error(string msg) => MessageBox.Show(msg, "PKHeX Error", MessageBoxButtons.OK, MessageBoxIcon.Stop);
private static void UIThreadException(object sender, ThreadExceptionEventArgs t)
{
DialogResult result = DialogResult.Cancel;
try
{
var e = t.Exception;
string errorMessage = GetErrorMessage(e);
result = ErrorWindow.ShowErrorDialog(errorMessage, e, true);
}
catch (Exception reportingException)
{
HandleReportingException(t.Exception, reportingException);
}
if (result == DialogResult.Abort)
Application.Exit();
}
private static string GetErrorMessage(Exception e)
{
try
{
if (IsPluginError<IPlugin>(e, out var pluginName))
return $"An error occurred in a PKHeX plugin. Please report this error to the plugin author/maintainer.\n{pluginName}";
}
catch { }
return "An error occurred in PKHeX. Please report this error to the PKHeX author.";
}
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var ex = e.ExceptionObject as Exception;
try
{
if (IsOldPkhexCorePresent(ex))
{
Error("You have upgraded PKHeX incorrectly. Please delete PKHeX.Core.dll.");
}
else if (IsPkhexCoreMissing(ex))
{
Error("You have installed PKHeX incorrectly. Please ensure you have unzipped all files before running.");
}
else if (ex is not null)
{
var msg = GetErrorMessage(ex);
ErrorWindow.ShowErrorDialog($"{msg}\nPKHeX must now close.", ex, false);
}
else
{
Error("A fatal non-UI error has occurred in PKHeX, and the details could not be displayed. Please report this to the author.");
}
}
catch (Exception reportingException)
{
HandleReportingException(ex, reportingException);
}
}
private static bool IsPluginError<T>(Exception exception, out string pluginName)
{
pluginName = string.Empty;
var stackTrace = new System.Diagnostics.StackTrace(exception);
foreach (var frame in stackTrace.GetFrames())
{
var method = frame.GetMethod();
var type = method?.DeclaringType;
if (!typeof(T).IsAssignableFrom(type))
continue;
pluginName = type.Namespace ?? string.Empty;
return true;
}
return false;
}
private static void HandleReportingException(Exception? ex, Exception reportingException)
{
try
{
EmergencyErrorLog(ex, reportingException);
}
catch
{
}
if (reportingException is FileNotFoundException x && x.FileName?.StartsWith("PKHeX.Core") == true)
{
Error("Could not locate PKHeX.Core.dll. Make sure you're running PKHeX together with its code library. Usually caused when all files are not extracted.");
return;
}
try
{
Error("A fatal non-UI error has occurred in PKHeX, and there was a problem displaying the details. Please report this to the author.");
}
finally
{
Application.Exit();
}
}
private static bool EmergencyErrorLog(Exception? originalException, Exception errorHandlingException)
{
try
{
var message = (originalException?.ToString() ?? "null first exception") + Environment.NewLine + errorHandlingException;
File.WriteAllText($"PKHeX_Error_Report {DateTime.Now:yyyyMMddHHmmss}.txt", message);
}
catch (Exception)
{
return false;
}
return true;
}
private static bool IsOldPkhexCorePresent([NotNullWhen(true)] Exception? ex)
{
return ex is MissingMethodException or TypeLoadException or TypeInitializationException
&& File.Exists("PKHeX.Core.dll")
&& AssemblyName.GetAssemblyName("PKHeX.Core.dll").Version < CurrentVersion;
}
private static bool IsPkhexCoreMissing([NotNullWhen(true)] Exception? ex)
{
return ex is FileNotFoundException { FileName: {} n } && n.Contains("PKHeX.Core");
}
#endif
}