diff --git a/RetroBat/RetroBat/AutostartManager.cs b/RetroBat/RetroBat/AutostartManager.cs new file mode 100644 index 0000000..9f05618 --- /dev/null +++ b/RetroBat/RetroBat/AutostartManager.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using Microsoft.Win32; + +namespace RetroBat +{ + internal static class AutostartManager + { + public static void CleanupLegacyShortcut() + { + try + { + string startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup); + string linkStartup = Path.Combine(startupFolder, "RetroBat.lnk"); + + if (File.Exists(linkStartup)) + { + try { File.Delete(linkStartup); } + catch (Exception ex) { SimpleLogger.Instance.Warning("Failed to delete legacy RetroBat.lnk: " + ex.Message); } + } + } + catch (Exception ex) { SimpleLogger.Instance.Warning("CleanupStartup failed: " + ex.Message); } + } + + public static void Apply(int autostartMode, string appFolder, string appExe) + { + if (autostartMode == 1) + { + AddToStartupFolder(appFolder, appExe); + RemoveFromStartupReg(); + } + else if (autostartMode == 2) + { + AddToStartupReg(appFolder, appExe); + RemoveFromStartupFolder("RetroBat"); + } + else + { + RemoveFromStartupReg(); + RemoveFromStartupFolder("RetroBat"); + } + } + + private static void AddToStartupReg(string appPath, string appExe) + { + SimpleLogger.Instance.Info("Setting RetroBat to launch at startup."); + + string batPath = Path.Combine(appPath, appExe); + + string regValue = string.Format( + "cmd.exe /c \"cd /d {0} && start \"\" \"{1}\"\"\"", + appPath, + batPath + ); + + try + { + RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); + key.SetValue("RetroBat", regValue); + SimpleLogger.Instance.Info("RetroBat set in registry to startup."); + } + catch (Exception ex) + { + SimpleLogger.Instance.Warning("Failed to set startup registry key: " + ex.Message); + } + } + + private static void AddToStartupFolder(string exePath, string shortcutName) + { + try + { + string startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup); + string exeName = Path.GetFileNameWithoutExtension(shortcutName); + string batPath = Path.Combine(startupFolder, exeName + ".bat"); + string exe = Path.Combine(exePath, shortcutName); + + // Write a simple batch file to start RetroBat + string batContent = $"@echo off{Environment.NewLine}cd /d \"{exePath}\"{Environment.NewLine}\"{exe}\""; + File.WriteAllText(batPath, batContent); + + SimpleLogger.Instance.Info("RetroBat batch added to Startup folder: " + batPath); + } + catch (Exception ex) + { + SimpleLogger.Instance.Warning("Failed to add RetroBat to Startup folder: " + ex.Message); + } + } + + private static void RemoveFromStartupFolder(string shortcutName) + { + try + { + string startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup); + string batPath = Path.Combine(startupFolder, shortcutName + ".bat"); + + if (File.Exists(batPath)) + { + File.Delete(batPath); + SimpleLogger.Instance.Info("RetroBat removed from Startup folder: " + batPath); + } + else + { + SimpleLogger.Instance.Info("RetroBat startup batch not found, nothing to remove."); + } + } + catch (Exception ex) + { + SimpleLogger.Instance.Warning("Failed to remove RetroBat from Startup folder: " + ex.Message); + } + } + + private static void RemoveFromStartupReg() + { + SimpleLogger.Instance.Info("Ensuring RetroBat does not launch at startup."); + + try + { + RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); + key.DeleteValue("RetroBat"); + } + catch (Exception ex) + { + SimpleLogger.Instance.Warning("Failed to remove startup registry key: " + ex.Message); + } + } + } +} diff --git a/RetroBat/RetroBat/DpiAwarenessManager.cs b/RetroBat/RetroBat/DpiAwarenessManager.cs new file mode 100644 index 0000000..cf62d02 --- /dev/null +++ b/RetroBat/RetroBat/DpiAwarenessManager.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Win32; + +namespace RetroBat +{ + internal static class DpiAwarenessManager + { + public static void ApplyOverridesIfNeeded(string appFolder) + { + if (!HasDpiScaling()) + return; + + string dpiFile = Path.Combine(appFolder, "system", "tools", "dpi_awareness.txt"); + + if (!File.Exists(dpiFile)) + return; + + try + { + var dpiLines = File.ReadAllLines(dpiFile); + + if (dpiLines.Length > 0) + { + foreach (var dpiLine in dpiLines) + { + string dpiExePath = Path.Combine(appFolder, dpiLine.Trim()); + + if (File.Exists(dpiExePath)) + SetDpiAwarenessOverride(dpiExePath, true); + } + } + } + catch (Exception ex) { SimpleLogger.Instance.Warning("Failed to apply DPI awareness overrides: " + ex.Message); } + } + + public static bool HasDpiScaling() + { + using (var key = Registry.LocalMachine.OpenSubKey( + @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI")) + { + object val = key != null ? key.GetValue("LogPixels") : null; + if (val is int dpi) + return dpi != 96; + } + + using (var key = Registry.CurrentUser.OpenSubKey( + @"Control Panel\Desktop")) + { + object val = key != null ? key.GetValue("LogPixels") : null; + if (val is int dpi) + return dpi != 96; + } + + return false; + } + + public static void SetDpiAwarenessOverride(string exePath, bool enable) + { + const string keyPath = @"Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers"; + + RegistryKey key = Registry.CurrentUser.OpenSubKey(keyPath, true) + ?? Registry.CurrentUser.CreateSubKey(keyPath); + + if (key == null) + return; + + using (key) + { + string current = key.GetValue(exePath) as string ?? string.Empty; + + var flags = new HashSet(current.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); + + if (enable) + { + if (flags.Contains("HIGHDPIAWARE")) + return; + flags.Add("HIGHDPIAWARE"); + } + else + { + if (!flags.Contains("HIGHDPIAWARE")) + return; + flags.Remove("HIGHDPIAWARE"); + } + + if (flags.Count == 0) + key.DeleteValue(exePath, false); + else + key.SetValue(exePath, string.Join(" ", flags), RegistryValueKind.String); + } + } + } +} diff --git a/RetroBat/RetroBat/EmulationStationLauncher.cs b/RetroBat/RetroBat/EmulationStationLauncher.cs new file mode 100644 index 0000000..3271414 --- /dev/null +++ b/RetroBat/RetroBat/EmulationStationLauncher.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Windows.Forms; + +namespace RetroBat +{ + internal static class EmulationStationLauncher + { + public static string BuildArguments(RetroBatConfig config, string esPath, Screen[] screens) + { + List commandArray = new List(); + + bool borderless = config.FullscreenBorderless; + + if (config.Fullscreen && config.ForceFullscreenRes) + { + commandArray.Add("--resolution"); + commandArray.Add(config.WindowXSize.ToString()); + commandArray.Add(config.WindowYSize.ToString()); + } + else if (!config.Fullscreen && !borderless) + { + commandArray.Add("--windowed"); + commandArray.Add("--resolution"); + commandArray.Add(config.WindowXSize.ToString()); + commandArray.Add(config.WindowYSize.ToString()); + } + else if (borderless) + { + commandArray.Add("--fullscreen-borderless"); + } + else + { + commandArray.Add("--fullscreen"); + } + + if (config.GameListOnly) + commandArray.Add("--gamelist-only"); + + if (config.InterfaceMode == 2) + commandArray.Add("--force-kid"); + else if (config.InterfaceMode == 1) + commandArray.Add("--force-kiosk"); + + if (config.MonitorIndex > 0 && config.MonitorIndex < screens.Length) + { + commandArray.Add("--monitor"); + commandArray.Add(config.MonitorIndex.ToString()); + } + + if (config.NoExitMenu) + commandArray.Add("--no-exit"); + + if (config.VSync) + commandArray.Add("--vsync 1"); + else + commandArray.Add("--vsync 0"); + + if (config.DrawFramerate) + commandArray.Add("--draw-framerate"); + + commandArray.Add("--home"); + commandArray.Add(esPath); + + return string.Join(" ", commandArray.Select(a => a.Contains(" ") ? "\"" + a + "\"" : a)); + } + + public static void RunWiimoteGun(string esPath) + { + SimpleLogger.Instance.Info("Running WiimoteGun."); + + string wgunExe = Path.Combine(esPath, "WiimoteGun.exe"); + + if (!File.Exists(wgunExe)) + { + SimpleLogger.Instance.Warning("WiimoteGun executable not found at: " + wgunExe); + return; + } + + try + { + var wgStart = new ProcessStartInfo + { + FileName = wgunExe, + WorkingDirectory = esPath, + UseShellExecute = false, + CreateNoWindow = true + }; + + Process.Start(wgStart); + SimpleLogger.Instance.Info("WiimoteGun started successfully."); + } + catch (Exception ex) { SimpleLogger.Instance.Warning("Failed to start WiimoteGun: " + ex.Message); } + } + + /// Launches every configured companion app (AppLauncher, AppLauncher2... in retrobat.ini), if any, in parallel with EmulationStation. Fire-and-forget: does not wait for them to exit. + public static void RunExternalApps(IEnumerable appLaunchers) + { + if (appLaunchers == null) + return; + + foreach (var appLauncher in appLaunchers) + RunExternalApp(appLauncher); + } + + /// Launches a single companion app entry. Append " -nowindow" to the ini value to start it hidden; otherwise it starts normally. + private static void RunExternalApp(string appLauncher) + { + if (string.IsNullOrWhiteSpace(appLauncher)) + return; + + string appPath = ParseAppLauncherPath(appLauncher, out bool noWindow); + + if (string.IsNullOrWhiteSpace(appPath) || !File.Exists(appPath)) + { + SimpleLogger.Instance.Warning("AppLauncher file not found at: " + appPath); + return; + } + + SimpleLogger.Instance.Info("Starting external app: " + appPath + (noWindow ? " (no window)" : "")); + + try + { + var appStart = new ProcessStartInfo + { + FileName = appPath, + WorkingDirectory = Path.GetDirectoryName(appPath), + UseShellExecute = !noWindow, + CreateNoWindow = noWindow + }; + + Process.Start(appStart); + SimpleLogger.Instance.Info("External app started successfully."); + } + catch (Exception ex) { SimpleLogger.Instance.Warning("Failed to start external app: " + ex.Message); } + } + + private static string ParseAppLauncherPath(string raw, out bool noWindow) + { + noWindow = false; + string value = raw.Trim(); + + const string flag = "-nowindow"; + if (value.EndsWith(flag, StringComparison.OrdinalIgnoreCase)) + { + value = value.Substring(0, value.Length - flag.Length).Trim(); + noWindow = true; + } + + return value.Trim('"'); + } + + /// Starts EmulationStation and waits for/restores focus on its window. Returns false if the process failed to start. + public static bool LaunchAndFocus(ProcessStartInfo start, RetroBatConfig config, bool isExternalLauncher) + { + try + { + SimpleLogger.Instance.Info("Launching " + start.FileName + " " + start.Arguments); + + var exe = Process.Start(start); + if (exe == null) + { + SimpleLogger.Instance.Error("Failed to start EmulationStation process."); + return false; + } + + int maxWaitMs = 10000; + int intervalMs = 50; + int waited = 0; + + IntPtr esHandle = IntPtr.Zero; + + SimpleLogger.Instance.Info("Waiting for EmulationStation main window…"); + while (!exe.HasExited && esHandle == IntPtr.Zero && waited < maxWaitMs) + { + Thread.Sleep(intervalMs); + waited += intervalMs; + exe.Refresh(); + esHandle = exe.MainWindowHandle; + + if (waited % 1000 == 0) + SimpleLogger.Instance.Info($"…still waiting ({waited / 1000}s)"); + } + + if (esHandle == IntPtr.Zero) + { + SimpleLogger.Instance.Warning("EmulationStation window handle not detected (likely exclusive fullscreen). Skipping focus."); + } + + if (esHandle != IntPtr.Zero && !isExternalLauncher) + { + SplashVideo.CloseBlackSplash(); + Thread.Sleep(300); + + if (config.FocusDelay > 0) + { + Thread.Sleep(config.FocusDelay); + } + + FocusHelper.BringProcessWindowToFront(exe); + } + else + { + if (exe.HasExited) + SimpleLogger.Instance.Error("EmulationStation process exited before creating a window."); + else + SimpleLogger.Instance.Warning("EmulationStation process is running but no main window detected."); + } + } + catch (Exception ex) + { + SimpleLogger.Instance.Warning("Failed to start EmulationStation: " + ex.Message); + } + + return true; + } + } +} diff --git a/RetroBat/RetroBat/EmulationStationSettings.cs b/RetroBat/RetroBat/EmulationStationSettings.cs new file mode 100644 index 0000000..e3ae456 --- /dev/null +++ b/RetroBat/RetroBat/EmulationStationSettings.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Xml; + +namespace RetroBat +{ + internal static class EmulationStationSettings + { + private static readonly Random _rand = new Random(); + + public static void WriteLanguage(string esPath, CultureInfo culture) + { + string cultureText = culture.Name.ToString().Replace('-', '_'); + string esSettingsPath = Path.Combine(esPath, ".emulationstation", "es_settings.cfg"); + if (!File.Exists(esSettingsPath)) + { + SimpleLogger.Instance.Error("es_settings.cfg cannot be found at: " + esSettingsPath); + throw new FileNotFoundException("es_settings.cfg not found."); + } + else + SimpleLogger.Instance.Info("es_settings.cfg path: " + esSettingsPath); + + SimpleLogger.Instance.Info("Updating EmulationStation language."); + + try + { + XmlDocument xml = new XmlDocument(); + xml.Load(esSettingsPath); + XmlNode languageNode = xml.SelectSingleNode("//string[@name='Language']"); + + if (languageNode != null && languageNode.Attributes != null) + { + // Update existing node + languageNode.Attributes["value"].Value = cultureText; + } + else + { + // Create the node + XmlElement newNode = xml.CreateElement("string"); + newNode.SetAttribute("name", "Language"); + newNode.SetAttribute("value", cultureText); + + // Append to root element + XmlNode configNode = xml.SelectSingleNode("/config"); + if (configNode != null) + configNode.AppendChild(newNode); + else + SimpleLogger.Instance.Warning("Could not update EmulationStation language."); + } + xml.Save(esSettingsPath); + } + catch (Exception ex) { SimpleLogger.Instance.Warning("Could not update EmulationStation language: " + ex.Message); } + } + + public static void SetGLVersion(string esPath, bool oldOpenGL) + { + string esSettingsPath = Path.Combine(esPath, ".emulationstation", "es_settings.cfg"); + if (!File.Exists(esSettingsPath)) + { + SimpleLogger.Instance.Error("es_settings.cfg cannot be found at: " + esSettingsPath); + throw new FileNotFoundException("es_settings.cfg not found."); + } + else + SimpleLogger.Instance.Info("es_settings.cfg path: " + esSettingsPath); + + try + { + XmlDocument xml = new XmlDocument(); + xml.Load(esSettingsPath); + XmlNode GLNode = xml.SelectSingleNode("//string[@name='Renderer']"); + + if (GLNode != null && GLNode.Attributes != null) + { + if (oldOpenGL) + { + SimpleLogger.Instance.Info("es_settings.cfg, setting old renderer"); + GLNode.Attributes["value"].Value = "OPENGL 2.1"; + } + else + GLNode.RemoveAll(); + } + else if (oldOpenGL) + { + // Create the node + XmlElement newNode = xml.CreateElement("string"); + newNode.SetAttribute("name", "Renderer"); + newNode.SetAttribute("value", "OPENGL 2.1"); + + // Append to root element + XmlNode configNode = xml.SelectSingleNode("/config"); + if (configNode != null) + configNode.AppendChild(newNode); + else + SimpleLogger.Instance.Warning("Could not update EmulationStation renderer."); + } + xml.Save(esSettingsPath); + } + catch (Exception ex) { SimpleLogger.Instance.Warning("Could not update EmulationStation renderer: " + ex.Message); } + } + + public static void SetRandomTheme(string esPath, bool randomTheme) + { + if (!randomTheme) + return; + + bool updated = false; + + string esSettingsPath = Path.Combine(esPath, ".emulationstation", "es_settings.cfg"); + if (!File.Exists(esSettingsPath)) + { + SimpleLogger.Instance.Error("es_settings.cfg cannot be found at: " + esSettingsPath); + throw new FileNotFoundException("es_settings.cfg not found."); + } + else + SimpleLogger.Instance.Info("es_settings.cfg path: " + esSettingsPath); + + try + { + XmlDocument xml = new XmlDocument(); + xml.Load(esSettingsPath); + XmlNode Theme = xml.SelectSingleNode("//string[@name='ThemeSet']"); + + if (Theme != null && Theme.Attributes != null) + { + string currentTheme = Theme.Attributes["value"]?.Value; + string themesPath = Path.Combine(esPath, ".emulationstation", "themes"); + + if (Directory.Exists(themesPath)) + { + var themeDirs = Directory.GetDirectories(themesPath); + var candidates = themeDirs.Select(Path.GetFileName).Where(t => !string.Equals(t, currentTheme, StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (candidates.Length > 0) + { + string randomThemeName = candidates[_rand.Next(candidates.Length)]; + SimpleLogger.Instance.Info("es_settings.cfg, setting random theme: " + randomThemeName); + Theme.Attributes["value"].Value = randomThemeName; + updated = true; + } + else + SimpleLogger.Instance.Warning("No themes found in themes directory."); + } + else + SimpleLogger.Instance.Warning("Themes directory not found at: " + themesPath); + } + if (updated) + xml.Save(esSettingsPath); + } + catch (Exception ex) { SimpleLogger.Instance.Warning("Could not update EmulationStation theme: " + ex.Message); } + } + + public static void ResetToDefaults(string path) + { + SimpleLogger.Instance.Info("Resetting configuration."); + + List filesToReset = new List + { + "es_input.cfg", + "es_padtokey.cfg", + "es_settings.cfg", + "es_systems.cfg" + }; + + string templatepathES = Path.Combine(path, "system", "templates", "emulationstation"); + string esPath = Path.Combine(path, "emulationstation"); + string targetPath = Path.Combine(esPath, ".emulationstation"); + + foreach (var file in filesToReset) + { + string sourceFile = Path.Combine(templatepathES, file); + string targetFile = Path.Combine(targetPath, file); + + if (File.Exists(sourceFile)) + { + try + { + string oldFile = targetFile + ".old"; + File.Delete(oldFile); + File.Move(targetFile, oldFile); + File.Copy(sourceFile, targetFile, true); + SimpleLogger.Instance.Info($"Reset {file} to default."); + } + catch (Exception ex) { SimpleLogger.Instance.Warning($"Could not reset {file}: " + ex.Message); } + } + else + SimpleLogger.Instance.Warning($"Template file {sourceFile} does not exist."); + } + + string rbIniFile = Path.Combine(path, "retrobat.ini"); + + try + { + if (File.Exists(rbIniFile)) + { + try { File.Delete(rbIniFile); } + catch (Exception ex) { SimpleLogger.Instance.Warning("Could not delete RetroBat ini file: " + ex.Message); } + + SimpleLogger.Instance.Info("Deleted RetroBat ini file: " + rbIniFile); + } + + try + { + string iniDefault = IniFile.GetDefaultIniContent(); + File.WriteAllText(rbIniFile, iniDefault); + SimpleLogger.Instance.Info("ini file regenrated with default values."); + } + catch { SimpleLogger.Instance.Warning("Impossible to create ini file."); } + } + catch { SimpleLogger.Instance.Warning("Could not reinitialize ini file."); } + } + } +} diff --git a/RetroBat/RetroBat/IniReader.cs b/RetroBat/RetroBat/IniReader.cs index 11ac149..da05b65 100644 --- a/RetroBat/RetroBat/IniReader.cs +++ b/RetroBat/RetroBat/IniReader.cs @@ -63,6 +63,11 @@ public static string GetDefaultIniContent() ; Run WiimoteGun at RetroBat's startup. You can use your wiimote as a gun and navigate through EmulationStation. WiimoteGun=0 +; Path to an additional application to launch in parallel with RetroBat (.exe, .bat, .cmd...). Leave empty to disable. Use quotes if the path contains spaces. +; Add -nowindow at the end to start it without a visible window (e.g. AppLauncher=""C:\tools\app.exe"" -nowindow). Without -nowindow, it starts normally. +; To launch more than one application, add AppLauncher2, AppLauncher3, etc. with the same syntax. +AppLauncher= + [SplashScreen] ; Set if video introduction is played before running the interface. @@ -239,7 +244,8 @@ public IniFile(string path, IniOptions options = (IniOptions)0) } catch (Exception ex) { - throw ex; + SimpleLogger.Instance.Error("[IniFile] Failed to parse ini file " + path, ex); + throw; } } @@ -661,6 +667,7 @@ public class RetroBatConfig public int Autostart { get; set; } public int AutoStartDelay { get; set; } public bool WiimoteGun { get; set; } + public List AppLaunchers { get; set; } public bool EnableIntro { get; set; } public string FileName { get; set; } public string FilePath { get; set; } diff --git a/RetroBat/RetroBat/Program.cs b/RetroBat/RetroBat/Program.cs index 171eb45..3316afc 100644 --- a/RetroBat/RetroBat/Program.cs +++ b/RetroBat/RetroBat/Program.cs @@ -1,18 +1,11 @@ -using Microsoft.Win32; using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Windows.Forms; -using System.Xml; -using System.Xml.Linq; -using static System.Windows.Forms.VisualStyles.VisualStyleElement.Rebar; namespace RetroBat { @@ -56,7 +49,7 @@ static void Main(string[] args) { MessageBox.Show("Executable name has been changed!", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); } - + SimpleLogger.Instance.Info("[Startup] RetroBat.exe"); CultureInfo windowsCulture = CultureInfo.CurrentUICulture; @@ -80,104 +73,16 @@ static void Main(string[] args) } // Check existence of required files - SimpleLogger.Instance.Info("Checking availability of necessary files."); - string templatepathES = Path.Combine(appFolder, "system", "templates", "emulationstation"); - string versionInfoFile = Path.Combine(appFolder, "system", "version.info"); - - // ES folder - var esFiles = new HashSet(Directory.EnumerateFiles(esPath).Select(Path.GetFileName),StringComparer.OrdinalIgnoreCase); - - // about.info - if (!esFiles.Contains("about.info")) - { - SimpleLogger.Instance.Warning("Creating file 'about.info'"); - try { File.WriteAllText(Path.Combine(esPath, "about.info"), "RETROBAT"); } - catch { SimpleLogger.Instance.Warning("Impossible to create about.info file."); } - } - - // emulationstation - if (!esFiles.Contains("emulationstation.exe")) - { - SimpleLogger.Instance.Error("EmulationStation cannot be found at: " + Path.Combine(esPath, "emulationstation.exe")); - throw new FileNotFoundException("EmulationStation executable not found."); - } - - // emulatorlauncher - if (!esFiles.Contains("emulatorlauncher.exe")) - { - SimpleLogger.Instance.Error("EmulatorLauncher cannot be found at: " + Path.Combine(esPath, "emulatorlauncher.exe")); - throw new FileNotFoundException("EmulatorLauncher executable not found."); - } - - // optional - if (!esFiles.Contains("batocera-store.exe")) - SimpleLogger.Instance.Warning("Batocera-store executable not found, continuing without it."); - - if (!esFiles.Contains("batocera-systems.exe")) - SimpleLogger.Instance.Warning("Batocera-systems executable not found, continuing without it."); - - if (!esFiles.Contains("es-update.exe")) - SimpleLogger.Instance.Warning("es-update executable not found, continuing without it."); - - if (!esFiles.Contains("es-checkversion.exe")) - SimpleLogger.Instance.Warning("es-checkversion executable not found, continuing without it."); - - if (!esFiles.Contains("emulatorlauncher.common.dll")) - { - SimpleLogger.Instance.Error("emulatorlauncher common DLL does not exist"); - throw new FileNotFoundException("emulatorlauncher common DLL not found."); - } - - // check that es_features exists - if (!File.Exists(Path.Combine(esPath, ".emulationstation", "es_features.cfg"))) - { - SimpleLogger.Instance.Error("es_features cannot be found at: " + Path.Combine(esPath, ".emulationstation", "es_features.cfg")); - throw new FileNotFoundException("es_features not found."); - } - - // check that es_settings exists - if (!File.Exists(Path.Combine(esPath, ".emulationstation", "es_systems.cfg"))) - { - SimpleLogger.Instance.Warning("es_systems cannot be found, trying to copy template."); - - try { File.Copy(Path.Combine(templatepathES, "es_systems.cfg"), Path.Combine(esPath, ".emulationstation", "es_systems.cfg"), true); } catch { } - - if (!File.Exists(Path.Combine(esPath, ".emulationstation", "es_systems.cfg"))) - { - SimpleLogger.Instance.Error("es_systems cannot be found at: " + Path.Combine(esPath, ".emulationstation", "es_systems.cfg")); - throw new FileNotFoundException("es_systems not found."); - } - } - - // check that emulatorlauncher.cfg exists - if (!File.Exists(Path.Combine(esPath, "emulatorLauncher.cfg"))) - { - SimpleLogger.Instance.Warning("emulatorLauncher.cfg cannot be found, trying to copy template."); - - try { File.Copy(Path.Combine(templatepathES, "emulatorLauncher.cfg"), Path.Combine(esPath, "emulatorLauncher.cfg"), true); } catch { } - - if (!File.Exists(Path.Combine(esPath, "emulatorLauncher.cfg"))) - { - SimpleLogger.Instance.Error("emulatorLauncher.cfg cannot be found at: " + Path.Combine(esPath, "emulatorLauncher.cfg")); - throw new FileNotFoundException("emulatorLauncher.cfg not found."); - } - } - SimpleLogger.Instance.Info("All necessary files exist."); + StartupFileChecker.EnsureRequiredFilesExist(appFolder, esPath); // Write path to registry RegistryTools.SetRegistryKey(appFolder); // Get values from ini file - RetroBatConfig config = new RetroBatConfig(); - - using (IniFile ini = new IniFile(iniPath)) - { - SimpleLogger.Instance.Info("Reading values from inifile: " + iniPath); - config = GetConfigValues(ini); + RetroBatConfig config = RetroBatConfigLoader.Load(iniPath); - foreach (PropertyInfo prop in config.GetType().GetProperties()) - try { SimpleLogger.Instance.Info($"{prop.Name} = {prop.GetValue(config, null)}"); } catch { } - } + // Launch companion apps as early as possible, in parallel with the rest of the startup sequence + EmulationStationLauncher.RunExternalApps(config.AppLaunchers); // Get emulationstation.exe path string emulationStationExe = Path.Combine(esPath, "emulationstation.exe"); @@ -190,62 +95,25 @@ static void Main(string[] args) SimpleLogger.Instance.Info("EmulationStation.exe found."); // DPI Awareness - if (HasDpiScaling()) - { - string dpiFile = Path.Combine(appFolder, "system", "tools", "dpi_awareness.txt"); - - if (File.Exists(dpiFile)) - { - try - { - var dpiLines = File.ReadAllLines(dpiFile); - - if (dpiLines.Length > 0) - { - foreach (var dpiLine in dpiLines) - { - string dpiExePath = Path.Combine(appFolder, dpiLine.Trim()); - - if (File.Exists(dpiExePath)) - SetDpiAwarenessOverride(dpiExePath, true); - } - } - } - catch { } - } - } + DpiAwarenessManager.ApplyOverridesIfNeeded(appFolder); // Language if (config.LanguageDetection) - WriteLanguageToES(esPath, windowsCulture); + EmulationStationSettings.WriteLanguage(esPath, windowsCulture); // Set old OpenGL - SetGLVersion(esPath, config.OpenGL2_1); + EmulationStationSettings.SetGLVersion(esPath, config.OpenGL2_1); // Set theme to random if enabled - SetRandomTheme(esPath, config.RandomTheme); + EmulationStationSettings.SetRandomTheme(esPath, config.RandomTheme); // Set RetroBat to start at startup - CleanupStartup(); - if (config.Autostart == 1) - { - AddToStartupFolder(appFolder, "RetroBat.exe"); - RemoveFromStartupReg(); - } - else if (config.Autostart == 2) - { - AddToStartupReg(appFolder, "RetroBat.exe"); - RemoveFromStartupFolder("RetroBat"); - } - else - { - RemoveFromStartupReg(); - RemoveFromStartupFolder("RetroBat"); - } + AutostartManager.CleanupLegacyShortcut(); + AutostartManager.Apply(config.Autostart, appFolder, "RetroBat.exe"); // Reset es_settings if (config.ResetConfigMode) - ResetESConfig(appFolder); + EmulationStationSettings.ResetToDefaults(appFolder); // Run splash video if enabled var screens = Screen.AllScreens; @@ -270,7 +138,7 @@ static void Main(string[] args) SplashVideo.ShowBlackSplash(targetScreen); var splashStart = DateTime.UtcNow; - var videoDone = SplashVideo.RunIntroVideo(config, esPath, targetScreen); + var videoDone = SplashVideo.RunIntroVideo(config, esPath, targetScreen, isExternalLauncher); // Wait depending on mode if (config.WaitForVideoEnd) @@ -294,67 +162,11 @@ static void Main(string[] args) // Arguments SimpleLogger.Instance.Info("Setting up arguments to run EmulationStation."); - List commandArray = new List(); - - bool borderless = config.FullscreenBorderless; - - if (config.Fullscreen && config.ForceFullscreenRes) - { - commandArray.Add("--resolution"); - commandArray.Add(config.WindowXSize.ToString()); - commandArray.Add(config.WindowYSize.ToString()); - } - - else if (!config.Fullscreen && !borderless) - { - commandArray.Add("--windowed"); - commandArray.Add("--resolution"); - commandArray.Add(config.WindowXSize.ToString()); - commandArray.Add(config.WindowYSize.ToString()); - } - - else if (borderless) - { - commandArray.Add("--fullscreen-borderless"); - } - else - { - commandArray.Add("--fullscreen"); - } - - if (config.GameListOnly) - commandArray.Add("--gamelist-only"); - - if (config.InterfaceMode == 2) - commandArray.Add("--force-kid"); - else if (config.InterfaceMode == 1) - commandArray.Add("--force-kiosk"); - - if (config.MonitorIndex > 0 && config.MonitorIndex < screens.Length) - { - commandArray.Add("--monitor"); - commandArray.Add(config.MonitorIndex.ToString()); - } - - if (config.NoExitMenu) - commandArray.Add("--no-exit"); - - if (config.VSync) - commandArray.Add("--vsync 1"); - else - commandArray.Add("--vsync 0"); - - if (config.DrawFramerate) - commandArray.Add("--draw-framerate"); - - commandArray.Add("--home"); - commandArray.Add(esPath); - - string elargs = string.Join(" ", commandArray.Select(a => a.Contains(" ") ? "\"" + a + "\"" : a)); + string elargs = EmulationStationLauncher.BuildArguments(config, esPath, screens); // Run wiimoteGun if enabled if (config.WiimoteGun) - RunWiimoteGun(esPath); + EmulationStationLauncher.RunWiimoteGun(esPath); // Run EmulationStation SimpleLogger.Instance.Info("Preparing to run emulationstation."); @@ -367,76 +179,16 @@ static void Main(string[] args) UseShellExecute = false }; - if (start == null) - return; - TimeSpan uptime = TimeSpan.FromMilliseconds(Environment.TickCount); if (config.Autostart != 0 && uptime.TotalSeconds < 10 && config.AutoStartDelay > 0) { SimpleLogger.Instance.Info("RetroBat set to run at startup, adding a delay."); int delay = config.AutoStartDelay; - System.Threading.Thread.Sleep(delay); + Thread.Sleep(delay); } - try - { - SimpleLogger.Instance.Info("Launching " + emulationStationExe + " " + elargs); - - var exe = Process.Start(start); - if (exe == null) - { - SimpleLogger.Instance.Error("Failed to start EmulationStation process."); - return; - } - - int maxWaitMs = 10000; - int intervalMs = 50; - int waited = 0; - - IntPtr esHandle = IntPtr.Zero; - - SimpleLogger.Instance.Info("Waiting for EmulationStation main window…"); - while (!exe.HasExited && esHandle == IntPtr.Zero && waited < maxWaitMs) - { - Thread.Sleep(intervalMs); - waited += intervalMs; - exe.Refresh(); - esHandle = exe.MainWindowHandle; - - if (waited % 1000 == 0) - SimpleLogger.Instance.Info($"…still waiting ({waited / 1000}s)"); - } - - if (esHandle == IntPtr.Zero) - { - SimpleLogger.Instance.Warning("EmulationStation window handle not detected (likely exclusive fullscreen). Skipping focus."); - } - - if (esHandle != IntPtr.Zero && !isExternalLauncher) - { - SplashVideo.CloseBlackSplash(); - Thread.Sleep(300); - - if (config.FocusDelay > 0) - { - Thread.Sleep(config.FocusDelay); - } - - FocusHelper.BringProcessWindowToFront(exe); - } - - else - { - if (exe.HasExited) - SimpleLogger.Instance.Error("EmulationStation process exited before creating a window."); - else - SimpleLogger.Instance.Warning("EmulationStation process is running but no main window detected."); - } - } - catch (Exception ex) - { - SimpleLogger.Instance.Warning("Failed to start EmulationStation: " + ex.Message); - } + if (!EmulationStationLauncher.LaunchAndFocus(start, config, isExternalLauncher)) + return; } finally @@ -446,468 +198,5 @@ static void Main(string[] args) SimpleLogger.Instance.Info("All is good, enjoy, quitting RetroBat launcher."); } - - private static RetroBatConfig GetConfigValues(IniFile ini) - { - RetroBatConfig config = new RetroBatConfig - { - LanguageDetection = GetOptBoolean(IniFile.GetOptionValue(ini, "RetroBat", "LanguageDetection", "true")), - ResetConfigMode = GetOptBoolean(IniFile.GetOptionValue(ini, "RetroBat", "ResetConfigMode", "false")), - WiimoteGun = GetOptBoolean(IniFile.GetOptionValue(ini, "RetroBat", "WiimoteGun", "false")), - EnableIntro = GetOptBoolean(IniFile.GetOptionValue(ini, "SplashScreen", "EnableIntro", "true")), - RandomVideo = GetOptBoolean(IniFile.GetOptionValue(ini, "SplashScreen", "RandomVideo", "true")), - GamepadVideoKill = GetOptBoolean(IniFile.GetOptionValue(ini, "SplashScreen", "GamepadVideoKill", "true")), - KillVideoWhenESReady = GetOptBoolean(IniFile.GetOptionValue(ini, "SplashScreen", "KillVideoWhenESReady", "false")), - WaitForVideoEnd = GetOptBoolean(IniFile.GetOptionValue(ini, "SplashScreen", "WaitForVideoEnd", "true")), - FileName = IniFile.GetOptionValue(ini, "SplashScreen", "FileName", "retrobat-neon.mp4"), - FilePath = IniFile.GetOptionValue(ini, "SplashScreen", "FilePath", "default"), - Fullscreen = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "Fullscreen", "true")), - FullscreenBorderless = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "FullscreenBorderless", "true")), - ForceFullscreenRes = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "ForceFullscreenRes", "false")), - GameListOnly = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "GameListOnly", "false")), - NoExitMenu = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "NoExitMenu", "false")), - OpenGL2_1 = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "OpenGL2_1", "false")), - VSync = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "VSync", "true")), - DrawFramerate = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "DrawFramerate", "false")), - RandomTheme = GetOptBoolean(IniFile.GetOptionValue(ini, "EmulationStation", "RandomTheme", "false")) - }; - - if (int.TryParse(IniFile.GetOptionValue(ini, "RetroBat", "Autostart", "0"), out int Autostart)) - config.Autostart = Autostart; - else - config.Autostart = 0; - - if (int.TryParse(IniFile.GetOptionValue(ini, "RetroBat", "AutoStartDelay", "0"), out int startdelay)) - config.AutoStartDelay = startdelay; - else - config.AutoStartDelay = 0; - - if (int.TryParse(IniFile.GetOptionValue(ini, "EmulationStation", "FocusDelay", "2000"), out int FocusDelay)) - config.FocusDelay = FocusDelay; - else - config.FocusDelay = 1000; - - if (int.TryParse(IniFile.GetOptionValue(ini, "SplashScreen", "VideoDelay", "5000"), out int VideoDelay)) - config.VideoDelay = VideoDelay; - else - config.VideoDelay = 1000; - - if (int.TryParse(IniFile.GetOptionValue(ini, "EmulationStation", "InterfaceMode", "0"), out int interfaceMode)) - config.InterfaceMode = interfaceMode; - else - config.InterfaceMode = 0; - - if (int.TryParse(IniFile.GetOptionValue(ini, "EmulationStation", "MonitorIndex", "0"), out int monitorIndex)) - config.MonitorIndex = monitorIndex; - else - config.MonitorIndex = 0; - - if (int.TryParse(IniFile.GetOptionValue(ini, "EmulationStation", "WindowXSize", "1280"), out int windowX)) - config.WindowXSize = windowX; - else - config.WindowXSize = 1280; - - if (int.TryParse(IniFile.GetOptionValue(ini, "EmulationStation", "WindowYSize", "720"), out int windowY)) - config.WindowYSize = windowY; - else - config.WindowYSize = 720; - - return config; - } - - public static bool GetOptBoolean(string input) - { - if (input == "1" || input == "true" || input == "yes") - return true; - else - return false; - } - - private static void AddToStartupReg(string appPath, string appExe) - { - SimpleLogger.Instance.Info("Setting RetroBat to launch at startup."); - - string batPath = Path.Combine(appPath, appExe); - - string regValue = string.Format( - "cmd.exe /c \"cd /d {0} && start \"\" \"{1}\"\"\"", - appPath, - batPath - ); - - try - { - RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); - key.SetValue("RetroBat", regValue); - SimpleLogger.Instance.Info("RetroBat set in registry to startup."); - } - catch (Exception ex) - { - SimpleLogger.Instance.Warning("Failed to set startup registry key: " + ex.Message); - } - } - - private static void AddToStartupFolder(string exePath, string shortcutName) - { - try - { - string startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup); - string exeName = Path.GetFileNameWithoutExtension(shortcutName); - string batPath = Path.Combine(startupFolder, exeName + ".bat"); - string exe = Path.Combine(exePath, shortcutName); - - // Write a simple batch file to start RetroBat - string batContent = $"@echo off{Environment.NewLine}cd /d \"{exePath}\"{Environment.NewLine}\"{exe}\""; - File.WriteAllText(batPath, batContent); - - SimpleLogger.Instance.Info("RetroBat batch added to Startup folder: " + batPath); - } - catch (Exception ex) - { - SimpleLogger.Instance.Warning("Failed to add RetroBat to Startup folder: " + ex.Message); - } - } - - private static void RemoveFromStartupFolder(string shortcutName) - { - try - { - string startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup); - string batPath = Path.Combine(startupFolder, shortcutName + ".bat"); - - if (File.Exists(batPath)) - { - File.Delete(batPath); - SimpleLogger.Instance.Info("RetroBat removed from Startup folder: " + batPath); - } - else - { - SimpleLogger.Instance.Info("RetroBat startup batch not found, nothing to remove."); - } - } - catch (Exception ex) - { - SimpleLogger.Instance.Warning("Failed to remove RetroBat from Startup folder: " + ex.Message); - } - } - - private static void CleanupStartup() - { - try - { - string startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup); - string linkStartup = Path.Combine(startupFolder, "RetroBat.lnk"); - - if (File.Exists(linkStartup)) - { - try { File.Delete(linkStartup); } catch { } - } - } - catch { } - } - - private static void RemoveFromStartupReg() - { - SimpleLogger.Instance.Info("Ensuring RetroBat does not launch at startup."); - - try - { - RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); - key.DeleteValue("RetroBat"); - } - catch (Exception ex) - { - SimpleLogger.Instance.Warning("Failed to remove startup registry key: " + ex.Message); - } - } - - private static void RunWiimoteGun(string esPath) - { - SimpleLogger.Instance.Info("Running WiimoteGun."); - - string wgunExe = Path.Combine(esPath, "WiimoteGun.exe"); - - if (!File.Exists(wgunExe)) - { - SimpleLogger.Instance.Warning("WiimoteGun executable not found at: " + wgunExe); - return; - } - - try - { - var wgStart = new ProcessStartInfo - { - FileName = wgunExe, - WorkingDirectory = esPath, - UseShellExecute = false, - CreateNoWindow = true - }; - - Process.Start(wgStart); - SimpleLogger.Instance.Info("WiimoteGun started successfully."); - } - catch (Exception ex) { SimpleLogger.Instance.Warning("Failed to start WiimoteGun: " + ex.Message); } - } - - private static void ResetESConfig(string path) - { - SimpleLogger.Instance.Info("Resetting configuration."); - - List filesToReset = new List - { - "es_input.cfg", - "es_padtokey.cfg", - "es_settings.cfg", - "es_systems.cfg" - }; - - string templatepathES = Path.Combine(path, "system", "templates", "emulationstation"); - string esPath = Path.Combine(path, "emulationstation"); - string targetPath = Path.Combine(esPath, ".emulationstation"); - - foreach (var file in filesToReset) - { - string sourceFile = Path.Combine(templatepathES, file); - string targetFile = Path.Combine(targetPath, file); - - if (File.Exists(sourceFile)) - { - try - { - string oldFile = targetFile + ".old"; - File.Delete(oldFile); - File.Move(targetFile, oldFile); - File.Copy(sourceFile, targetFile, true); - SimpleLogger.Instance.Info($"Reset {file} to default."); - } - catch (Exception ex) { SimpleLogger.Instance.Warning($"Could not reset {file}: " + ex.Message); } - } - else - SimpleLogger.Instance.Warning($"Template file {sourceFile} does not exist."); - } - - string rbIniFile = Path.Combine(path, "retrobat.ini"); - - try - { - if (File.Exists(rbIniFile)) - { - try { File.Delete(rbIniFile); } - catch (Exception ex) { SimpleLogger.Instance.Warning("Could not delete RetroBat ini file: " + ex.Message); } - - SimpleLogger.Instance.Info("Deleted RetroBat ini file: " + rbIniFile); - } - - try - { - string iniDefault = IniFile.GetDefaultIniContent(); - File.WriteAllText(rbIniFile, iniDefault); - SimpleLogger.Instance.Info("ini file regenrated with default values."); - } - catch { SimpleLogger.Instance.Warning("Impossible to create ini file."); } - } - catch { SimpleLogger.Instance.Warning("Could not reinitialize ini file."); } - } - - private static void WriteLanguageToES(string esPath, CultureInfo culture) - { - string cultureText = culture.Name.ToString().Replace('-', '_'); - string esSettingsPath = Path.Combine(esPath, ".emulationstation", "es_settings.cfg"); - if (!File.Exists(esSettingsPath)) - { - SimpleLogger.Instance.Error("es_settings.cfg cannot be found at: " + esSettingsPath); - throw new FileNotFoundException("es_settings.cfg not found."); - } - else - SimpleLogger.Instance.Info("es_settings.cfg path: " + esSettingsPath); - - SimpleLogger.Instance.Info("Updating EmulationStation language."); - - try - { - XmlDocument xml = new XmlDocument(); - xml.Load(esSettingsPath); - XmlNode languageNode = xml.SelectSingleNode("//string[@name='Language']"); - - if (languageNode != null && languageNode.Attributes != null) - { - // Update existing node - languageNode.Attributes["value"].Value = cultureText; - } - else - { - // Create the node - XmlElement newNode = xml.CreateElement("string"); - newNode.SetAttribute("name", "Language"); - newNode.SetAttribute("value", cultureText); - - // Append to root element - XmlNode configNode = xml.SelectSingleNode("/config"); - if (configNode != null) - configNode.AppendChild(newNode); - else - SimpleLogger.Instance.Warning("Could not update EmulationStation language."); - } - xml.Save(esSettingsPath); - } - catch (Exception ex) { SimpleLogger.Instance.Warning("Could not update EmulationStation language: " + ex.Message); } - } - - private static void SetGLVersion(string esPath, bool oldOpenGL) - { - string esSettingsPath = Path.Combine(esPath, ".emulationstation", "es_settings.cfg"); - if (!File.Exists(esSettingsPath)) - { - SimpleLogger.Instance.Error("es_settings.cfg cannot be found at: " + esSettingsPath); - throw new FileNotFoundException("es_settings.cfg not found."); - } - else - SimpleLogger.Instance.Info("es_settings.cfg path: " + esSettingsPath); - - try - { - XmlDocument xml = new XmlDocument(); - xml.Load(esSettingsPath); - XmlNode GLNode = xml.SelectSingleNode("//string[@name='Renderer']"); - - if (GLNode != null && GLNode.Attributes != null) - { - if (oldOpenGL) - { - SimpleLogger.Instance.Info("es_settings.cfg, setting old renderer"); - GLNode.Attributes["value"].Value = "OPENGL 2.1"; - } - else - GLNode.RemoveAll(); - } - else if (oldOpenGL) - { - // Create the node - XmlElement newNode = xml.CreateElement("string"); - newNode.SetAttribute("name", "Renderer"); - newNode.SetAttribute("value", "OPENGL 2.1"); - - // Append to root element - XmlNode configNode = xml.SelectSingleNode("/config"); - if (configNode != null) - configNode.AppendChild(newNode); - else - SimpleLogger.Instance.Warning("Could not update EmulationStation renderer."); - } - xml.Save(esSettingsPath); - } - catch (Exception ex) { SimpleLogger.Instance.Warning("Could not update EmulationStation renderer: " + ex.Message); } - } - - private static readonly Random _rand = new Random(); - - private static void SetRandomTheme(string esPath, bool randomTheme) - { - if (!randomTheme) - return; - - bool updated = false; - - string esSettingsPath = Path.Combine(esPath, ".emulationstation", "es_settings.cfg"); - if (!File.Exists(esSettingsPath)) - { - SimpleLogger.Instance.Error("es_settings.cfg cannot be found at: " + esSettingsPath); - throw new FileNotFoundException("es_settings.cfg not found."); - } - else - SimpleLogger.Instance.Info("es_settings.cfg path: " + esSettingsPath); - - try - { - XmlDocument xml = new XmlDocument(); - xml.Load(esSettingsPath); - XmlNode Theme = xml.SelectSingleNode("//string[@name='ThemeSet']"); - - if (Theme != null && Theme.Attributes != null) - { - string currentTheme = Theme.Attributes["value"]?.Value; - string themesPath = Path.Combine(esPath, ".emulationstation", "themes"); - - if (Directory.Exists(themesPath)) - { - var themeDirs = Directory.GetDirectories(themesPath); - var candidates = themeDirs.Select(Path.GetFileName).Where(t => !string.Equals(t, currentTheme, StringComparison.OrdinalIgnoreCase)).ToArray(); - - if (candidates.Length > 0) - { - string randomThemeName = candidates[_rand.Next(candidates.Length)]; - SimpleLogger.Instance.Info("es_settings.cfg, setting random theme: " + randomThemeName); - Theme.Attributes["value"].Value = randomThemeName; - updated = true; - } - else - SimpleLogger.Instance.Warning("No themes found in themes directory."); - } - else - SimpleLogger.Instance.Warning("Themes directory not found at: " + themesPath); - } - if (updated) - xml.Save(esSettingsPath); - } - catch (Exception ex) { SimpleLogger.Instance.Warning("Could not update EmulationStation theme: " + ex.Message); } - } - - public static bool HasDpiScaling() - { - using (var key = Registry.LocalMachine.OpenSubKey( - @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI")) - { - object val = key != null ? key.GetValue("LogPixels") : null; - if (val is int dpi) - return dpi != 96; - } - - using (var key = Registry.CurrentUser.OpenSubKey( - @"Control Panel\Desktop")) - { - object val = key != null ? key.GetValue("LogPixels") : null; - if (val is int dpi) - return dpi != 96; - } - - return false; - } - - public static void SetDpiAwarenessOverride(string exePath, bool enable) - { - const string keyPath = @"Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers"; - - RegistryKey key = Registry.CurrentUser.OpenSubKey(keyPath, true) - ?? Registry.CurrentUser.CreateSubKey(keyPath); - - if (key == null) - return; - - using (key) - { - string current = key.GetValue(exePath) as string ?? string.Empty; - - var flags = new HashSet(current.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); - - if (enable) - { - if (flags.Contains("HIGHDPIAWARE")) - return; - flags.Add("HIGHDPIAWARE"); - } - else - { - if (!flags.Contains("HIGHDPIAWARE")) - return; - flags.Remove("HIGHDPIAWARE"); - } - - if (flags.Count == 0) - key.DeleteValue(exePath, false); - else - key.SetValue(exePath, string.Join(" ", flags), RegistryValueKind.String); - } - } } } - diff --git a/RetroBat/RetroBat/RawInput.cs b/RetroBat/RetroBat/RawInput.cs deleted file mode 100644 index 60ad937..0000000 --- a/RetroBat/RetroBat/RawInput.cs +++ /dev/null @@ -1,120 +0,0 @@ -using RetroBat; -using System; -using System.Drawing; -using System.Linq; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -namespace RetroBat -{ - public abstract class RawInputForm : Form - { - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - - SimpleLogger.Instance.Info("RawInputForm started, registering raw input devices..."); - - // Register to receive raw input from gamepads (usage page 1, usage 5 = gamepad) - RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1]; - rid[0].usUsagePage = 0x01; // Generic Desktop Controls - rid[0].usUsage = 0x05; // Gamepad (use 0x04 for Joystick) - rid[0].dwFlags = RIDEV_INPUTSINK; // Receive input even if not focused - rid[0].hwndTarget = this.Handle; - - if (!RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE)))) - SimpleLogger.Instance.Warning("Failed to register raw input device(s)."); - else - SimpleLogger.Instance.Info("Registered raw input device(s) successfully."); - } - - protected bool RawInputDetected { get; private set; } - - protected override void WndProc(ref Message m) - { - if (m.Msg == WM_INPUT) - { - uint dwSize = 0; - // First get the size of the raw input data - GetRawInputData(m.LParam, RID_INPUT, IntPtr.Zero, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))); - - if (dwSize > 0) - { - IntPtr buffer = Marshal.AllocHGlobal((int)dwSize); - try - { - uint readSize = GetRawInputData(m.LParam, RID_INPUT, buffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))); - if (readSize == dwSize) - { - // Read the header - RAWINPUTHEADER header = (RAWINPUTHEADER)Marshal.PtrToStructure(buffer, typeof(RAWINPUTHEADER)); - - if (header.dwType == RIM_TYPEHID) - { - IntPtr pRawHidData = IntPtr.Add(buffer, Marshal.SizeOf(typeof(RAWINPUTHEADER))); - RAWHID rawHid = (RAWHID)Marshal.PtrToStructure(pRawHidData, typeof(RAWHID)); - IntPtr pRawData = IntPtr.Add(pRawHidData, Marshal.SizeOf(typeof(RAWHID))); - - int rawDataLength = (int)(rawHid.dwSizeHid * rawHid.dwCount); - byte[] rawData = new byte[rawDataLength]; - Marshal.Copy(pRawData, rawData, 0, rawDataLength); - - - if (rawData.Any(r => r >= 1 && r <= 64)) - RawInputDetected = true; - } - } - } - finally - { - Marshal.FreeHGlobal(buffer); - } - } - return; - } - base.WndProc(ref m); - } - - #region Api - // Constants - const int WM_INPUT = 0x00FF; - const uint RID_INPUT = 0x10000003; - const uint RIM_TYPEHID = 2; - const uint RIDEV_INPUTSINK = 0x00000100; - - // P/Invoke declarations - [DllImport("User32.dll")] - static extern bool RegisterRawInputDevices(RAWINPUTDEVICE[] pRawInputDevices, uint uiNumDevices, uint cbSize); - - [DllImport("User32.dll")] - static extern uint GetRawInputData(IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader); - - // Structures - [StructLayout(LayoutKind.Sequential)] - struct RAWINPUTDEVICE - { - public ushort usUsagePage; - public ushort usUsage; - public uint dwFlags; - public IntPtr hwndTarget; - } - - [StructLayout(LayoutKind.Sequential)] - struct RAWINPUTHEADER - { - public uint dwType; - public uint dwSize; - public IntPtr hDevice; - public IntPtr wParam; - } - - [StructLayout(LayoutKind.Sequential)] - struct RAWHID - { - public uint dwSizeHid; - public uint dwCount; - // Followed by variable length raw data, handled manually - } - #endregion - } -} \ No newline at end of file diff --git a/RetroBat/RetroBat/RetroBat.csproj b/RetroBat/RetroBat/RetroBat.csproj index abd3d61..a01e627 100644 --- a/RetroBat/RetroBat/RetroBat.csproj +++ b/RetroBat/RetroBat/RetroBat.csproj @@ -1,4 +1,4 @@ - + @@ -58,16 +58,19 @@ + + + + - - Form - + + Form @@ -75,7 +78,6 @@ - diff --git a/RetroBat/RetroBat/RetroBatConfigLoader.cs b/RetroBat/RetroBat/RetroBatConfigLoader.cs new file mode 100644 index 0000000..2b7c8c2 --- /dev/null +++ b/RetroBat/RetroBat/RetroBatConfigLoader.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace RetroBat +{ + internal static class RetroBatConfigLoader + { + public static RetroBatConfig Load(string iniPath) + { + RetroBatConfig config; + + using (IniFile ini = new IniFile(iniPath)) + { + SimpleLogger.Instance.Info("Reading values from inifile: " + iniPath); + config = GetConfigValues(ini); + + foreach (PropertyInfo prop in config.GetType().GetProperties()) + { + try + { + object value = prop.GetValue(config, null); + if (value is List list) + value = string.Join(", ", list); + + SimpleLogger.Instance.Info($"{prop.Name} = {value}"); + } + catch (Exception ex) { SimpleLogger.Instance.Warning($"Failed to log config property '{prop.Name}': " + ex.Message); } + } + } + + return config; + } + + private static RetroBatConfig GetConfigValues(IniFile ini) + { + return new RetroBatConfig + { + LanguageDetection = GetOptBool(ini, "RetroBat", "LanguageDetection", true), + ResetConfigMode = GetOptBool(ini, "RetroBat", "ResetConfigMode", false), + Autostart = GetOptInt(ini, "RetroBat", "Autostart", 0), + AutoStartDelay = GetOptInt(ini, "RetroBat", "AutoStartDelay", 0), + WiimoteGun = GetOptBool(ini, "RetroBat", "WiimoteGun", false), + AppLaunchers = GetAppLauncherEntries(ini), + EnableIntro = GetOptBool(ini, "SplashScreen", "EnableIntro", true), + RandomVideo = GetOptBool(ini, "SplashScreen", "RandomVideo", true), + GamepadVideoKill = GetOptBool(ini, "SplashScreen", "GamepadVideoKill", true), + KillVideoWhenESReady = GetOptBool(ini, "SplashScreen", "KillVideoWhenESReady", false), + WaitForVideoEnd = GetOptBool(ini, "SplashScreen", "WaitForVideoEnd", true), + FileName = IniFile.GetOptionValue(ini, "SplashScreen", "FileName", "retrobat-neon.mp4"), + FilePath = IniFile.GetOptionValue(ini, "SplashScreen", "FilePath", "default"), + VideoDelay = GetOptInt(ini, "SplashScreen", "VideoDelay", 1000), + Fullscreen = GetOptBool(ini, "EmulationStation", "Fullscreen", true), + FullscreenBorderless = GetOptBool(ini, "EmulationStation", "FullscreenBorderless", true), + ForceFullscreenRes = GetOptBool(ini, "EmulationStation", "ForceFullscreenRes", false), + GameListOnly = GetOptBool(ini, "EmulationStation", "GameListOnly", false), + NoExitMenu = GetOptBool(ini, "EmulationStation", "NoExitMenu", false), + OpenGL2_1 = GetOptBool(ini, "EmulationStation", "OpenGL2_1", false), + VSync = GetOptBool(ini, "EmulationStation", "VSync", true), + DrawFramerate = GetOptBool(ini, "EmulationStation", "DrawFramerate", false), + RandomTheme = GetOptBool(ini, "EmulationStation", "RandomTheme", false), + FocusDelay = GetOptInt(ini, "EmulationStation", "FocusDelay", 2000), + InterfaceMode = GetOptInt(ini, "EmulationStation", "InterfaceMode", 0), + MonitorIndex = GetOptInt(ini, "EmulationStation", "MonitorIndex", 0), + WindowXSize = GetOptInt(ini, "EmulationStation", "WindowXSize", 1280), + WindowYSize = GetOptInt(ini, "EmulationStation", "WindowYSize", 720) + }; + } + + private static List GetAppLauncherEntries(IniFile ini) + { + var entries = new List(); + + // Backward compatible: original unnumbered key + string first = ini.GetValue("RetroBat", "AppLauncher"); + if (!string.IsNullOrWhiteSpace(first)) + entries.Add(first.Trim()); + + // Additional apps: AppLauncher2, AppLauncher3, ... + for (int i = 2; i <= 20; i++) + { + string value = ini.GetValue("RetroBat", "AppLauncher" + i); + if (!string.IsNullOrWhiteSpace(value)) + entries.Add(value.Trim()); + } + + return entries; + } + + private static bool GetOptBool(IniFile ini, string section, string key, bool defaultValue) + { + return GetOptBoolean(IniFile.GetOptionValue(ini, section, key, defaultValue ? "true" : "false")); + } + + private static int GetOptInt(IniFile ini, string section, string key, int defaultValue) + { + string raw = IniFile.GetOptionValue(ini, section, key, defaultValue.ToString()); + return int.TryParse(raw, out int value) ? value : defaultValue; + } + + private static bool GetOptBoolean(string input) + { + if (input == "1" || input == "true" || input == "yes") + return true; + else + return false; + } + } +} diff --git a/RetroBat/RetroBat/SplashVideo.cs b/RetroBat/RetroBat/SplashVideo.cs index 8da220f..83ee32f 100644 --- a/RetroBat/RetroBat/SplashVideo.cs +++ b/RetroBat/RetroBat/SplashVideo.cs @@ -144,7 +144,7 @@ public static void ShowBlackSplash(Screen targetScreen = null) _blackSplashForm.Focus(); _blackSplashForm.Activate(); } - catch { } + catch (Exception ex) { SimpleLogger.Instance.Warning("Failed to focus/activate black splash form: " + ex.Message); } }; _blackSplashForm.Shown += (s, e) => splashDone.Set(); @@ -158,7 +158,7 @@ public static void ShowBlackSplash(Screen targetScreen = null) watchdog.Stop(); _blackSplashForm?.Close(); } - catch { } + catch (Exception ex) { SimpleLogger.Instance.Warning("Black splash watchdog failed to close form: " + ex.Message); } }; watchdog.Start(); diff --git a/RetroBat/RetroBat/StartupFileChecker.cs b/RetroBat/RetroBat/StartupFileChecker.cs new file mode 100644 index 0000000..3753f2b --- /dev/null +++ b/RetroBat/RetroBat/StartupFileChecker.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace RetroBat +{ + internal static class StartupFileChecker + { + public static void EnsureRequiredFilesExist(string appFolder, string esPath) + { + SimpleLogger.Instance.Info("Checking availability of necessary files."); + string templatepathES = Path.Combine(appFolder, "system", "templates", "emulationstation"); + + var esFiles = new HashSet(Directory.EnumerateFiles(esPath).Select(Path.GetFileName), System.StringComparer.OrdinalIgnoreCase); + + if (!esFiles.Contains("about.info")) + { + SimpleLogger.Instance.Warning("Creating file 'about.info'"); + try { File.WriteAllText(Path.Combine(esPath, "about.info"), "RETROBAT"); } + catch { SimpleLogger.Instance.Warning("Impossible to create about.info file."); } + } + + if (!esFiles.Contains("emulationstation.exe")) + { + SimpleLogger.Instance.Error("EmulationStation cannot be found at: " + Path.Combine(esPath, "emulationstation.exe")); + throw new FileNotFoundException("EmulationStation executable not found."); + } + + if (!esFiles.Contains("emulatorlauncher.exe")) + { + SimpleLogger.Instance.Error("EmulatorLauncher cannot be found at: " + Path.Combine(esPath, "emulatorlauncher.exe")); + throw new FileNotFoundException("EmulatorLauncher executable not found."); + } + + if (!esFiles.Contains("batocera-store.exe")) + SimpleLogger.Instance.Warning("Batocera-store executable not found, continuing without it."); + + if (!esFiles.Contains("batocera-systems.exe")) + SimpleLogger.Instance.Warning("Batocera-systems executable not found, continuing without it."); + + if (!esFiles.Contains("es-update.exe")) + SimpleLogger.Instance.Warning("es-update executable not found, continuing without it."); + + if (!esFiles.Contains("es-checkversion.exe")) + SimpleLogger.Instance.Warning("es-checkversion executable not found, continuing without it."); + + if (!esFiles.Contains("emulatorlauncher.common.dll")) + { + SimpleLogger.Instance.Error("emulatorlauncher common DLL does not exist"); + throw new FileNotFoundException("emulatorlauncher common DLL not found."); + } + + if (!File.Exists(Path.Combine(esPath, ".emulationstation", "es_features.cfg"))) + { + SimpleLogger.Instance.Error("es_features cannot be found at: " + Path.Combine(esPath, ".emulationstation", "es_features.cfg")); + throw new FileNotFoundException("es_features not found."); + } + + if (!File.Exists(Path.Combine(esPath, ".emulationstation", "es_systems.cfg"))) + { + SimpleLogger.Instance.Warning("es_systems cannot be found, trying to copy template."); + + try { File.Copy(Path.Combine(templatepathES, "es_systems.cfg"), Path.Combine(esPath, ".emulationstation", "es_systems.cfg"), true); } + catch (System.Exception ex) { SimpleLogger.Instance.Warning("Failed to copy es_systems.cfg template: " + ex.Message); } + + if (!File.Exists(Path.Combine(esPath, ".emulationstation", "es_systems.cfg"))) + { + SimpleLogger.Instance.Error("es_systems cannot be found at: " + Path.Combine(esPath, ".emulationstation", "es_systems.cfg")); + throw new FileNotFoundException("es_systems not found."); + } + } + + if (!File.Exists(Path.Combine(esPath, "emulatorLauncher.cfg"))) + { + SimpleLogger.Instance.Warning("emulatorLauncher.cfg cannot be found, trying to copy template."); + + try { File.Copy(Path.Combine(templatepathES, "emulatorLauncher.cfg"), Path.Combine(esPath, "emulatorLauncher.cfg"), true); } + catch (System.Exception ex) { SimpleLogger.Instance.Warning("Failed to copy emulatorLauncher.cfg template: " + ex.Message); } + + if (!File.Exists(Path.Combine(esPath, "emulatorLauncher.cfg"))) + { + SimpleLogger.Instance.Error("emulatorLauncher.cfg cannot be found at: " + Path.Combine(esPath, "emulatorLauncher.cfg")); + throw new FileNotFoundException("emulatorLauncher.cfg not found."); + } + } + SimpleLogger.Instance.Info("All necessary files exist."); + } + } +} diff --git a/RetroBat/RetroBat/VideoPlayerForm.cs b/RetroBat/RetroBat/VideoPlayerForm.cs index 2c2a93b..c4e7e7a 100644 --- a/RetroBat/RetroBat/VideoPlayerForm.cs +++ b/RetroBat/RetroBat/VideoPlayerForm.cs @@ -31,7 +31,7 @@ public VideoPlayerForm(string videoPath, string path, bool gamepadKill = false, if (File.Exists(_path)) { try { File.Delete(_path); } - catch { } + catch (Exception ex) { SimpleLogger.Instance.Warning("Failed to delete stale emulationstation.ready file: " + ex.Message); } } var screen = targetScreen ?? Screen.PrimaryScreen; diff --git a/RetroBat/RetroBat/packages.config b/RetroBat/RetroBat/packages.config deleted file mode 100644 index 0b6b0a3..0000000 --- a/RetroBat/RetroBat/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file