199 lines
6.0 KiB
Python
199 lines
6.0 KiB
Python
"""
|
|
D2R Asset Extractor
|
|
|
|
Runs on your local Windows machine. Captures D2R, saves screenshot.
|
|
You then send the screenshot to the AI agent for analysis.
|
|
AI returns bounding boxes -> run crop.py to extract PNGs.
|
|
|
|
Usage:
|
|
Run: python asset_extractor.py
|
|
F1: Capture D2R screen -> screenshots/debug/latest.png
|
|
F2: Crop entities from screenshots/debug/latest_annotations.json
|
|
F3: List existing assets
|
|
F12: Exit
|
|
|
|
Workflow:
|
|
1. Run this script in the botty conda env
|
|
2. F1 to capture
|
|
3. Tell your AI agent to analyze screenshots/debug/latest.png
|
|
4. AI writes screenshots/debug/latest_annotations.json with bounding boxes
|
|
5. F2 to crop entities into assets/enemies/ or assets/npc/
|
|
"""
|
|
import os, sys, cv2, numpy as np, keyboard, json, ctypes, win32gui
|
|
from datetime import datetime
|
|
from mss import mss
|
|
|
|
# DPI awareness - must be first
|
|
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"))
|
|
|
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
|
SAVE_DIR = os.path.join(BASE, "screenshots", "debug")
|
|
ENEMIES_DIR = os.path.join(BASE, "assets", "enemies")
|
|
NPC_DIR = os.path.join(BASE, "assets", "npc")
|
|
|
|
for d in [SAVE_DIR, ENEMIES_DIR, NPC_DIR]:
|
|
os.makedirs(d, exist_ok=True)
|
|
|
|
LATEST_PATH = os.path.join(SAVE_DIR, "latest.png")
|
|
ANNOTATIONS_PATH = os.path.join(SAVE_DIR, "latest_annotations.json")
|
|
|
|
# Known NPC names for routing
|
|
NPC_NAMES = {
|
|
'akara', 'charsi', 'kashya', 'cain', 'drognan', 'lysander',
|
|
'fara', 'ormus', 'tyrael', 'jamella', 'halbu', 'qual_kehk',
|
|
'qual-kehk', 'qualkehk', 'malah', 'larzuk', 'anya'
|
|
}
|
|
|
|
|
|
def find_d2r():
|
|
hwnds = []
|
|
def cb(h, r):
|
|
title = win32gui.GetWindowText(h)
|
|
if 'diablo' in title.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. Resizes to 1280x720 if needed."""
|
|
hwnd = find_d2r()
|
|
if not hwnd:
|
|
print(" [ERROR] D2R not found. Is it running and visible?")
|
|
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
|
|
|
|
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 on_f1():
|
|
"""Capture D2R and save."""
|
|
print("\n[=== CAPTURING ===]")
|
|
img = grab()
|
|
if not img:
|
|
return
|
|
cv2.imwrite(LATEST_PATH, img)
|
|
print(f" [SAVED] {LATEST_PATH}")
|
|
print(f" Now ask your AI agent to analyze: {LATEST_PATH}")
|
|
print(f" AI should write: {ANNOTATIONS_PATH}")
|
|
print(' Format: [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
|
|
|
|
|
|
def on_f2():
|
|
"""Crop entities from latest capture using annotations JSON."""
|
|
print("\n[=== CROPPING ENTITIES ===]")
|
|
if not os.path.exists(LATEST_PATH):
|
|
print(" [ERROR] No capture found. Press F1 first.")
|
|
return
|
|
if not os.path.exists(ANNOTATIONS_PATH):
|
|
print(" [ERROR] No annotations found.")
|
|
print(f" Create: {ANNOTATIONS_PATH}")
|
|
print(' [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
|
|
return
|
|
|
|
img = cv2.imread(LATEST_PATH)
|
|
with open(ANNOTATIONS_PATH) as f:
|
|
entities = json.load(f)
|
|
|
|
print(f" Image: {img.shape[1]}x{img.shape[0]}, Entities: {len(entities)}")
|
|
|
|
saved = 0
|
|
for ent in entities:
|
|
name = ent['name'].lower().replace(' ', '_')
|
|
x, y = int(ent['x']), int(ent['y'])
|
|
w, h = int(ent['w']), int(ent['h'])
|
|
i_w, i_h = img.shape[1], img.shape[0]
|
|
|
|
# Crop with 5px padding
|
|
pad = 5
|
|
x1, y1 = max(0, x - pad), max(0, y - pad)
|
|
x2, y2 = min(i_w, x + w + pad), min(i_h, y + h + pad)
|
|
crop = img[y1:y2, x1:x2]
|
|
|
|
# Route to npc or enemies folder
|
|
if name in NPC_NAMES:
|
|
save_dir = NPC_DIR
|
|
else:
|
|
save_dir = ENEMIES_DIR
|
|
|
|
# Auto-number duplicates
|
|
fname = f"{name}.png"
|
|
save_path = os.path.join(save_dir, fname)
|
|
variant = 1
|
|
while os.path.exists(save_path):
|
|
variant += 1
|
|
fname = f"{name}_{variant}.png"
|
|
save_path = os.path.join(save_dir, fname)
|
|
|
|
cv2.imwrite(save_path, crop)
|
|
print(f" [SAVED] {save_path} ({crop.shape[1]}x{crop.shape[0]})")
|
|
saved += 1
|
|
|
|
print(f"\n Total: {saved} assets cropped.")
|
|
|
|
|
|
def on_f3():
|
|
"""List existing assets."""
|
|
print("\n[=== ASSETS INVENTORY ===]")
|
|
for label, d in [("enemies", ENEMIES_DIR), ("npc", NPC_DIR)]:
|
|
if os.path.isdir(d):
|
|
files = sorted(os.listdir(d))
|
|
print(f"\n assets/{label}/ ({len(files)} files):")
|
|
for f in files:
|
|
sz = os.path.getsize(os.path.join(d, f))
|
|
print(f" {f} ({sz}b)")
|
|
else:
|
|
print(f"\n assets/{label}/ - EMPTY")
|
|
|
|
|
|
def run():
|
|
print("=== D2R Asset Extractor ===")
|
|
print(" F1 - Capture D2R screen")
|
|
print(" F2 - Crop entities from annotations")
|
|
print(" F3 - List assets")
|
|
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('f12', lambda: (print("\nBye."), sys.exit(0)))
|
|
keyboard.wait()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|