qstbak/LLib/LLib.Notifications/OutOfGameNotifier.cs
2026-08-17 20:29:32 +10:00

291 lines
6.6 KiB
C#

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<OutOfGameNotifier> _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<OutOfGameNotifier> 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<NOTIFYICONDATA>(),
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<NOTIFYICONDATA>(),
hWnd = gameWindowHandle,
uID = 176u,
uTimeoutOrVersion = 4u
};
NativeMethods.Shell_NotifyIcon(4u, ref lpData2);
_trayHWnd = gameWindowHandle;
_trayIconRegistered = true;
}
NOTIFYICONDATA lpData3 = new NOTIFYICONDATA
{
cbSize = (uint)Marshal.SizeOf<NOTIFYICONDATA>(),
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<NOTIFYICONDATA>(),
hWnd = _trayHWnd,
uID = 176u
};
NativeMethods.Shell_NotifyIcon(2u, ref lpData);
_trayIconRegistered = false;
}
}
}
}