feat: detect Windows profile for install and input

This commit is contained in:
alex
2026-06-07 18:26:16 +02:00
parent d0ecf3afdc
commit 86e3cccbc9
10 changed files with 252 additions and 36 deletions
+16 -20
View File
@@ -5,26 +5,17 @@ No kernel drivers, no third-party DLLs. Only standard system DLLs (user32, kerne
import ctypes
import time
import struct
import platform
from ctypes import wintypes
from utils.misc import wait
from utils.os_detect import detect_os
# ─── Win10 vs Win11 mouse mode detection ───
# Win10 vs Win11 mouse mode detection
# Win10: SendInput needs MOUSEEVENTF_ABSOLUTE for correct positioning
# Win11 (25H2+): ABSOLUTE flag breaks mouse — use relative-only
# Detect at import time so the whole session uses the right mode.
def _is_win11():
"""Return True if running on Windows 11 (build >= 22000)."""
try:
ver = platform.win32_ver() # ('10', '10.0.26200', 'SP0', ...)
build_str = ver[1].split('.')[2] # '26200'
build = int(build_str)
return build >= 22000
except Exception:
return False
_USE_ABSOLUTE_MOUSE = not _is_win11() # True for Win10, False for Win11
_OS_INFO = detect_os()
_USE_ABSOLUTE_MOUSE = _OS_INFO.use_absolute_mouse
# ─── user32 constants ───
@@ -196,7 +187,7 @@ def _make_mouse_input(flags: int, dx: int = 0, dy: int = 0, data: int = 0):
i.union.mi.dx = dx
i.union.mi.dy = dy
i.union.mi.mouseData = data
i.union.mi.dwFlags = flags | (MOUSEEVENTF_ABSOLUTE if _USE_ABSOLUTE_MOUSE else 0)
i.union.mi.dwFlags = flags
i.union.mi.dwExtraInfo = None
return i
@@ -274,12 +265,17 @@ def get_screen_size():
return user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)
def mouse_move(x: int, y: int):
"""Move mouse to absolute screen position (uses absolute SendInput)."""
screen_w, screen_h = get_screen_size()
# SendInput expects normalized absolute coordinates 0-65535
norm_x = int(x * 65535 / screen_w)
norm_y = int(y * 65535 / screen_h)
_send_input(_make_mouse_input(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, norm_x, norm_y))
"""Move mouse to an absolute screen position using the OS-specific mode."""
if _USE_ABSOLUTE_MOUSE:
screen_w, screen_h = get_screen_size()
# SendInput expects normalized absolute coordinates 0-65535.
norm_x = int(x * 65535 / screen_w)
norm_y = int(y * 65535 / screen_h)
_send_input(_make_mouse_input(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, norm_x, norm_y))
return
cur_x, cur_y = get_cursor_pos()
_send_input(_make_mouse_input(MOUSEEVENTF_MOVE, int(x - cur_x), int(y - cur_y)))
def mouse_down(button: str = "left"):
"""Press mouse button down."""
+8 -10
View File
@@ -24,6 +24,7 @@ from game_controller import GameController
from utils.graphic_debugger import GraphicDebuggerController
from utils.misc import restore_d2r_window_visibility
from utils.auto_settings import adjust_settings, backup_settings, restore_settings_from_backup
from utils.os_detect import detect_os
from ui.run_selector import open_run_selector
@dataclass
@@ -66,18 +67,15 @@ def on_exit(controllers: Controllers):
def _log_platform_info():
"""Log Windows version, mouse mode, and OCR backend at startup."""
import platform
ver = platform.win32_ver()
try:
build = int(ver[1].split('.')[2])
except Exception:
build = 0
is_win11 = build >= 22000
win_label = f"Windows 11 (build {build})" if is_win11 else f"Windows 10 (build {build})"
Logger.info(f"Platform: {win_label}")
os_info = detect_os()
build = f" (build {os_info.build})" if os_info.build else ""
Logger.info(f"Platform: {os_info.label}{build}")
if not os_info.supported:
Logger.warning(f"Unsupported OS detected: {os_info.system} {os_info.release}")
Logger.info(f"Requirements profile: {os_info.requirements_file}")
from input_layer.win_input import _USE_ABSOLUTE_MOUSE
mouse_mode = "relative (Win11)" if not _USE_ABSOLUTE_MOUSE else "absolute (Win10)"
mouse_mode = "absolute (Windows 10)" if _USE_ABSOLUTE_MOUSE else "relative (Windows 11)"
Logger.info(f"Mouse input mode: {mouse_mode}")
# OCR backend check
+110
View File
@@ -0,0 +1,110 @@
"""Operating-system detection for Botty runtime and installers.
Windows reports Windows 11 as major version 10, so Botty must classify the OS
from the Windows build number instead of the marketing version string.
"""
from __future__ import annotations
from dataclasses import dataclass
import os
import platform
import sys
WINDOWS_11_MIN_BUILD = 22000
@dataclass(frozen=True)
class BottyOsInfo:
system: str
release: str
version: str
build: int
label: str
requirements_file: str
use_absolute_mouse: bool
supported: bool
@property
def mouse_mode(self) -> str:
return "absolute" if self.use_absolute_mouse else "relative"
def parse_windows_build(version: str | None) -> int:
"""Extract a Windows build number from strings like '10.0.22631'."""
if not version:
return 0
parts = str(version).split(".")
for part in reversed(parts):
try:
return int(part)
except ValueError:
continue
return 0
def classify_windows(build: int) -> tuple[str, str, bool, bool]:
"""Return label, requirements file, absolute-mouse flag, supported flag."""
if build >= WINDOWS_11_MIN_BUILD:
return "Windows 11", "requirements-win11.txt", False, True
if build > 0:
return "Windows 10", "requirements-win10.txt", True, True
return "Windows unknown", "requirements-win10.txt", True, False
def _registry_windows_build() -> int:
if sys.platform != "win32":
return 0
try:
import winreg
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion") as key:
value, _ = winreg.QueryValueEx(key, "CurrentBuild")
return int(value)
except Exception:
return 0
def detect_os() -> BottyOsInfo:
system = platform.system()
release = platform.release()
version = platform.version()
if system == "Windows" or os.name == "nt":
build = parse_windows_build(platform.win32_ver()[1]) or parse_windows_build(version) or _registry_windows_build()
label, req_file, use_absolute, supported = classify_windows(build)
return BottyOsInfo(
system="Windows",
release=release,
version=version,
build=build,
label=label,
requirements_file=req_file,
use_absolute_mouse=use_absolute,
supported=supported,
)
return BottyOsInfo(
system=system or "unknown",
release=release,
version=version,
build=0,
label=system or "unknown",
requirements_file="requirements.txt",
use_absolute_mouse=True,
supported=False,
)
def describe_current_os() -> str:
info = detect_os()
build = f" build {info.build}" if info.build else ""
support = "supported" if info.supported else "unsupported"
return (
f"{info.label}{build} ({support}); "
f"requirements={info.requirements_file}; mouse={info.mouse_mode}"
)
if __name__ == "__main__":
print(describe_current_os())