Files
my-botty/src/utils/npc_auto_label.py
T

146 lines
4.6 KiB
Python

"""
Auto-label NPCs visible on screen.
Scans for all known NPCs using their template groups (in parallel via
template_finder.search) and returns a dict of {npc_name: {center, score}}.
Can be called from any game loop tick without blocking the main thread.
Usage:
from utils.npc_auto_label import detect_visible_npcs
npcs_on_screen = detect_visible_npcs(img)
# npcs_on_screen = {"AKARA": {"center": (620, 300), "score": 0.82}, ...}
"""
import concurrent.futures
import template_finder
from screen import grab
# NPCs that exist per act — detected via their template groups in npc_manager
NPC_TEMPLATES = {
# Act 1
"AKARA": ["AKARA_FRONT", "AKARA_BACK", "AKARA_SIDE", "AKARA_SIDE_2"],
"CHARSI": ["CHARSI_FRONT", "CHARSI_BACK", "CHARSI_SIDE", "CHARSI_SIDE_2", "CHARSI_SIDE_3"],
"KASHYA": ["KASHYA_FRONT", "KASHYA_BACK", "KASHYA_SIDE", "KASHYA_SIDE_2"],
"CAIN": ["CAIN_0", "CAIN_1", "CAIN_2", "CAIN_3"],
# Act 2
"FARA": ["FARA_LIGHT_1", "FARA_LIGHT_3", "FARA_MEDIUM_1", "FARA_DARK_1"],
"DROGNAN": ["DROGNAN_FRONT", "DROGNAN_LEFT", "DROGNAN_RIGHT_SIDE"],
"LYSANDER": ["LYSANDER_FRONT", "LYSANDER_BACK", "LYSANDER_SIDE", "LYSANDER_SIDE_2"],
# Act 3
"ORMUS": ["ORMUS_0", "ORMUS_2", "ORMUS_4"],
# Act 4
"TYRAEL": ["TYRAEL_1", "TYRAEL_2"],
"JAMELLA": ["JAMELLA_FRONT", "JAMELLA_BACK", "JAMELLA_SIDE"],
"HALBU": ["HALBU_FRONT", "HALBU_BACK", "HALBU_SIDE", "HALBU_SIDE_2"],
# Act 5
"QUAL_KEHK": ["QUAL_0", "QUAL_45", "QUAL_180", "QUAL_270"],
"MALAH": ["MALAH_FRONT", "MALAH_BACK", "MALAH_45", "MALAH_SIDE"],
"LARZUK": ["LARZUK_FRONT", "LARZUK_BACK", "LARZUK_SIDE"],
"ANYA": ["ANYA_FRONT", "ANYA_BACK", "ANYA_SIDE"],
}
# How many NPCs to search in parallel (limit thread pool)
MAX_WORKERS = 8
# Search ROI — skip the bottom skill bar area to reduce false positives
SEARCH_ROI = [0, 0, 1280, 480]
def _search_npc(npc_name, template_keys, img):
"""Search for one NPC using all its template variants. Returns match or None."""
best = template_finder.search(
template_keys,
img,
threshold=0.55,
roi=SEARCH_ROI,
best_match=True,
)
if best.valid:
return {
"center": best.center,
"center_monitor": best.center_monitor,
"score": best.score,
}
return None
def detect_visible_npcs(img=None):
"""
Scan the screen for all known NPCs.
:param img: Screenshot (BGR numpy array). If None, grabs one.
:return: dict {npc_name: {"center": (x,y), "center_monitor": (x,y), "score": float}}
"""
if img is None:
img = grab()
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = {
executor.submit(_search_npc, name, templates, img): name
for name, templates in NPC_TEMPLATES.items()
}
for future in concurrent.futures.as_completed(futures):
npc_name = futures[future]
try:
match = future.result()
if match is not None:
results[npc_name] = match
except Exception:
pass
return results
# ─── Optional: persistent cache that expires after N seconds ───
_cache = {}
_cache_time = 0.0
def detect_visible_npcs_cached(img=None, ttl=5.0):
"""
Like detect_visible_npcs() but caches results for ttl seconds.
Use this when scanning every tick but only need fresh data periodically.
"""
import time
global _cache_time
now = time.time()
if now - _cache_time < ttl:
return _cache
_cache.clear()
_cache.update(detect_visible_npcs(img))
_cache_time = now
return _cache
if __name__ == "__main__":
import cv2
from input_layer import keyboard
from screen import start_detecting_window
start_detecting_window()
print("Press F12 to exit. Scanning for NPCs...")
while True:
keyboard.pause(0.05, True)
if keyboard.is_pressed("f12"):
break
img = grab()
found = detect_visible_npcs(img)
display = img.copy()
for name, info in found.items():
x, y = info["center"]
cv2.putText(display, name, (x - 30, y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA)
cv2.circle(display, (x, y), 5, (0, 255, 0), -1)
if found:
npc_strs = [f"{n}({info['score']:.2f})" for n, info in found.items()]
print(f"NPCs on screen: {', '.join(npc_strs)}")
cv2.imshow("NPC Auto-Label", display)
cv2.waitKey(1)