using System; using System.IO; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using LLib.ImGui; using Microsoft.Extensions.Logging; namespace LLib.Notifications; public sealed class OutOfGameNotifier : IDisposable { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct NOTIFYICONDATA { public uint cbSize; public nint hWnd; public uint uID; public uint uFlags; public uint uCallbackMessage; public nint hIcon; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string szTip; public uint dwState; public uint dwStateMask; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szInfo; public uint uTimeoutOrVersion; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string szInfoTitle; public uint dwInfoFlags; public Guid guidItem; public nint hBalloonIcon; } private static class NativeMethods { [DllImport("user32.dll")] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] public static extern nint GetForegroundWindow(); [DllImport("user32.dll")] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool MessageBeep(uint uType); [DllImport("winmm.dll", CharSet = CharSet.Unicode, EntryPoint = "PlaySoundW")] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool PlaySound(string? pszSound, nint hmod, uint fdwSound); [DllImport("user32.dll", CharSet = CharSet.Auto)] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] public static extern nint SendMessage(nint hWnd, uint msg, nint wParam, nint lParam); [DllImport("user32.dll", EntryPoint = "GetClassLongPtrW")] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] public static extern nint GetClassLongPtr(nint hWnd, int nIndex); [DllImport("user32.dll", EntryPoint = "LoadIconW")] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] public static extern nint LoadIcon(nint hInstance, nint lpIconName); [DllImport("shell32.dll", CharSet = CharSet.Unicode)] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool Shell_NotifyIcon(uint dwMessage, ref NOTIFYICONDATA lpData); } private const uint NIM_ADD = 0u; private const uint NIM_MODIFY = 1u; private const uint NIM_DELETE = 2u; private const uint NIM_SETVERSION = 4u; private const uint NIF_ICON = 2u; private const uint NIF_TIP = 4u; private const uint NIF_INFO = 16u; private const uint NIIF_INFO = 1u; private const uint NOTIFYICON_VERSION_4 = 4u; private const uint MB_ICONASTERISK = 64u; private const uint SND_ASYNC = 1u; private const uint SND_NODEFAULT = 2u; private const uint SND_FILENAME = 131072u; private const uint WM_GETICON = 127u; private const nint ICON_SMALL = 0; private const nint ICON_BIG = 1; private const int GCLP_HICON = -14; private const int GCLP_HICONSM = -34; private const nint IDI_APPLICATION = 32512; private const uint TrayIconId = 176u; private readonly ILogger _logger; private bool _trayIconRegistered; private nint _trayHWnd; private bool _disposed; private unsafe static nint GameWindowHandle { get { Device* ptr = Device.Instance(); if (ptr == null) { return 0; } return (nint)ptr->hWnd; } } public bool IsGameForeground { get { nint gameWindowHandle = GameWindowHandle; if (gameWindowHandle != 0) { return NativeMethods.GetForegroundWindow() == gameWindowHandle; } return false; } } public OutOfGameNotifier(ILogger logger) { _logger = logger; } public void FlashTaskbar() { WindowFlash.Flash(GameWindowHandle, 0u); } public void PlayAlertSound(string? wavPath) { if (!string.IsNullOrEmpty(wavPath) && File.Exists(wavPath)) { if (NativeMethods.PlaySound(wavPath, 0, 131075u)) { return; } _logger.LogTrace("PlaySound failed for {Path}, falling back to beep", wavPath); } if (!NativeMethods.MessageBeep(64u)) { _logger.LogTrace("MessageBeep returned false"); } } public void ShowTrayNotification(string title, string message) { nint gameWindowHandle = GameWindowHandle; if (gameWindowHandle == 0) { _logger.LogWarning("Cannot show tray notification - game window handle not available"); return; } try { if (!_trayIconRegistered) { nint num = GetGameIcon(gameWindowHandle); if (num == 0) { num = NativeMethods.LoadIcon(0, 32512); } NOTIFYICONDATA lpData = new NOTIFYICONDATA { cbSize = (uint)Marshal.SizeOf(), hWnd = gameWindowHandle, uID = 176u, uFlags = 6u, hIcon = num, szTip = Truncate(title, 127) }; if (!NativeMethods.Shell_NotifyIcon(0u, ref lpData)) { _logger.LogWarning("Shell_NotifyIcon(NIM_ADD) failed"); return; } NOTIFYICONDATA lpData2 = new NOTIFYICONDATA { cbSize = (uint)Marshal.SizeOf(), hWnd = gameWindowHandle, uID = 176u, uTimeoutOrVersion = 4u }; NativeMethods.Shell_NotifyIcon(4u, ref lpData2); _trayHWnd = gameWindowHandle; _trayIconRegistered = true; } NOTIFYICONDATA lpData3 = new NOTIFYICONDATA { cbSize = (uint)Marshal.SizeOf(), hWnd = _trayHWnd, uID = 176u, uFlags = 16u, szInfoTitle = Truncate(title, 63), szInfo = Truncate(message, 255), dwInfoFlags = 1u }; if (!NativeMethods.Shell_NotifyIcon(1u, ref lpData3)) { _logger.LogWarning("Shell_NotifyIcon(NIM_MODIFY) failed"); } } catch (Exception exception) { _logger.LogError(exception, "Failed to show tray notification"); } } private static nint GetGameIcon(nint hWnd) { nint num = NativeMethods.SendMessage(hWnd, 127u, 1, 0); if (num == 0) { num = NativeMethods.SendMessage(hWnd, 127u, 0, 0); } if (num == 0) { num = NativeMethods.GetClassLongPtr(hWnd, -14); } if (num == 0) { num = NativeMethods.GetClassLongPtr(hWnd, -34); } return num; } private static string Truncate(string value, int maxChars) { if (value.Length > maxChars) { return value.Substring(0, maxChars); } return value; } public void Dispose() { if (!_disposed) { _disposed = true; if (_trayIconRegistered) { NOTIFYICONDATA lpData = new NOTIFYICONDATA { cbSize = (uint)Marshal.SizeOf(), hWnd = _trayHWnd, uID = 176u }; NativeMethods.Shell_NotifyIcon(2u, ref lpData); _trayIconRegistered = false; } } } }