Files
DirtyLeague_Bot/window_utils.py

223 lines
8.0 KiB
Python

"""
Utility module for window management, screen capture, and coordinate translation
for the Unity game bot.
"""
import ctypes
from ctypes import wintypes
import time
from typing import Optional, Tuple
import numpy as np
from PIL import Image
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
# Enable Per-Monitor DPI Awareness V2
try:
shcore = ctypes.windll.shcore
shcore.SetProcessDpiAwareness(2)
except Exception:
pass
DESKTOP_ALL_ACCESS = 0x01FF
PW_RENDERFULLCONTENT = 2
SRCCOPY = 0x00CC0020
SW_RESTORE = 9
SW_SHOW = 5
def ensure_interactive_desktop():
"""
Ensures the current thread is attached to the interactive 'Default' desktop.
Required when running under background/isolated sandbox sessions.
"""
try:
h_desk = user32.OpenDesktopW("Default", 0, False, DESKTOP_ALL_ACCESS)
if h_desk:
user32.SetThreadDesktop(h_desk)
except Exception:
pass
ensure_interactive_desktop()
class WindowManager:
"""Manages window discovery, activation, and capture."""
def __init__(self, window_title: str = "DirtyLeague", process_name: str = "DirtyLeague.exe"):
self.window_title = window_title
self.process_name = process_name
self._hwnd: Optional[int] = None
def find_window(self) -> Optional[int]:
"""Finds the target game window handle."""
ensure_interactive_desktop()
# 1. Search by exact window title
hwnd = user32.FindWindowW(None, self.window_title)
if hwnd and user32.IsWindow(hwnd):
self._hwnd = hwnd
return hwnd
# 2. Search by partial title or process
found_hwnds = []
WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
def _enum_callback(h, _):
if user32.IsWindowVisible(h):
length = user32.GetWindowTextLengthW(h)
if length > 0:
buff = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(h, buff, length + 1)
title = buff.value
if self.window_title.lower() in title.lower():
found_hwnds.append(h)
return True
h_desk = user32.OpenDesktopW("Default", 0, False, DESKTOP_ALL_ACCESS)
if h_desk:
user32.EnumDesktopWindows(h_desk, WNDENUMPROC(_enum_callback), 0)
user32.CloseDesktop(h_desk)
else:
user32.EnumWindows(WNDENUMPROC(_enum_callback), 0)
if found_hwnds:
self._hwnd = found_hwnds[0]
return self._hwnd
return None
@property
def hwnd(self) -> int:
"""Returns active hwnd or searches for it."""
if not self._hwnd or not user32.IsWindow(self._hwnd):
hwnd = self.find_window()
if not hwnd:
raise RuntimeError(f"Window '{self.window_title}' not found. Ensure the game is running.")
return self._hwnd
def focus(self) -> bool:
"""Brings the window to the foreground and unminimizes if needed."""
hwnd = self.hwnd
if user32.IsIconic(hwnd):
user32.ShowWindow(hwnd, SW_RESTORE)
time.sleep(0.2)
user32.ShowWindow(hwnd, SW_SHOW)
user32.SetForegroundWindow(hwnd)
time.sleep(0.1)
return True
def get_window_rect(self) -> Tuple[int, int, int, int]:
"""Returns (left, top, width, height) of the entire window."""
rect = wintypes.RECT()
user32.GetWindowRect(self.hwnd, ctypes.byref(rect))
return rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top
def get_client_rect(self) -> Tuple[int, int, int, int]:
"""Returns (client_left, client_top, client_width, client_height) in screen coordinates."""
hwnd = self.hwnd
crect = wintypes.RECT()
user32.GetClientRect(hwnd, ctypes.byref(crect))
width = crect.right - crect.left
height = crect.bottom - crect.top
pt = wintypes.POINT(0, 0)
user32.ClientToScreen(hwnd, ctypes.byref(pt))
return pt.x, pt.y, width, height
def capture_frame(self, client_only: bool = True) -> np.ndarray:
"""
Captures the current window content and returns a BGR NumPy array (OpenCV format).
Uses direct Desktop BitBlt for highest fidelity and hardware acceleration support.
"""
ensure_interactive_desktop()
hwnd = self.hwnd
c_left, c_top, c_width, c_height = self.get_client_rect()
if c_width <= 0 or c_height <= 0:
raise RuntimeError(f"Invalid client dimensions: {c_width}x{c_height}")
if client_only:
capture_x, capture_y, capture_w, capture_h = c_left, c_top, c_width, c_height
hdc_source = user32.GetDC(0) # Desktop DC
else:
w_left, w_top, w_width, w_height = self.get_window_rect()
capture_x, capture_y, capture_w, capture_h = w_left, w_top, w_width, w_height
hdc_source = user32.GetDC(0)
mem_dc = gdi32.CreateCompatibleDC(hdc_source)
save_bitmap = gdi32.CreateCompatibleBitmap(hdc_source, capture_w, capture_h)
gdi32.SelectObject(mem_dc, save_bitmap)
gdi32.BitBlt(mem_dc, 0, 0, capture_w, capture_h, hdc_source, capture_x, capture_y, SRCCOPY)
class BITMAPINFOHEADER(ctypes.Structure):
_fields_ = [
('biSize', wintypes.DWORD),
('biWidth', wintypes.LONG),
('biHeight', wintypes.LONG),
('biPlanes', wintypes.WORD),
('biBitCount', wintypes.WORD),
('biCompression', wintypes.DWORD),
('biSizeImage', wintypes.DWORD),
('biXPelsPerMeter', wintypes.LONG),
('biYPelsPerMeter', wintypes.LONG),
('biClrUsed', wintypes.DWORD),
('biClrImportant', wintypes.DWORD)
]
bmi = BITMAPINFOHEADER()
bmi.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bmi.biWidth = capture_w
bmi.biHeight = -capture_h
bmi.biPlanes = 1
bmi.biBitCount = 32
bmi.biCompression = 0
buffer_len = capture_w * capture_h * 4
buf = (ctypes.c_ubyte * buffer_len)()
gdi32.GetDIBits(mem_dc, save_bitmap, 0, capture_h, ctypes.byref(buf), ctypes.byref(bmi), 0)
gdi32.DeleteObject(save_bitmap)
gdi32.DeleteDC(mem_dc)
user32.ReleaseDC(0, hdc_source)
raw_arr = np.ctypeslib.as_array(buf).reshape((capture_h, capture_w, 4))
return raw_arr[:, :, :3].copy()
def client_to_screen(self, x: int, y: int) -> Tuple[int, int]:
"""Translates client coordinates (x, y) to absolute desktop screen coordinates."""
c_left, c_top, _, _ = self.get_client_rect()
return c_left + x, c_top + y
def rel_to_client(self, rx: float, ry: float) -> Tuple[int, int]:
"""Translates normalized relative coordinates (0.0..1.0) to client pixel coordinates (x, y)."""
_, _, width, height = self.get_client_rect()
return int(rx * width), int(ry * height)
def rel_to_client_rect(self, rx1: float, ry1: float, rx2: float, ry2: float) -> Tuple[int, int, int, int]:
"""Translates normalized relative ROI (rx1, ry1, rx2, ry2) to client pixel rect (x1, y1, x2, y2)."""
_, _, width, height = self.get_client_rect()
return int(rx1 * width), int(ry1 * height), int(rx2 * width), int(ry2 * height)
def rel_to_screen(self, rx: float, ry: float) -> Tuple[int, int]:
"""Translates normalized relative coordinates (0.0..1.0) directly to absolute screen coordinates."""
cx, cy = self.rel_to_client(rx, ry)
return self.client_to_screen(cx, cy)
if __name__ == "__main__":
wm = WindowManager()
hwnd = wm.find_window()
print(f"Found Window HWND: {hwnd}")
if hwnd:
rect = wm.get_window_rect()
crect = wm.get_client_rect()
print(f"Window Rect: {rect}")
print(f"Client Rect: {crect}")
frame = wm.capture_frame(client_only=True)
print(f"Captured frame shape: {frame.shape}")