feat: detect Windows profile for install and input
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user