// HDT plugin: makes the in-game overlay work under Wine/Proton on Hyprland. // // Wine's X11 driver decides per window whether it is "managed" (handed to the // compositor, which tiles/floats/focuses it) or override-redirect (placed where // the app says, stacked on top, left alone). A popup becomes managed for good // once it is the active window during a show or SetWindowPos. WPF's Show() // activates by default, so HDT's overlay ends up managed: Hyprland places it // wherever it likes instead of over the game. // // Separately, Wine cuts every alpha-0 pixel of a layered window out of the X11 // shape, and XWayland shows those pixels as opaque black. Painting the window // background at alpha 1/255 keeps every pixel inside the shape while staying // invisible. // // Both fixes have to land before the overlay is first shown. HDT loads enabled // plugins (plugins.xml) before it starts its overlay update loop, so OnLoad is // early enough; enabling the plugin mid-session needs an HDT restart. // // Approach from alexzfe/hdt-omarchy (Utility/Wine.cs), done as a plugin so the // official HDT build and its updater stay in place. using System; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using System.Windows.Media; using Hearthstone_Deck_Tracker; using Hearthstone_Deck_Tracker.Plugins; using Hearthstone_Deck_Tracker.Utility.Logging; namespace HdtWineOverlay { public class WineOverlayPlugin : IPlugin { public string Name => "Wine Overlay Fix"; public string Description => "Transparent, correctly placed overlay under Wine/Proton (Wayland). Restart HDT after enabling."; public string ButtonText => ""; public string Author => "fred"; public Version Version => new Version(0, 1, 0); public MenuItem MenuItem => null; private HwndSource _hookedSource; private RECT _lastGameRect; private DateTime _nextPoll = DateTime.MinValue; public void OnLoad() { if(!IsWine()) { Log.Info("Not running under Wine, nothing to do"); return; } var overlay = Core.Overlay; var handle = new WindowInteropHelper(overlay).Handle; if(handle != IntPtr.Zero && overlay.IsVisible) Log.Warn("Overlay was already shown before this plugin loaded; restart HDT for the fix to apply"); // Never activate on show: keeps Wine from making the overlay a managed window. overlay.ShowActivated = false; if(!(overlay.Background is SolidColorBrush { Color.A: > 0 })) { var brush = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); brush.Freeze(); overlay.Background = brush; } if(handle != IntPtr.Zero) HookMouseActivate(handle); else overlay.SourceInitialized += OnOverlaySourceInitialized; Log.Info("Applied Wine overlay workarounds"); } public void OnUnload() { Core.Overlay.SourceInitialized -= OnOverlaySourceInitialized; _hookedSource?.RemoveHook(WndProc); _hookedSource = null; } public void OnButtonPress() { } // Called by HDT every 100ms on the UI thread. public void OnUpdate() { if(_hookedSource == null || DateTime.UtcNow < _nextPoll) return; _nextPoll = DateTime.UtcNow.AddMilliseconds(250); // HDT follows the game window through an out-of-context WinEvent hook // (EVENT_OBJECT_LOCATIONCHANGE), which Wine doesn't deliver for // compositor-driven moves. Poll the game's client rect instead. var overlay = Core.Overlay; if(!overlay.IsContentVisible) return; var game = FindWindow("UnityWndClass", "Hearthstone"); if(game == IntPtr.Zero || !GetClientRect(game, out var rect)) return; var origin = new POINT(); if(!ClientToScreen(game, ref origin)) return; rect.Left += origin.X; rect.Right += origin.X; rect.Top += origin.Y; rect.Bottom += origin.Y; if(rect.Equals(_lastGameRect)) return; _lastGameRect = rect; overlay.UpdatePosition(); } private void OnOverlaySourceInitialized(object sender, EventArgs e) => HookMouseActivate(new WindowInteropHelper(Core.Overlay).Handle); private void HookMouseActivate(IntPtr handle) { _hookedSource = HwndSource.FromHwnd(handle); _hookedSource?.AddHook(WndProc); } // Clicking an overlay button would otherwise activate the window, which // makes Wine turn it into a managed window on the next position update. private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { const int wmMouseActivate = 0x0021; const int maNoActivate = 3; if(msg != wmMouseActivate) return IntPtr.Zero; handled = true; return new IntPtr(maNoActivate); } private static bool IsWine() { var ntdll = GetModuleHandle("ntdll.dll"); return ntdll != IntPtr.Zero && GetProcAddress(ntdll, "wine_get_version") != IntPtr.Zero; } [StructLayout(LayoutKind.Sequential)] private struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; } [DllImport("kernel32.dll", CharSet = CharSet.Ansi)] private static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32.dll", CharSet = CharSet.Ansi)] private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint); } }