diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs
index 89b965e..e41526b 100644
--- a/Core/RenderManager.cs
+++ b/Core/RenderManager.cs
@@ -14,6 +14,7 @@ using ADOFAIRenderer.UI.Window;
using ADOFAIRenderer.Util;
using ADOFAIRenderer.Util.Audio;
using ADOFAIRenderer.Util.FFmpeg;
+using ADOFAIRenderer.Util.OS;
using FFmpeg.AutoGen;
using HarmonyLib;
using MelonLoader;
diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs
index 2a0e17d..6666ef3 100644
--- a/UI/Window/RenderConfigWindow.cs
+++ b/UI/Window/RenderConfigWindow.cs
@@ -7,7 +7,7 @@ using ADOFAIRenderer.Core;
using ADOFAIRenderer.Core.Data;
using ADOFAIRenderer.Data;
using ADOFAIRenderer.Localization;
-using ADOFAIRenderer.Util;
+using ADOFAIRenderer.Util.OS;
using MelonLoader;
using O5Kit.Control;
using O5Kit.Core;
diff --git a/Util/GPUUtil.cs b/Util/GPUUtil.cs
index fc174fb..586dcba 100644
--- a/Util/GPUUtil.cs
+++ b/Util/GPUUtil.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using ADOFAIRenderer.Data;
using ADOFAIRenderer.Util.FFmpeg;
+using ADOFAIRenderer.Util.OS;
using MelonLoader;
using UnityEngine;
diff --git a/Util/FolderDialogUtil.cs b/Util/OS/FolderDialogUtil.cs
similarity index 99%
rename from Util/FolderDialogUtil.cs
rename to Util/OS/FolderDialogUtil.cs
index d3b8133..ccae4fc 100644
--- a/Util/FolderDialogUtil.cs
+++ b/Util/OS/FolderDialogUtil.cs
@@ -8,7 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using MelonLoader;
-namespace ADOFAIRenderer.Util;
+namespace ADOFAIRenderer.Util.OS;
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "UnusedMember.Local")]
diff --git a/Util/OS/OSNotification.cs b/Util/OS/OSNotification.cs
new file mode 100644
index 0000000..7632e9a
--- /dev/null
+++ b/Util/OS/OSNotification.cs
@@ -0,0 +1,577 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+using MelonLoader;
+
+namespace ADOFAIRenderer.Util.OS;
+
+///
+/// Sends native desktop notifications on Windows, macOS, and Linux.
+///
+/// - Windows: Uses Win32 Shell_NotifyIcon (system tray balloon) via P/Invoke.
+/// No PowerShell, no UWP, no WinRT dependencies.
+///
+///
+/// - macOS: Uses osascript with AppleScript's display notification.
+///
+///
+/// - Linux: Uses notify-send (libnotify) with xdg-open for folder actions.
+///
+///
+internal static class OSNotification
+{
+ ///
+ /// Sends a native desktop notification.
+ /// If is provided, Windows and Linux will attempt to open
+ /// the folder when the notification is clicked/dismissed.
+ ///
+ /// The notification title.
+ /// The notification body text.
+ /// Optional folder path to open on click (Windows balloon click / Linux action).
+ public static void Show(string title, string message, string? folderPath = null)
+ {
+ try
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ WindowsNotification.Send(title, message, folderPath);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ MacNotification.Send(title, message);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ LinuxNotification.Send(title, message, folderPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] Failed to send notification: {ex.Message}");
+ }
+ }
+
+ #region Windows — Toast (primary) + Shell_NotifyIcon (fallback)
+
+ ///
+ /// Windows notification with two strategies:
+ /// 1. Toast via PowerShell — persists in Action Center.
+ /// 2. Shell_NotifyIcon balloon via P/Invoke — fallback if toast fails.
+ ///
+ private static class WindowsNotification
+ {
+ public static void Send(string title, string message, string? folderPath)
+ {
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ if (!TrySendToast(title, message, folderPath))
+ {
+ SendBalloon(title, message, folderPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] Windows notification failed: {ex.Message}");
+ }
+ })
+ {
+ IsBackground = true,
+ Name = "OSNotification-Win"
+ };
+ thread.Start();
+ }
+
+ #region Toast via PowerShell
+
+ ///
+ /// Attempts to show a Windows Toast notification via PowerShell.
+ /// Returns true if the process launched successfully, false otherwise.
+ ///
+ private static bool TrySendToast(string title, string message, string? folderPath)
+ {
+ try
+ {
+ string xmlTitle = System.Security.SecurityElement.Escape(title) ?? title;
+ string xmlMessage = System.Security.SecurityElement.Escape(message) ?? message;
+
+ string toastXml;
+ string psScript;
+
+ if (!string.IsNullOrEmpty(folderPath))
+ {
+ var escapedFolder = folderPath.Replace("'", "''");
+
+ toastXml =
+ "" +
+ "" +
+ $"{xmlTitle}{xmlMessage}" +
+ "" +
+ "" +
+ "" +
+ "";
+
+ psScript =
+ "$protocolPath = 'HKCU:\\Software\\Classes\\adofai-renderer'\n" +
+ "$null = New-Item -Path $protocolPath -Force\n" +
+ "$null = New-ItemProperty -Path $protocolPath -Name 'URL Protocol' -Value '' -PropertyType String -Force\n" +
+ "Set-Item -Path $protocolPath -Value 'URL:adofai-renderer Protocol' | Out-Null\n" +
+ "$cmdPath = \"$protocolPath\\shell\\open\\command\"\n" +
+ "$null = New-Item -Path $cmdPath -Force\n" +
+ $"Set-Item -Path $cmdPath -Value 'explorer.exe \"{escapedFolder}\"' | Out-Null\n" +
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null\n" +
+ "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data, ContentType = WindowsRuntime] | Out-Null\n" +
+ "$xml = [Windows.Data.Xml.Dom.XmlDocument]::new()\n" +
+ $"$xml.LoadXml('{toastXml}')\n" +
+ "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))";
+ }
+ else
+ {
+ toastXml =
+ "" +
+ $"{xmlTitle}{xmlMessage}" +
+ "";
+
+ psScript =
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null\n" +
+ "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data, ContentType = WindowsRuntime] | Out-Null\n" +
+ "$xml = [Windows.Data.Xml.Dom.XmlDocument]::new()\n" +
+ $"$xml.LoadXml('{toastXml}')\n" +
+ "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))";
+ }
+
+ byte[] bytes = System.Text.Encoding.Unicode.GetBytes(psScript);
+ string base64 = Convert.ToBase64String(bytes);
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "powershell.exe",
+ Arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {base64}",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardError = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+
+ using var process = new Process();
+ process.StartInfo = startInfo;
+ process.Start();
+ process.WaitForExit(5000);
+
+ // If PowerShell exited with error, treat as failure
+ if (process.ExitCode != 0)
+ {
+ string stderr = process.StandardError.ReadToEnd().Trim();
+ if (!string.IsNullOrEmpty(stderr))
+ MelonLogger.Warning($"[OSNotification] Toast failed (using fallback): {stderr}");
+ return false;
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] Toast failed (using fallback): {ex.Message}");
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Balloon via Shell_NotifyIcon
+
+ // ReSharper disable InconsistentNaming
+ private const int NIM_ADD = 0x00;
+ private const int NIM_DELETE = 0x02;
+
+ private const int NIF_MESSAGE = 0x01;
+ private const int NIF_ICON = 0x02;
+ private const int NIF_TIP = 0x04;
+ private const int NIF_INFO = 0x10;
+
+ private const int NIIF_INFO = 0x01;
+
+ private const int WM_USER = 0x0400;
+ private const int WM_TRAYICON = WM_USER + 1;
+ private const int NIN_BALLOONUSERCLICK = 0x0405;
+
+ private const int WS_OVERLAPPED = 0x00000000;
+ private const uint WM_CLOSE = 0x0010;
+ private const int TRAY_ICON_UID = 29471;
+ // ReSharper restore InconsistentNaming
+
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
+ private static extern bool Shell_NotifyIconW(int dwMessage, ref NotifyIconDataW lpData);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ private static extern IntPtr CreateWindowExW(
+ int dwExStyle, string lpClassName, string lpWindowName,
+ int dwStyle, int x, int y, int nWidth, int nHeight,
+ IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool DestroyWindow(IntPtr hWnd);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr LoadIconW(IntPtr hInstance, IntPtr lpIconName);
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool GetMessageW(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr TranslateMessage(ref Msg lpMsg);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr DispatchMessageW(ref Msg lpMsg);
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool PostMessageW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct NotifyIconDataW
+ {
+ public int cbSize;
+ public IntPtr hWnd;
+ public int uID;
+ public int uFlags;
+ public int uCallbackMessage;
+ public IntPtr hIcon;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string szTip;
+ public int dwState;
+ public int dwStateMask;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
+ public string szInfo;
+ public int uTimeoutOrVersion;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
+ public string szInfoTitle;
+ public int dwInfoFlags;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct Msg
+ {
+ public IntPtr hwnd;
+ public uint message;
+ public IntPtr wParam;
+ public IntPtr lParam;
+ public uint time;
+ public int pt_x;
+ public int pt_y;
+ }
+
+ private static void SendBalloon(string title, string message, string? folderPath)
+ {
+ if (title.Length > 63) title = title.Substring(0, 63);
+ if (message.Length > 255) message = message.Substring(0, 255);
+
+ IntPtr hWnd = IntPtr.Zero;
+ var nid = new NotifyIconDataW();
+
+ try
+ {
+ var hwndMessage = new IntPtr(-3);
+ hWnd = CreateWindowExW(
+ 0, "Static", "ADOFAIRendererNotify",
+ WS_OVERLAPPED, 0, 0, 0, 0,
+ hwndMessage, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
+
+ if (hWnd == IntPtr.Zero)
+ {
+ MelonLogger.Warning("[OSNotification] Cannot create message window.");
+ return;
+ }
+
+ IntPtr hIcon = LoadIconW(IntPtr.Zero, (IntPtr)32512);
+
+ nid.cbSize = Marshal.SizeOf(typeof(NotifyIconDataW));
+ nid.hWnd = hWnd;
+ nid.uID = TRAY_ICON_UID;
+ nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_INFO;
+ nid.uCallbackMessage = WM_TRAYICON;
+ nid.hIcon = hIcon;
+ nid.szTip = "ADOFAI Renderer";
+ nid.szInfo = message;
+ nid.szInfoTitle = title;
+ nid.dwInfoFlags = NIIF_INFO;
+ nid.uTimeoutOrVersion = 4;
+
+ Shell_NotifyIconW(NIM_ADD, ref nid);
+
+ if (!string.IsNullOrEmpty(folderPath))
+ {
+ var capturedFolder = folderPath;
+ var closeTimer = new System.Threading.Timer(_ =>
+ {
+ try { PostMessageW(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); }
+ catch { /* ignored */ }
+ }, null, 12000, Timeout.Infinite);
+
+ try
+ {
+ while (GetMessageW(out var msg, IntPtr.Zero, 0, 0))
+ {
+ if (msg.message == WM_CLOSE)
+ break;
+
+ if (msg.message == WM_TRAYICON)
+ {
+ int notifyMsg = (int)(long)msg.lParam;
+ if (notifyMsg == NIN_BALLOONUSERCLICK)
+ {
+ try
+ {
+ var psi = new ProcessStartInfo();
+ psi.FileName = capturedFolder;
+ psi.UseShellExecute = true;
+ Process.Start(psi);
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning(
+ $"[OSNotification] Cannot open folder: {ex.Message}");
+ }
+
+ break;
+ }
+ }
+
+ TranslateMessage(ref msg);
+ DispatchMessageW(ref msg);
+ }
+ }
+ finally
+ {
+ closeTimer.Dispose();
+ }
+ }
+ else
+ {
+ Thread.Sleep(500);
+ }
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] Balloon notification failed: {ex.Message}");
+ }
+ finally
+ {
+ if (hWnd != IntPtr.Zero)
+ {
+ nid.uFlags = 0;
+ Shell_NotifyIconW(NIM_DELETE, ref nid);
+ DestroyWindow(hWnd);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ #endregion
+
+ #region macOS — osascript
+
+ ///
+ /// macOS notification using osascript -e 'display notification ...'.
+ ///
+ private static class MacNotification
+ {
+ public static void Send(string title, string message)
+ {
+ // AppleScript string escaping: backslash and double-quote
+ string escapedTitle = EscapeAppleScript(title);
+ string escapedMessage = EscapeAppleScript(message);
+
+ // Use ProcessStartInfo with ArgumentList to avoid shell-level escaping issues.
+ // Each argument is passed directly to the process without shell interpretation.
+ var script =
+ $"display notification \"{escapedMessage}\" with title \"{escapedTitle}\"";
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "osascript",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ startInfo.ArgumentList.Add("-e");
+ startInfo.ArgumentList.Add(script);
+
+ RunProcess(startInfo);
+ }
+
+ private static string EscapeAppleScript(string value)
+ {
+ // In AppleScript string literals, escape backslash and double-quote
+ return value
+ .Replace("\\", "\\\\")
+ .Replace("\"", "\\\"");
+ }
+ }
+
+ #endregion
+
+ #region Linux — notify-send / gdbus
+
+ ///
+ /// Linux notification using notify-send (libnotify CLI).
+ /// Falls back to gdbus (D-Bus direct call) if notify-send is unavailable.
+ ///
+ private static class LinuxNotification
+ {
+ public static void Send(string title, string message, string? folderPath)
+ {
+ if (!string.IsNullOrEmpty(folderPath))
+ {
+ ShowWithAction(title, message, folderPath);
+ }
+ else
+ {
+ ShowSimple(title, message);
+ }
+ }
+
+ private static void ShowSimple(string title, string message)
+ {
+ // Try notify-send first
+ if (TryNotifySend(title, message))
+ return;
+
+ // Fallback to gdbus
+ TryGdbus(title, message);
+ }
+
+ private static void ShowWithAction(string title, string message, string folderPath)
+ {
+ // Run on a background thread because notify-send with --action blocks
+ // until the notification is dismissed or the action is clicked.
+ System.Threading.Tasks.Task.Run(() =>
+ {
+ try
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "notify-send",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ startInfo.ArgumentList.Add("-a");
+ startInfo.ArgumentList.Add("ADOFAI Renderer");
+ startInfo.ArgumentList.Add("--action=open_folder=Open Render Folder");
+ startInfo.ArgumentList.Add(title);
+ startInfo.ArgumentList.Add(message);
+
+ using var process = new Process();
+ process.StartInfo = startInfo;
+ process.Start();
+ string output = process.StandardOutput.ReadToEnd().Trim();
+ process.WaitForExit();
+
+ if (output == "open_folder")
+ {
+ var openInfo = new ProcessStartInfo
+ {
+ FileName = "xdg-open",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ openInfo.ArgumentList.Add(folderPath);
+
+ RunProcess(openInfo);
+ }
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] Linux notification processing failed: {ex.Message}");
+
+ // Fallback: show without action
+ ShowSimple(title, message);
+ }
+ });
+ }
+
+ private static bool TryNotifySend(string title, string message)
+ {
+ try
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "notify-send",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ startInfo.ArgumentList.Add("-a");
+ startInfo.ArgumentList.Add("ADOFAI Renderer");
+ startInfo.ArgumentList.Add(title);
+ startInfo.ArgumentList.Add(message);
+
+ RunProcess(startInfo);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static void TryGdbus(string title, string message)
+ {
+ try
+ {
+ // Direct D-Bus call to org.freedesktop.Notifications
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "gdbus",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ startInfo.ArgumentList.Add("call");
+ startInfo.ArgumentList.Add("--session");
+ startInfo.ArgumentList.Add("--dest=org.freedesktop.Notifications");
+ startInfo.ArgumentList.Add("--object-path=/org/freedesktop/Notifications");
+ startInfo.ArgumentList.Add("--method=org.freedesktop.Notifications.Notify");
+ startInfo.ArgumentList.Add("ADOFAI Renderer"); // app_name
+ startInfo.ArgumentList.Add("0"); // replaces_id
+ startInfo.ArgumentList.Add(""); // app_icon
+ startInfo.ArgumentList.Add(title); // summary
+ startInfo.ArgumentList.Add(message); // body
+ startInfo.ArgumentList.Add("[]"); // actions
+ startInfo.ArgumentList.Add("{}"); // hints
+ startInfo.ArgumentList.Add("-1"); // expire_timeout
+
+ RunProcess(startInfo);
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] gdbus notification failed: {ex.Message}");
+ }
+ }
+ }
+
+ #endregion
+
+ #region Shared utility
+
+ ///
+ /// Starts a process fire-and-forget. Does not redirect output or wait for exit.
+ ///
+ private static void RunProcess(ProcessStartInfo startInfo)
+ {
+ using var process = new Process();
+ process.StartInfo = startInfo;
+ process.Start();
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Util/OS/RegistryUtil.cs b/Util/OS/RegistryUtil.cs
new file mode 100644
index 0000000..91ecfc6
--- /dev/null
+++ b/Util/OS/RegistryUtil.cs
@@ -0,0 +1,129 @@
+using System;
+
+namespace ADOFAIRenderer.Util.OS;
+
+internal static class RegistryUtil
+{
+ private static readonly Type? RegistryKeyType;
+ private static readonly Type? RegistryHiveType;
+ private static readonly Type? RegistryViewType;
+ private static readonly System.Reflection.MethodInfo? OpenBaseKeyMethod;
+ private static readonly System.Reflection.MethodInfo? OpenSubKeyMethod;
+ private static readonly System.Reflection.MethodInfo? GetSubKeyNamesMethod;
+ private static readonly System.Reflection.MethodInfo? GetValueMethod;
+
+ static RegistryUtil()
+ {
+ try
+ {
+ RegistryKeyType = Type.GetType("Microsoft.Win32.RegistryKey");
+ RegistryHiveType = Type.GetType("Microsoft.Win32.RegistryHive");
+ RegistryViewType = Type.GetType("Microsoft.Win32.RegistryView");
+
+ if (RegistryKeyType == null || RegistryHiveType == null || RegistryViewType == null)
+ {
+ var mscorlib = typeof(object).Assembly;
+ RegistryKeyType = mscorlib.GetType("Microsoft.Win32.RegistryKey");
+ RegistryHiveType = mscorlib.GetType("Microsoft.Win32.RegistryHive");
+ RegistryViewType = mscorlib.GetType("Microsoft.Win32.RegistryView");
+ }
+
+ if (RegistryKeyType != null)
+ {
+ OpenBaseKeyMethod = RegistryKeyType.GetMethod("OpenBaseKey", new[] { RegistryHiveType!, RegistryViewType! });
+ OpenSubKeyMethod = RegistryKeyType.GetMethod("OpenSubKey", new[] { typeof(string) });
+ GetSubKeyNamesMethod = RegistryKeyType.GetMethod("GetSubKeyNames");
+ GetValueMethod = RegistryKeyType.GetMethod("GetValue", new[] { typeof(string) });
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ private static void CloseKey(object? key)
+ {
+ if (key == null || RegistryKeyType == null) return;
+ try
+ {
+ if (key is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ else
+ {
+ var closeMethod = RegistryKeyType.GetMethod("Close");
+ closeMethod?.Invoke(key, null);
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ // ReSharper disable once InconsistentNaming
+ public static string[]? GetHKLMSubKeyNames(string parentKeyPath)
+ {
+ if (RegistryKeyType == null || OpenBaseKeyMethod == null || OpenSubKeyMethod == null || GetSubKeyNamesMethod == null)
+ return null;
+
+ try
+ {
+ object hiveLocalMachine = Enum.ToObject(RegistryHiveType!, 0x80000002);
+ object viewRegistry64 = Enum.ToObject(RegistryViewType!, 256);
+
+ object? baseKey = OpenBaseKeyMethod.Invoke(null, new[] { hiveLocalMachine, viewRegistry64 });
+ if (baseKey == null) return null;
+
+ object? key = OpenSubKeyMethod.Invoke(baseKey, new object[] { parentKeyPath });
+ if (key == null)
+ {
+ CloseKey(baseKey);
+ return null;
+ }
+
+ string[]? subkeyNames = GetSubKeyNamesMethod.Invoke(key, null) as string[];
+ CloseKey(key);
+ CloseKey(baseKey);
+ return subkeyNames;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ // ReSharper disable once InconsistentNaming
+ public static string? GetHKLMValue(string keyPath, string valueName)
+ {
+ if (RegistryKeyType == null || OpenBaseKeyMethod == null || OpenSubKeyMethod == null || GetValueMethod == null)
+ return null;
+
+ try
+ {
+ object hiveLocalMachine = Enum.ToObject(RegistryHiveType!, 0x80000002);
+ object viewRegistry64 = Enum.ToObject(RegistryViewType!, 256);
+
+ object? baseKey = OpenBaseKeyMethod.Invoke(null, new[] { hiveLocalMachine, viewRegistry64 });
+ if (baseKey == null) return null;
+
+ object? key = OpenSubKeyMethod.Invoke(baseKey, new object[] { keyPath });
+ if (key == null)
+ {
+ CloseKey(baseKey);
+ return null;
+ }
+
+ string? value = GetValueMethod.Invoke(key, new object[] { valueName }) as string;
+ CloseKey(key);
+ CloseKey(baseKey);
+ return value;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Util/OSNotification.cs b/Util/OSNotification.cs
deleted file mode 100644
index 346ad37..0000000
--- a/Util/OSNotification.cs
+++ /dev/null
@@ -1,578 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Threading;
-using MelonLoader;
-
-namespace ADOFAIRenderer.Util
-{
- ///
- /// Sends native desktop notifications on Windows, macOS, and Linux.
- ///
- /// - Windows: Uses Win32 Shell_NotifyIcon (system tray balloon) via P/Invoke.
- /// No PowerShell, no UWP, no WinRT dependencies.
- ///
- ///
- /// - macOS: Uses osascript with AppleScript's display notification.
- ///
- ///
- /// - Linux: Uses notify-send (libnotify) with xdg-open for folder actions.
- ///
- ///
- internal static class OSNotification
- {
- ///
- /// Sends a native desktop notification.
- /// If is provided, Windows and Linux will attempt to open
- /// the folder when the notification is clicked/dismissed.
- ///
- /// The notification title.
- /// The notification body text.
- /// Optional folder path to open on click (Windows balloon click / Linux action).
- public static void Show(string title, string message, string? folderPath = null)
- {
- try
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- WindowsNotification.Send(title, message, folderPath);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- MacNotification.Send(title, message);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- LinuxNotification.Send(title, message, folderPath);
- }
- }
- catch (Exception ex)
- {
- MelonLogger.Warning($"[OSNotification] Failed to send notification: {ex.Message}");
- }
- }
-
- #region Windows — Toast (primary) + Shell_NotifyIcon (fallback)
-
- ///
- /// Windows notification with two strategies:
- /// 1. Toast via PowerShell — persists in Action Center.
- /// 2. Shell_NotifyIcon balloon via P/Invoke — fallback if toast fails.
- ///
- private static class WindowsNotification
- {
- public static void Send(string title, string message, string? folderPath)
- {
- var thread = new Thread(() =>
- {
- try
- {
- if (!TrySendToast(title, message, folderPath))
- {
- SendBalloon(title, message, folderPath);
- }
- }
- catch (Exception ex)
- {
- MelonLogger.Warning($"[OSNotification] Windows notification failed: {ex.Message}");
- }
- })
- {
- IsBackground = true,
- Name = "OSNotification-Win"
- };
- thread.Start();
- }
-
- #region Toast via PowerShell
-
- ///
- /// Attempts to show a Windows Toast notification via PowerShell.
- /// Returns true if the process launched successfully, false otherwise.
- ///
- private static bool TrySendToast(string title, string message, string? folderPath)
- {
- try
- {
- string xmlTitle = System.Security.SecurityElement.Escape(title) ?? title;
- string xmlMessage = System.Security.SecurityElement.Escape(message) ?? message;
-
- string toastXml;
- string psScript;
-
- if (!string.IsNullOrEmpty(folderPath))
- {
- var escapedFolder = folderPath.Replace("'", "''");
-
- toastXml =
- "" +
- "" +
- $"{xmlTitle}{xmlMessage}" +
- "" +
- "" +
- "" +
- "";
-
- psScript =
- "$protocolPath = 'HKCU:\\Software\\Classes\\adofai-renderer'\n" +
- "$null = New-Item -Path $protocolPath -Force\n" +
- "$null = New-ItemProperty -Path $protocolPath -Name 'URL Protocol' -Value '' -PropertyType String -Force\n" +
- "Set-Item -Path $protocolPath -Value 'URL:adofai-renderer Protocol' | Out-Null\n" +
- "$cmdPath = \"$protocolPath\\shell\\open\\command\"\n" +
- "$null = New-Item -Path $cmdPath -Force\n" +
- $"Set-Item -Path $cmdPath -Value 'explorer.exe \"{escapedFolder}\"' | Out-Null\n" +
- "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null\n" +
- "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data, ContentType = WindowsRuntime] | Out-Null\n" +
- "$xml = [Windows.Data.Xml.Dom.XmlDocument]::new()\n" +
- $"$xml.LoadXml('{toastXml}')\n" +
- "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))";
- }
- else
- {
- toastXml =
- "" +
- $"{xmlTitle}{xmlMessage}" +
- "";
-
- psScript =
- "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null\n" +
- "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data, ContentType = WindowsRuntime] | Out-Null\n" +
- "$xml = [Windows.Data.Xml.Dom.XmlDocument]::new()\n" +
- $"$xml.LoadXml('{toastXml}')\n" +
- "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))";
- }
-
- byte[] bytes = System.Text.Encoding.Unicode.GetBytes(psScript);
- string base64 = Convert.ToBase64String(bytes);
-
- var startInfo = new ProcessStartInfo
- {
- FileName = "powershell.exe",
- Arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {base64}",
- CreateNoWindow = true,
- UseShellExecute = false,
- RedirectStandardError = true,
- WindowStyle = ProcessWindowStyle.Hidden
- };
-
- using var process = new Process();
- process.StartInfo = startInfo;
- process.Start();
- process.WaitForExit(5000);
-
- // If PowerShell exited with error, treat as failure
- if (process.ExitCode != 0)
- {
- string stderr = process.StandardError.ReadToEnd().Trim();
- if (!string.IsNullOrEmpty(stderr))
- MelonLogger.Warning($"[OSNotification] Toast failed (using fallback): {stderr}");
- return false;
- }
-
- return true;
- }
- catch (Exception ex)
- {
- MelonLogger.Warning($"[OSNotification] Toast failed (using fallback): {ex.Message}");
- return false;
- }
- }
-
- #endregion
-
- #region Balloon via Shell_NotifyIcon
-
- // ReSharper disable InconsistentNaming
- private const int NIM_ADD = 0x00;
- private const int NIM_DELETE = 0x02;
-
- private const int NIF_MESSAGE = 0x01;
- private const int NIF_ICON = 0x02;
- private const int NIF_TIP = 0x04;
- private const int NIF_INFO = 0x10;
-
- private const int NIIF_INFO = 0x01;
-
- private const int WM_USER = 0x0400;
- private const int WM_TRAYICON = WM_USER + 1;
- private const int NIN_BALLOONUSERCLICK = 0x0405;
-
- private const int WS_OVERLAPPED = 0x00000000;
- private const uint WM_CLOSE = 0x0010;
- private const int TRAY_ICON_UID = 29471;
- // ReSharper restore InconsistentNaming
-
- [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
- private static extern bool Shell_NotifyIconW(int dwMessage, ref NotifyIconDataW lpData);
-
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
- private static extern IntPtr CreateWindowExW(
- int dwExStyle, string lpClassName, string lpWindowName,
- int dwStyle, int x, int y, int nWidth, int nHeight,
- IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
-
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- private static extern bool DestroyWindow(IntPtr hWnd);
-
- [DllImport("user32.dll")]
- private static extern IntPtr LoadIconW(IntPtr hInstance, IntPtr lpIconName);
-
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- private static extern bool GetMessageW(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
-
- [DllImport("user32.dll")]
- private static extern IntPtr TranslateMessage(ref Msg lpMsg);
-
- [DllImport("user32.dll")]
- private static extern IntPtr DispatchMessageW(ref Msg lpMsg);
-
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- private static extern bool PostMessageW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
-
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
- private struct NotifyIconDataW
- {
- public int cbSize;
- public IntPtr hWnd;
- public int uID;
- public int uFlags;
- public int uCallbackMessage;
- public IntPtr hIcon;
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
- public string szTip;
- public int dwState;
- public int dwStateMask;
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
- public string szInfo;
- public int uTimeoutOrVersion;
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
- public string szInfoTitle;
- public int dwInfoFlags;
- }
-
- [StructLayout(LayoutKind.Sequential)]
- private struct Msg
- {
- public IntPtr hwnd;
- public uint message;
- public IntPtr wParam;
- public IntPtr lParam;
- public uint time;
- public int pt_x;
- public int pt_y;
- }
-
- private static void SendBalloon(string title, string message, string? folderPath)
- {
- if (title.Length > 63) title = title.Substring(0, 63);
- if (message.Length > 255) message = message.Substring(0, 255);
-
- IntPtr hWnd = IntPtr.Zero;
- var nid = new NotifyIconDataW();
-
- try
- {
- var hwndMessage = new IntPtr(-3);
- hWnd = CreateWindowExW(
- 0, "Static", "ADOFAIRendererNotify",
- WS_OVERLAPPED, 0, 0, 0, 0,
- hwndMessage, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
-
- if (hWnd == IntPtr.Zero)
- {
- MelonLogger.Warning("[OSNotification] Cannot create message window.");
- return;
- }
-
- IntPtr hIcon = LoadIconW(IntPtr.Zero, (IntPtr)32512);
-
- nid.cbSize = Marshal.SizeOf(typeof(NotifyIconDataW));
- nid.hWnd = hWnd;
- nid.uID = TRAY_ICON_UID;
- nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_INFO;
- nid.uCallbackMessage = WM_TRAYICON;
- nid.hIcon = hIcon;
- nid.szTip = "ADOFAI Renderer";
- nid.szInfo = message;
- nid.szInfoTitle = title;
- nid.dwInfoFlags = NIIF_INFO;
- nid.uTimeoutOrVersion = 4;
-
- Shell_NotifyIconW(NIM_ADD, ref nid);
-
- if (!string.IsNullOrEmpty(folderPath))
- {
- var capturedFolder = folderPath;
- var closeTimer = new System.Threading.Timer(_ =>
- {
- try { PostMessageW(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); }
- catch { /* ignored */ }
- }, null, 12000, Timeout.Infinite);
-
- try
- {
- while (GetMessageW(out var msg, IntPtr.Zero, 0, 0))
- {
- if (msg.message == WM_CLOSE)
- break;
-
- if (msg.message == WM_TRAYICON)
- {
- int notifyMsg = (int)(long)msg.lParam;
- if (notifyMsg == NIN_BALLOONUSERCLICK)
- {
- try
- {
- var psi = new ProcessStartInfo();
- psi.FileName = capturedFolder;
- psi.UseShellExecute = true;
- Process.Start(psi);
- }
- catch (Exception ex)
- {
- MelonLogger.Warning(
- $"[OSNotification] Cannot open folder: {ex.Message}");
- }
-
- break;
- }
- }
-
- TranslateMessage(ref msg);
- DispatchMessageW(ref msg);
- }
- }
- finally
- {
- closeTimer.Dispose();
- }
- }
- else
- {
- Thread.Sleep(500);
- }
- }
- catch (Exception ex)
- {
- MelonLogger.Warning($"[OSNotification] Balloon notification failed: {ex.Message}");
- }
- finally
- {
- if (hWnd != IntPtr.Zero)
- {
- nid.uFlags = 0;
- Shell_NotifyIconW(NIM_DELETE, ref nid);
- DestroyWindow(hWnd);
- }
- }
- }
-
- #endregion
- }
-
- #endregion
-
- #region macOS — osascript
-
- ///
- /// macOS notification using osascript -e 'display notification ...'.
- ///
- private static class MacNotification
- {
- public static void Send(string title, string message)
- {
- // AppleScript string escaping: backslash and double-quote
- string escapedTitle = EscapeAppleScript(title);
- string escapedMessage = EscapeAppleScript(message);
-
- // Use ProcessStartInfo with ArgumentList to avoid shell-level escaping issues.
- // Each argument is passed directly to the process without shell interpretation.
- var script =
- $"display notification \"{escapedMessage}\" with title \"{escapedTitle}\"";
-
- var startInfo = new ProcessStartInfo
- {
- FileName = "osascript",
- CreateNoWindow = true,
- UseShellExecute = false,
- WindowStyle = ProcessWindowStyle.Hidden
- };
- startInfo.ArgumentList.Add("-e");
- startInfo.ArgumentList.Add(script);
-
- RunProcess(startInfo);
- }
-
- private static string EscapeAppleScript(string value)
- {
- // In AppleScript string literals, escape backslash and double-quote
- return value
- .Replace("\\", "\\\\")
- .Replace("\"", "\\\"");
- }
- }
-
- #endregion
-
- #region Linux — notify-send / gdbus
-
- ///
- /// Linux notification using notify-send (libnotify CLI).
- /// Falls back to gdbus (D-Bus direct call) if notify-send is unavailable.
- ///
- private static class LinuxNotification
- {
- public static void Send(string title, string message, string? folderPath)
- {
- if (!string.IsNullOrEmpty(folderPath))
- {
- ShowWithAction(title, message, folderPath);
- }
- else
- {
- ShowSimple(title, message);
- }
- }
-
- private static void ShowSimple(string title, string message)
- {
- // Try notify-send first
- if (TryNotifySend(title, message))
- return;
-
- // Fallback to gdbus
- TryGdbus(title, message);
- }
-
- private static void ShowWithAction(string title, string message, string folderPath)
- {
- // Run on a background thread because notify-send with --action blocks
- // until the notification is dismissed or the action is clicked.
- System.Threading.Tasks.Task.Run(() =>
- {
- try
- {
- var startInfo = new ProcessStartInfo
- {
- FileName = "notify-send",
- CreateNoWindow = true,
- UseShellExecute = false,
- RedirectStandardOutput = true,
- WindowStyle = ProcessWindowStyle.Hidden
- };
- startInfo.ArgumentList.Add("-a");
- startInfo.ArgumentList.Add("ADOFAI Renderer");
- startInfo.ArgumentList.Add("--action=open_folder=Open Render Folder");
- startInfo.ArgumentList.Add(title);
- startInfo.ArgumentList.Add(message);
-
- using var process = new Process();
- process.StartInfo = startInfo;
- process.Start();
- string output = process.StandardOutput.ReadToEnd().Trim();
- process.WaitForExit();
-
- if (output == "open_folder")
- {
- var openInfo = new ProcessStartInfo
- {
- FileName = "xdg-open",
- CreateNoWindow = true,
- UseShellExecute = false,
- WindowStyle = ProcessWindowStyle.Hidden
- };
- openInfo.ArgumentList.Add(folderPath);
-
- RunProcess(openInfo);
- }
- }
- catch (Exception ex)
- {
- MelonLogger.Warning($"[OSNotification] Linux notification processing failed: {ex.Message}");
-
- // Fallback: show without action
- ShowSimple(title, message);
- }
- });
- }
-
- private static bool TryNotifySend(string title, string message)
- {
- try
- {
- var startInfo = new ProcessStartInfo
- {
- FileName = "notify-send",
- CreateNoWindow = true,
- UseShellExecute = false,
- WindowStyle = ProcessWindowStyle.Hidden
- };
- startInfo.ArgumentList.Add("-a");
- startInfo.ArgumentList.Add("ADOFAI Renderer");
- startInfo.ArgumentList.Add(title);
- startInfo.ArgumentList.Add(message);
-
- RunProcess(startInfo);
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- private static void TryGdbus(string title, string message)
- {
- try
- {
- // Direct D-Bus call to org.freedesktop.Notifications
- var startInfo = new ProcessStartInfo
- {
- FileName = "gdbus",
- CreateNoWindow = true,
- UseShellExecute = false,
- WindowStyle = ProcessWindowStyle.Hidden
- };
- startInfo.ArgumentList.Add("call");
- startInfo.ArgumentList.Add("--session");
- startInfo.ArgumentList.Add("--dest=org.freedesktop.Notifications");
- startInfo.ArgumentList.Add("--object-path=/org/freedesktop/Notifications");
- startInfo.ArgumentList.Add("--method=org.freedesktop.Notifications.Notify");
- startInfo.ArgumentList.Add("ADOFAI Renderer"); // app_name
- startInfo.ArgumentList.Add("0"); // replaces_id
- startInfo.ArgumentList.Add(""); // app_icon
- startInfo.ArgumentList.Add(title); // summary
- startInfo.ArgumentList.Add(message); // body
- startInfo.ArgumentList.Add("[]"); // actions
- startInfo.ArgumentList.Add("{}"); // hints
- startInfo.ArgumentList.Add("-1"); // expire_timeout
-
- RunProcess(startInfo);
- }
- catch (Exception ex)
- {
- MelonLogger.Warning($"[OSNotification] gdbus notification failed: {ex.Message}");
- }
- }
- }
-
- #endregion
-
- #region Shared utility
-
- ///
- /// Starts a process fire-and-forget. Does not redirect output or wait for exit.
- ///
- private static void RunProcess(ProcessStartInfo startInfo)
- {
- using var process = new Process();
- process.StartInfo = startInfo;
- process.Start();
- }
-
- #endregion
- }
-}
diff --git a/Util/RegistryUtil.cs b/Util/RegistryUtil.cs
deleted file mode 100644
index 1b0f201..0000000
--- a/Util/RegistryUtil.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System;
-
-namespace ADOFAIRenderer.Util
-{
- internal static class RegistryUtil
- {
- private static readonly Type? RegistryKeyType;
- private static readonly Type? RegistryHiveType;
- private static readonly Type? RegistryViewType;
- private static readonly System.Reflection.MethodInfo? OpenBaseKeyMethod;
- private static readonly System.Reflection.MethodInfo? OpenSubKeyMethod;
- private static readonly System.Reflection.MethodInfo? GetSubKeyNamesMethod;
- private static readonly System.Reflection.MethodInfo? GetValueMethod;
-
- static RegistryUtil()
- {
- try
- {
- RegistryKeyType = Type.GetType("Microsoft.Win32.RegistryKey");
- RegistryHiveType = Type.GetType("Microsoft.Win32.RegistryHive");
- RegistryViewType = Type.GetType("Microsoft.Win32.RegistryView");
-
- if (RegistryKeyType == null || RegistryHiveType == null || RegistryViewType == null)
- {
- var mscorlib = typeof(object).Assembly;
- RegistryKeyType = mscorlib.GetType("Microsoft.Win32.RegistryKey");
- RegistryHiveType = mscorlib.GetType("Microsoft.Win32.RegistryHive");
- RegistryViewType = mscorlib.GetType("Microsoft.Win32.RegistryView");
- }
-
- if (RegistryKeyType != null)
- {
- OpenBaseKeyMethod = RegistryKeyType.GetMethod("OpenBaseKey", new[] { RegistryHiveType!, RegistryViewType! });
- OpenSubKeyMethod = RegistryKeyType.GetMethod("OpenSubKey", new[] { typeof(string) });
- GetSubKeyNamesMethod = RegistryKeyType.GetMethod("GetSubKeyNames");
- GetValueMethod = RegistryKeyType.GetMethod("GetValue", new[] { typeof(string) });
- }
- }
- catch
- {
- // ignored
- }
- }
-
- private static void CloseKey(object? key)
- {
- if (key == null || RegistryKeyType == null) return;
- try
- {
- if (key is IDisposable disposable)
- {
- disposable.Dispose();
- }
- else
- {
- var closeMethod = RegistryKeyType.GetMethod("Close");
- closeMethod?.Invoke(key, null);
- }
- }
- catch
- {
- // ignored
- }
- }
-
- // ReSharper disable once InconsistentNaming
- public static string[]? GetHKLMSubKeyNames(string parentKeyPath)
- {
- if (RegistryKeyType == null || OpenBaseKeyMethod == null || OpenSubKeyMethod == null || GetSubKeyNamesMethod == null)
- return null;
-
- try
- {
- object hiveLocalMachine = Enum.ToObject(RegistryHiveType!, 0x80000002);
- object viewRegistry64 = Enum.ToObject(RegistryViewType!, 256);
-
- object? baseKey = OpenBaseKeyMethod.Invoke(null, new[] { hiveLocalMachine, viewRegistry64 });
- if (baseKey == null) return null;
-
- object? key = OpenSubKeyMethod.Invoke(baseKey, new object[] { parentKeyPath });
- if (key == null)
- {
- CloseKey(baseKey);
- return null;
- }
-
- string[]? subkeyNames = GetSubKeyNamesMethod.Invoke(key, null) as string[];
- CloseKey(key);
- CloseKey(baseKey);
- return subkeyNames;
- }
- catch
- {
- return null;
- }
- }
-
- // ReSharper disable once InconsistentNaming
- public static string? GetHKLMValue(string keyPath, string valueName)
- {
- if (RegistryKeyType == null || OpenBaseKeyMethod == null || OpenSubKeyMethod == null || GetValueMethod == null)
- return null;
-
- try
- {
- object hiveLocalMachine = Enum.ToObject(RegistryHiveType!, 0x80000002);
- object viewRegistry64 = Enum.ToObject(RegistryViewType!, 256);
-
- object? baseKey = OpenBaseKeyMethod.Invoke(null, new[] { hiveLocalMachine, viewRegistry64 });
- if (baseKey == null) return null;
-
- object? key = OpenSubKeyMethod.Invoke(baseKey, new object[] { keyPath });
- if (key == null)
- {
- CloseKey(baseKey);
- return null;
- }
-
- string? value = GetValueMethod.Invoke(key, new object[] { valueName }) as string;
- CloseKey(key);
- CloseKey(baseKey);
- return value;
- }
- catch
- {
- return null;
- }
- }
- }
-}