Files
my-botty/quest_debug.py
alex 0af8a54116 feat: auto-detect D2R key bindings from .key/.keyo file
- Read character .key file from Saved Games / D2R install dir / APPDATA
- Auto-fill empty hotkeys in [char] section (inventory, belt, potions, etc.)
- Match skill slots to configured skills for validation
- Normalize key variants (left alt ~ alt, left shift ~ shift)
- params.ini values always take priority over detected bindings
- Warn on genuine mismatches between config and .key file
2026-05-19 16:46:24 +02:00

225 lines
6.0 KiB
Python

"""
D2R capture tool - works with Windows DPI scaling.
Set DPI awareness then grab the D2R client area directly.
Keys: F1-full OCR F2-dialogue F3-questlog F4-NPCs F5-pixel F12-exit
"""
import os, sys, cv2, numpy as np, keyboard, win32gui, win32con, ctypes
from datetime import datetime
# Set DPI awareness - this makes Win32 APIs return logical (unscaled) coordinates
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except:
try:
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except:
pass
# Fix tesserocr DLLs
if sys.platform == "win32":
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
if os.path.isdir(_dll):
os.add_dll_directory(_dll)
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
SAVE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "screenshots", "debug")
os.makedirs(SAVE, exist_ok=True)
def find_d2r():
hwnds = []
def cb(h, r):
if 'diablo' in win32gui.GetWindowText(h).lower() and win32gui.IsWindowVisible(h):
r.append(h)
win32gui.EnumWindows(cb, hwnds)
return hwnds[0] if hwnds else None
def grab():
"""Grab D2R client area at native 1280x720 resolution."""
from mss import mss
hwnd = find_d2r()
if not hwnd:
print(" [ERROR] D2R not found")
return None
client = win32gui.GetClientRect(hwnd)
w, h = client[2]-client[0], client[3]-client[1]
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
with mss() as sct:
region = {
'top': screen_pos[1],
'left': screen_pos[0],
'width': w,
'height': h
}
sct_img = sct.grab(region)
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
# Resize to 1280x720 if needed
if w != 1280 or h != 720:
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
print(f" [RESIZED] {w}x{h} -> 1280x720")
else:
print(f" [CAPTURED] {w}x{h}")
return img
def ocr(img, roi=None):
try:
from d2r_image.ocr import image_to_text
target = img if roi is None else img[roi[1]:roi[1]+roi[3], roi[0]:roi[0]+roi[2]]
result = image_to_text(target, psm=6, scale=1.5, threshold=25)
return [r.text.strip() for r in result if r.text.strip()]
except Exception as e:
return [f"[OCR ERROR] {e}"]
def save(img, label):
path = os.path.join(SAVE, f"{label}_{datetime.now().strftime('%H%M%S')}.png")
cv2.imwrite(path, img)
print(f" [SAVED] {path}")
return path
# === Handlers ===
def on_f1():
print("\n[=== FULL CAPTURE ===]")
img = grab()
if not img:
return
h, w = img.shape[:2]
print(f" Size: {w}x{h}")
save(img, "full")
# UI detection
print(" UI:")
try:
from ui_manager import ScreenObjects, is_visible
found = False
for name in ['InGame', 'Loading', 'MainMenu', 'OnlineStatus', 'DeathScreen',
'NPCDialogue', 'RightPanel', 'LeftPanel', 'SkillsExpanded']:
obj = getattr(ScreenObjects, name, None)
if obj and is_visible(obj, img):
print(f" [VISIBLE] {name}")
found = True
if not found:
print(" (none)")
except Exception as e:
print(f" [err] {e}")
# Full OCR
print(" OCR:")
lines = ocr(img)
for l in lines[:30]:
print(f" {l}")
if len(lines) > 30:
print(f" ... and {len(lines)-30} more")
def on_f2():
print("\n[=== DIALOGUE ===]")
img = grab()
if not img:
return
save(img, "dialogue")
text = ocr(img, (200, 460, 880, 100))
if text:
print(" NPC says:")
for l in text:
print(f" {l}")
else:
print(" (no NPC text)")
opts = ocr(img, (200, 560, 880, 140))
if opts:
print(" Options:")
for i, o in enumerate(opts):
print(f" [{i}] {o}")
else:
print(" (no options detected - is dialogue box open?)")
def on_f3():
print("\n[=== QUEST LOG ===]")
img = grab()
if not img:
return
save(img, "quest_log")
for l in ocr(img, (200, 100, 880, 520)):
print(f" {l}")
def on_f4():
print("\n[=== NPC DETECTION ===]")
img = grab()
if not img:
return
save(img, "npcs")
try:
import template_finder
from npc_manager import npcs
found = []
for name, data in npcs.items():
for t in data.get("template_group", []):
r = template_finder.search(t, img, threshold=0.35)
if r.valid:
found.append(f" {name} at {r.center_monitor} ({r.score:.2f})")
break
for f in found:
print(f)
if not found:
print(" (none)")
except Exception as e:
print(f" [ERROR] {e}")
def on_f5():
img = grab()
if not img:
return
import mouse as _mouse
mx, my = _mouse.get_position()
hwnd = find_d2r()
if hwnd:
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
ix = mx - screen_pos[0]
iy = my - screen_pos[1]
if 0 <= ix < img.shape[1] and 0 <= iy < img.shape[0]:
b, g, r = img[iy, ix]
print(f" ({ix},{iy}) RGB({r},{g},{b})")
else:
print(" Mouse outside D2R client area")
# === Run ===
def run():
print("=== Botty Capture Tool ===")
print(" F1 - Full capture + OCR + UI detection")
print(" F2 - Dialogue capture + OCR")
print(" F3 - Quest log OCR (press O in D2R first)")
print(" F4 - Detect NPCs")
print(" F5 - Mouse pixel color")
print(" F12 - Exit")
print("Ready.")
keyboard.add_hotkey('f1', on_f1)
keyboard.add_hotkey('f2', on_f2)
keyboard.add_hotkey('f3', on_f3)
keyboard.add_hotkey('f4', on_f4)
keyboard.add_hotkey('f5', on_f5)
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
keyboard.wait()
if __name__ == "__main__":
run()