291 lines
6.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|