|
2025-09-27 12:50
조회: 925
추천: 0
코딩 잘 아시는 분 계신가요? 급합니다.![]() 파판7 리메이크 미니게임이 너무 어려워서 자동 조준 툴을 만드는 중인데요. 마우스로 원하는 지점의 색상을 클릭하면 그곳으로 WASD키를 누르면서 자동으로 조준 보정을 하는 툴을 만들고 있습니다. 저는 코딩에 대해 전혀 기초적인 베이스가 없는 사람이구요. 본격적으로 이 길을 가려고 하기보단 그냥 간단한 툴을 만들어서 이 구간을 깨고 싶은 사람입니다. 파이썬 기반이고요. 지금까지 짠 코드는 이렇습니다. # FF7R Darts Helper v12.0 (FULL) # - 투명 HUD 오버레이(클릭스루, 전체 화면) # - 원형 ROI: F1(중심→가장자리 드래그식 2클릭), F2(중심/반지름 직접 입력) # - 좌하단 상태창(노란 글씨), 우하단 Target Shape(선택 색상 미리보기) # - 전역 단축키(RegisterHotKey): F1~F9, Ctrl+Alt+Q (게임 포커스와 무관) # - WASD 자동 보정, Enter 자동 발사(최소 반경 통과 시) # - 색상 픽(F7) & 연결성 기반 색상 추적(F8) — ROI 내부로 제한 # - 아이드롭퍼 .cur 시스템 커서 교체(실패 시 HUD 스포이드 PNG), 어떤 종료에도 복구 보장 import os, sys, time, threading, math, signal, atexit, queue, logging, pathlib from collections import deque import numpy as np import cv2 import mss from pynput.keyboard import Controller as KeyController, Key from pynput.mouse import Controller as MouseController, Button from pynput.mouse import Listener as MouseListener from PyQt6 import QtCore, QtGui, QtWidgets from ctypes import windll, wintypes import ctypes # ===================== 전역 설정 ===================== USE_BEEP = True # 조준 보정(WASD) 파라미터 AIM_TOLERANCE = 4 PID_KP, PID_KI, PID_KD = 0.16, 0.00, 0.08 TAP_MIN_MS, TAP_MAX_MS = 14, 45 TAP_DEADZONE = 1.5 # 원(게이지) 탐지 파라미터 SMOOTH_N = 5 RADIUS_MIN_VALID = 6 HOUGH_DP, HOUGH_MIN_DIST = 1.2, 15 HOUGH_PARAM1, HOUGH_PARAM2 = 120, 20 MINR, MAXR = 5, 120 MIN_PEAK_GAP = 0.25 # 색상 추적(HSV) COLOR_TOL_H, COLOR_TOL_S, COLOR_TOL_V = 10, 80, 80 MIN_COLOR_AREA = 18 MAX_JUMP_PX = 30 MORPH_K = np.ones((3,3), np.uint8) # HUD 색/스타일 YELLOW = QtGui.QColor(255, 255, 0) WHITE = QtGui.QColor(255, 255, 255) PANEL_BG = QtGui.QColor(40, 40, 40, 180) TARGET_BORDER = QtGui.QColor(255, 255, 255, 220) ROI_COLOR = QtGui.QColor(255, 60, 60, 220) # 원형 ROI 테두리(빨강) # 커서/스포이드 표시 정책 USE_SYSTEM_CURSOR = True # 실행 중 .cur로 시스템 커서를 바꾸기 SHOW_HUD_EYEDROPPER = False # HUD 스포이드 PNG도 따라다니게(겹치면 False 권장) EYE_HOTSPOT = (16, 30) # HUD 스포이드 PNG 핫스팟 오프셋(px) # ===================== 로깅 ===================== def setup_logging(): log_dir = pathlib.Path(os.getenv("LOCALAPPDATA", os.getcwd())) / "ff7_darts" log_dir.mkdir(parents=True, exist_ok=True) logging.basicConfig( filename=str(log_dir / "ff7.log"), level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s" ) setup_logging() def log_ex(ex: Exception): try: logging.exception(ex) except: pass # ===================== 리소스 경로 ===================== def resource_path(rel_path: str) -> str: if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, rel_path) base = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base, rel_path) EYEDROPPER_PNG = resource_path("eyedropper.png") EYEDROPPER_CUR = resource_path("eyedropper.cur") # ===================== 전역 상태 ===================== kb = KeyController() mouse = MouseController() # 모니터 / ROI 초기값은 Qt로 읽어서 설정 (mss는 스레드 로컬에서 생성) MON = {'left': 0, 'top': 0, 'width': 1920, 'height': 1080} # fallback ROI_C = {'cx': 960, 'cy': 540, 'r': 360} assist_enabled = True autofire_enabled = True target_pt = None # ROI-rect 좌표계 (x,y) target_locked = False smooth_q = deque(maxlen=SMOOTH_N) prev_r = None prev_dr_sign = None last_trigger_t = 0.0 prev_time = time.time() err_int_x = err_int_y = 0.0 prev_err_x = prev_err_y = 0.0 color_track_enabled = False selected_hsv = None seed_xy = None last_centroid = None last_circle_xy = None # 최근 검출 원 중심(ROI-rect) last_radius_s = None # 스무딩 반경 eyedrop_img = None # HUD 스포이드 PNG (있으면 표시) running = True state_lock = threading.Lock() stop_event = threading.Event() gui_app: QtWidgets.QApplication | None = None # GUI 종료 신호용 # ===================== 유틸 ===================== def beep(): if not USE_BEEP: return try: import winsound winsound.Beep(1850, 70) except: pass def clip(v, lo, hi): return max(lo, min(hi, v)) def short_tap(axis, ms, sign): """WASD 전송: 축/길이/부호(방향)로 짧은 탭""" ms = clip(ms, TAP_MIN_MS, TAP_MAX_MS) key = ('d' if sign > 0 else 'a') if axis == 'x' else ('s' if sign > 0 else 'w') try: kb.press(key); time.sleep(ms/1000.0); kb.release(key) except Exception as e: print("[Tap] failed", e) def calc_error(cur_xy, tgt_xy): if cur_xy is None or tgt_xy is None: return None, None cx, cy = cur_xy; tx, ty = tgt_xy return (tx - cx), (ty - cy) def pid_hold(dx, dy, dt): """오차 기반 짧은 탭 반복으로 보정""" global err_int_x, err_int_y, prev_err_x, prev_err_y ax = dx if abs(dx) > TAP_DEADZONE else 0.0 ay = dy if abs(dy) > TAP_DEADZONE else 0.0 err_int_x += ax * dt; err_int_y += ay * dt derr_x = (ax - prev_err_x) / dt if dt > 0 else 0.0 derr_y = (ay - prev_err_y) / dt if dt > 0 else 0.0 tap_x_ms = (PID_KP*abs(ax) + PID_KI*abs(err_int_x) + PID_KD*abs(derr_x))*18.0 if ax != 0 else 0 tap_y_ms = (PID_KP*abs(ay) + PID_KI*abs(err_int_y) + PID_KD*abs(derr_y))*18.0 if ay != 0 else 0 if ax != 0: short_tap('x', tap_x_ms, 1 if dx > 0 else -1) if ay != 0: short_tap('y', tap_y_ms, 1 if dy > 0 else -1) prev_err_x, prev_err_y = ax, ay # ===================== 커서(.cur) 교체/복구 ===================== _user32 = windll.user32 IMAGE_CURSOR = 2 LR_LOADFROMFILE = 0x0010 SPI_SETCURSORS = 0x0057 OCR_IDS = [ 32512, # IDC_ARROW 32513, # IBEAM 32515, # CROSS 32649, # HAND 32651, # HELP 32650, # APPSTARTING 32648, # NO 32645, # SIZENS 32644, # SIZEWE 32642, # SIZENWSE 32643, # SIZENESW 32646, # SIZEALL 32516, # UPARROW ] def set_system_cursor_to_file(cur_path: str) -> bool: if not os.path.isfile(cur_path): print(f"[Cursor] 파일 없음: {cur_path}") return False _user32.LoadImageW.restype = wintypes.HANDLE hcur = _user32.LoadImageW(None, cur_path, IMAGE_CURSOR, 0, 0, LR_LOADFROMFILE) if not hcur: print("[Cursor] LoadImageW 실패") return False ok_all = True for cid in OCR_IDS: ok = _user32.SetSystemCursor(hcur, cid) if not ok: ok_all = False if ok_all: print("[Cursor] 전역 아이드롭퍼 적용") else: print("[Cursor] 일부 커서 적용 실패") return ok_all def reset_system_cursors(): try: _user32.SystemParametersInfoW(SPI_SETCURSORS, 0, None, 0) print("[Cursor] 시스템 커서 복구") except Exception as e: print("[Cursor] 복구 실패:", e) # 어떤 종료 경로든 복구 보장 atexit.register(reset_system_cursors) def _signal_handler(sig, frame): try: reset_system_cursors() finally: os._exit(0) for _sig in (signal.SIGINT, signal.SIGTERM): try: signal.signal(_sig, _signal_handler) except: pass try: signal.signal(signal.SIGBREAK, _signal_handler) except: pass def request_quit(): """전역 종료 요청 (Qt 이벤트 루프로 안전하게 종료)""" stop_event.set() if gui_app: QtCore.QTimer.singleShot(0, gui_app.quit) # ===================== ROI/좌표 도우미 ===================== def roi_bounding_rect_from_circle(cx, cy, r, mon=None): """원형 ROI의 외접 사각형 (모니터 경계로 클램프)""" if mon is None: mon = MON left = int(cx - r); top = int(cy - r) width = int(2*r); height = int(2*r) left = clip(left, mon['left'], mon['left'] + mon['width'] - 1) top = clip(top, mon['top'], mon['top'] + mon['height'] - 1) if left + width > mon['left'] + mon['width']: width = mon['left'] + mon['width'] - left if top + height > mon['top'] + mon['height']: height = mon['top'] + mon['height'] - top return {'left': left, 'top': top, 'width': width, 'height': height} def point_in_circle_abs(x, y, cx, cy, r): return (x-cx)*(x-cx) + (y-cy)*(y-cy) <= r*r def circular_mask_for_rect(h, w, cx_abs, cy_abs, r, rect): yy, xx = np.ogrid[:h, :w] cx_r = cx_abs - rect['left'] cy_r = cy_abs - rect['top'] dist2 = (xx - cx_r)**2 + (yy - cy_r)**2 return dist2 <= (r*r) # ===================== 입력(마우스 클릭 수집) ===================== def wait_for_left_click(): """전역 마우스 왼클릭 좌표 1회 수집 (pynput Listener 사용)""" pos = {"xy": None} def on_click(x, y, button, pressed): if pressed and button == Button.left: pos["xy"] = (x, y); return False with MouseListener(on_click=on_click) as listener: listener.join() return pos["xy"] def set_roi_circle_by_drag(): """F1: 중심 클릭 → 가장자리 클릭으로 반지름 산출""" print("[ROI] 중심점 클릭...") c = wait_for_left_click() if c is None: print("[ROI] 취소"); return print("[ROI] 가장자리(반지름) 점 클릭...") e = wait_for_left_click() if e is None: print("[ROI] 취소"); return cx, cy = c; ex, ey = e r = int(math.hypot(ex - cx, ey - cy)) with state_lock: ROI_C['cx'], ROI_C['cy'], ROI_C['r'] = cx, cy, max(5, r) print(f"[ROI] Circle center=({ROI_C['cx']},{ROI_C['cy']}), r={ROI_C['r']}") def set_roi_circle_by_input(): """F2: Tk 입력창으로 중심/반지름 직접 지정""" import tkinter as tk from tkinter import simpledialog, messagebox root = None try: root = tk.Tk(); root.withdraw() cx = simpledialog.askinteger("ROI Circle", "Center X", parent=root, minvalue=0) if cx is None: return cy = simpledialog.askinteger("ROI Circle", "Center Y", parent=root, minvalue=0) if cy is None: return r = simpledialog.askinteger("ROI Circle", "Radius", parent=root, minvalue=1) if r is None: return with state_lock: ROI_C['cx'], ROI_C['cy'], ROI_C['r'] = cx, cy, r print(f"[ROI] Circle center=({cx},{cy}), r={r}") except Exception as e: try: messagebox.showerror("ROI 입력 오류", str(e)) except: print("[ROI] 입력 오류:", e) finally: try: if root is not None: root.destroy() except: pass def save_target_from_mouse(mon=None): """F3: 현재 마우스 좌표를 ROI-rect 좌표로 변환하여 target 잠금""" if mon is None: mon = MON mx, my = mouse.position if not point_in_circle_abs(mx, my, ROI_C['cx'], ROI_C['cy'], ROI_C['r']): print("[Target] 원형 ROI 밖"); return None rect = roi_bounding_rect_from_circle(ROI_C['cx'], ROI_C['cy'], ROI_C['r'], mon) tx, ty = mx - rect['left'], my - rect['top'] if 0 <= tx < rect['width'] and 0 <= ty < rect['height']: return (int(tx), int(ty)) return None # ===================== 비전: 원 검출/색상 추적 ===================== def detect_aim_circle(gray): c = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=HOUGH_DP, minDist=HOUGH_MIN_DIST, param1=HOUGH_PARAM1, param2=HOUGH_PARAM2, minRadius=MINR, maxRadius=MAXR) if c is None: return None c = np.uint16(np.around(c))[0] idx = np.argmin(c[:,2]) # 가장 작은 원(게이지) x, y, r = int(c[idx][0]), int(c[idx][1]), int(c[idx][2]) return None if r < RADIUS_MIN_VALID else (x, y, r) def smooth_radius(r): smooth_q.append(r) return sum(smooth_q)/len(smooth_q) if smooth_q else r def pick_color_under_mouse(frame_bgra, rect): """F7: ROI 내부에서 마우스 아래 픽셀 HSV 저장 + seed 지정""" global selected_hsv, seed_xy, last_centroid, target_locked mx, my = mouse.position px = mx - rect['left']; py = my - rect['top'] if not (0 <= px < rect['width'] and 0 <= py < rect['height']): print("[ColorPick] ROI-rect 밖"); return False b, g, r = frame_bgra[py, px][:3] hsv = cv2.cvtColor(np.uint8([[[b, g, r]]]), cv2.COLOR_BGR2HSV)[0][0] selected_hsv = (int(hsv[0]), int(hsv[1]), int(hsv[2])) seed_xy = (int(px), int(py)) last_centroid = None target_locked = True print(f"[ColorPick] HSV={selected_hsv} @ {seed_xy}") return True def detect_color_target_connected(frame_bgra, rect): """시드와 연결된 성분만(원형 ROI 내부 제한) 추적 → 중심(ROI-rect 좌표)""" global last_centroid if selected_hsv is None or seed_xy is None: return None h, w = frame_bgra.shape[:2] mask_circle = circular_mask_for_rect(h, w, ROI_C['cx'], ROI_C['cy'], ROI_C['r'], rect) bgr = frame_bgra[:, :, :3] hsv_img = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) h0, s0, v0 = selected_hsv low = np.array([max(0, h0 - COLOR_TOL_H), max(0, s0 - COLOR_TOL_S), max(0, v0 - COLOR_TOL_V)]) high = np.array([min(179, h0 + COLOR_TOL_H), min(255, s0 + COLOR_TOL_S), min(255, v0 + COLOR_TOL_V)]) mask = cv2.inRange(hsv_img, low, high) mask[~mask_circle] = 0 mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, MORPH_K) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, MORPH_K) num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8) if num_labels <= 1: return None sx, sy = seed_xy if not (0 <= sx < labels.shape[1] and 0 <= sy < labels.shape[0]): return None seed_label = labels[sy, sx] if seed_label == 0: return None if stats[seed_label, cv2.CC_STAT_AREA] < MIN_COLOR_AREA: return None cx, cy = centroids[seed_label]; cx, cy = int(cx), int(cy) if last_centroid is not None and np.hypot(cx - last_centroid[0], cy - last_centroid[1]) > MAX_JUMP_PX: return last_centroid last_centroid = (cx, cy) return last_centroid def autofire_on_minimum(r_s, cur_xy): """반경이 하강→상승으로 바뀌는 극소점 통과 + 조준 오차 허용 → Enter""" global prev_r, prev_dr_sign, last_trigger_t if prev_r is not None: dr = r_s - prev_r sign = 1 if dr > 0 else (-1 if dr < 0 else 0) if prev_dr_sign is not None and prev_dr_sign < 0 and sign > 0: t = time.time() if t - last_trigger_t > MIN_PEAK_GAP and target_pt is not None and target_locked: dx, dy = calc_error(cur_xy, target_pt) if dx is not None and abs(dx) <= AIM_TOLERANCE and abs(dy) <= AIM_TOLERANCE: try: kb.press(Key.enter); kb.release(Key.enter) beep(); print("[AUTO] THROW!") except Exception as e: print("[AUTO] Enter 실패:", e) last_trigger_t = t prev_dr_sign = sign prev_r = r_s def reset_circle_state(): global prev_r, prev_dr_sign, target_pt, target_locked, last_centroid, last_circle_xy, last_radius_s prev_r=None; prev_dr_sign=None target_pt=None; target_locked=False last_centroid=None; last_circle_xy=None; last_radius_s=None # ===================== 전역 단축키(RegisterHotKey) ===================== user32 = ctypes.windll.user32 WM_HOTKEY = 0x0312 MOD_ALT=0x0001; MOD_CTRL=0x0002 VK_F1=0x70;VK_F2=0x71;VK_F3=0x72;VK_F4=0x73 VK_F5=0x74;VK_F6=0x75;VK_F7=0x76;VK_F8=0x77;VK_F9=0x78 VK_Q=0x51 class HotkeyManager: def __init__(self): self.events = queue.Queue() self._running = threading.Event() def _register(self,mod,vk,id_): if not user32.RegisterHotKey(None,id_,mod,vk): raise RuntimeError(f"RegisterHotKey fail: id={id_}") def _unregister_all(self): for i in range(1,20): user32.UnregisterHotKey(None,i) def start(self): self._register(0,VK_F1,1); self._register(0,VK_F2,2) self._register(0,VK_F3,3); self._register(0,VK_F4,4) self._register(0,VK_F5,5); self._register(0,VK_F6,6) self._register(0,VK_F7,7); self._register(0,VK_F8,8) self._register(0,VK_F9,9); self._register(MOD_CTRL|MOD_ALT,VK_Q,10) self._running.set() threading.Thread(target=self._loop,daemon=True).start() print("[Hotkeys] registered") def stop(self): self._running.clear(); self._unregister_all() print("[Hotkeys] unregistered") def _loop(self): msg = wintypes.MSG() while self._running.is_set(): ret = user32.GetMessageW(ctypes.byref(msg),0,0,0) if ret<=0: break if msg.message==WM_HOTKEY: try: self.events.put_nowait(int(msg.wParam)) except: pass user32.TranslateMessage(ctypes.byref(msg)) user32.DispatchMessageW(ctypes.byref(msg)) # ===================== HUD 오버레이 ===================== class Overlay(QtWidgets.QWidget): def __init__(self): super().__init__() self.setWindowTitle("FF7 Darts HUD") self.setWindowFlags( QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.WindowStaysOnTopHint | QtCore.Qt.WindowType.Tool ) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) self.setWindowFlag(QtCore.Qt.WindowType.WindowTransparentForInput, True) self.setWindowFlag(QtCore.Qt.WindowType.WindowDoesNotAcceptFocus, True) screen = QtGui.QGuiApplication.primaryScreen() self.setGeometry(screen.geometry()) self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(16) # 스포이드 PNG 로드 global eyedrop_img if os.path.isfile(EYEDROPPER_PNG): qimg = QtGui.QImage(EYEDROPPER_PNG) if not qimg.isNull(): eyedrop_img = qimg def paintEvent(self, event): painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) with state_lock: cx, cy, r = ROI_C['cx'], ROI_C['cy'], ROI_C['r'] a_on, f_on, c_on = assist_enabled, autofire_enabled, color_track_enabled circle_xy = last_circle_xy radius_s = last_radius_s s_hsv = selected_hsv show_eyed = SHOW_HUD_EYEDROPPER # ROI 원(빨강) painter.setPen(QtGui.QPen(ROI_COLOR, 2)) painter.setBrush(QtCore.Qt.BrushStyle.NoBrush) painter.drawEllipse(QtCore.QPoint(cx, cy), r, r) # 검출 원(녹색) if circle_xy is not None and radius_s is not None: rect = roi_bounding_rect_from_circle(cx, cy, r) ccx = rect['left'] + int(circle_xy[0]) ccy = rect['top'] + int(circle_xy[1]) painter.setPen(QtGui.QPen(QtGui.QColor(0,255,0,220), 2)) painter.drawEllipse(QtCore.QPoint(ccx, ccy), int(radius_s), int(radius_s)) # 좌하단 상태창 self.draw_status_panel(painter, a_on, f_on, c_on, cx, cy, r, show_eyed) # 우하단 Target Shape (선택 HSV 미리보기) if s_hsv is not None: self.draw_target_shape(painter, s_hsv) # HUD 스포이드 (옵션) if show_eyed and eyedrop_img is not None: pos = QtGui.QCursor.pos() x = pos.x() - EYE_HOTSPOT[0]; y = pos.y() - EYE_HOTSPOT[1] painter.drawImage(x, y, eyedrop_img) painter.end() def draw_status_panel(self, painter, assist_on, fire_on, color_on, cx, cy, r, show_hud_cursor): lines = [ f"Assist (WASD): {'ON' if assist_on else 'OFF'}", f"AutoFire(Enter): {'ON' if fire_on else 'OFF'}", f"ColorTrack : {'ON' if color_on else 'OFF'}", f"Target Lock : {'LOCKED' if target_locked else 'OFF'}", f"HUD Eyedrop : {'ON' if show_hud_cursor else 'OFF'} (F9)", f"ROI Center=({cx},{cy}) R={r}", "Hotkeys: F1 SetROI(Click-Click), F2 ROIInput, F3 SetTarget", " F4 Assist, F5 AutoFire, F6 Clear, F7 PickColor, F8 Track, F9 HUDcursor", "Exit: Ctrl+Alt+Q", ] pad = 10; line_h = 22 box_w = 640; box_h = pad*2 + line_h*len(lines) x0, y0 = 20, self.height() - box_h - 20 rect = QtCore.QRect(x0, y0, box_w, box_h) painter.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.transparent)) painter.setBrush(QtGui.QBrush(PANEL_BG)); painter.drawRect(rect) painter.setPen(QtGui.QPen(TARGET_BORDER, 1)); painter.setBrush(QtCore.Qt.BrushStyle.NoBrush); painter.drawRect(rect) painter.setPen(QtGui.QPen(YELLOW)); painter.setFont(QtGui.QFont("Segoe UI", 10)) for i, text in enumerate(lines): painter.drawText(x0+10, y0+pad + (i+1)*line_h, text) def draw_target_shape(self, painter, hsv_color): box_w, box_h = 180, 110 x1 = self.width() - box_w - 20 y1 = self.height() - box_h - 20 rect = QtCore.QRect(x1, y1, box_w, box_h) painter.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.transparent)) painter.setBrush(QtGui.QBrush(PANEL_BG)); painter.drawRect(rect) painter.setPen(QtGui.QPen(TARGET_BORDER, 2)); painter.setBrush(QtCore.Qt.BrushStyle.NoBrush); painter.drawRect(rect) painter.setPen(QtGui.QPen(WHITE)); painter.setFont(QtGui.QFont("Segoe UI", 10)) painter.drawText(x1+10, y1+25, "Target Shape") # HSV → BGR bgr = cv2.cvtColor(np.uint8([[list(hsv_color)]]), cv2.COLOR_HSV2BGR)[0][0].tolist() b, g, r = int(bgr[0]), int(bgr[1]), int(bgr[2]) painter.setPen(QtGui.QPen(WHITE, 2)) painter.setBrush(QtGui.QBrush(QtGui.QColor(r, g, b))) center = QtCore.QPoint(x1 + box_w//2, y1 + box_h//2 + 15) painter.drawEllipse(center, 22, 22) # ===================== 로직 스레드 ===================== def logic_loop(hotkeys: 'HotkeyManager'): """캡처/분석/입력/핫키 처리 — 모두 이 스레드에서 수행 (mss는 여기서 생성!)""" global assist_enabled, autofire_enabled, target_pt, target_locked global prev_time, last_circle_xy, last_radius_s, selected_hsv, seed_xy global color_track_enabled, SHOW_HUD_EYEDROPPER, MON try: # mss 는 스레드 로컬! → 여기서 생성/사용 sct = mss.mss() mon = sct.monitors[1] # 주 모니터 사각형 MON = {'left': mon['left'], 'top': mon['top'], 'width': mon['width'], 'height': mon['height']} print(f"[mss] monitor: {MON}") def capture_roi_rect(): rect = roi_bounding_rect_from_circle(ROI_C['cx'], ROI_C['cy'], ROI_C['r'], MON) return np.array(sct.grab(rect)), rect while running and not stop_event.is_set(): # 1) 전역 단축키 처리 (비블로킹) try: while True: hid = hotkeys.events.get_nowait() if hid == 1: # F1: ROI 드래그식 설정 set_roi_circle_by_drag() smooth_q.clear(); reset_circle_state() elif hid == 2: # F2: ROI 입력 set_roi_circle_by_input() smooth_q.clear(); reset_circle_state() elif hid == 3: # F3: 현재 마우스 지점 타깃 잠금 t = save_target_from_mouse(MON) if t is not None: with state_lock: target_pt = t; target_locked = True; last_centroid = t print(f"[Target] {t} LOCKED") else: print("[Target] 선택 실패(ROI 밖?)") elif hid == 4: # F4: 보정 토글 assist_enabled = not assist_enabled print("[Assist]", "ON" if assist_enabled else "OFF") elif hid == 5: # F5: 자동발사 토글 autofire_enabled = not autofire_enabled print("[AutoFire]", "ON" if autofire_enabled else "OFF") elif hid == 6: # F6: 타깃/추적 클리어 target_pt=None; target_locked=False; seed_xy=None; selected_hsv=None; last_centroid=None print("[Target] cleared") elif hid == 7: # F7: 색상 픽 frame, rect = capture_roi_rect() # ROI 내부 클릭 검증 mx, my = mouse.position if point_in_circle_abs(mx, my, ROI_C['cx'], ROI_C['cy'], ROI_C['r']): if pick_color_under_mouse(frame, rect): color_track_enabled = True print("[ColorTrack] enabled & locked") else: print("[ColorPick] ROI 밖") elif hid == 8: # F8: 색상 추적 토글 color_track_enabled = not color_track_enabled print("[ColorTrack]", "ON" if color_track_enabled else "OFF") elif hid == 9: # F9: HUD 스포이드 PNG 토글 SHOW_HUD_EYEDROPPER = not SHOW_HUD_EYEDROPPER print("[HUD Eyedrop]", "ON" if SHOW_HUD_EYEDROPPER else "OFF") elif hid == 10: # Ctrl+Alt+Q: 종료 request_quit() except queue.Empty: pass # 2) 프레임 캡처 & ROI 마스킹 frame, rect = capture_roi_rect() # BGRA gray = cv2.cvtColor(frame, cv2.COLOR_BGRA2GRAY) mask = circular_mask_for_rect(rect['height'], rect['width'], ROI_C['cx'], ROI_C['cy'], ROI_C['r'], rect) gray[~mask] = 0 # 3) 원(게이지) 탐지 gray_blur = cv2.GaussianBlur(gray, (5,5), 1.2) c = detect_aim_circle(gray_blur) if c is None: with state_lock: last_circle_xy = None; last_radius_s = None time.sleep(0.006); continue cx, cy, rdet = c r_s = smooth_radius(rdet) with state_lock: last_circle_xy = (cx, cy) last_radius_s = r_s # 4) 색상 추적으로 타깃 갱신(있으면) if color_track_enabled and selected_hsv is not None: pt = detect_color_target_connected(frame, rect) if pt is not None: with state_lock: target_pt = pt; target_locked = True # 5) 자동 보정(WASD 탭) dt = time.time() - prev_time; prev_time = time.time() if assist_enabled and target_pt is not None and target_locked: dx, dy = calc_error((cx, cy), target_pt) if dx is not None and (abs(dx) > AIM_TOLERANCE or abs(dy) > AIM_TOLERANCE): pid_hold(dx, dy, dt) # 6) 자동 발사(최소 반경 통과) if autofire_enabled: autofire_on_minimum(r_s, (cx, cy)) time.sleep(0.006) except Exception as e: print("[logic_loop] crash:", e) log_ex(e) stop_event.set() # ===================== 메인 ===================== def main(): global gui_app, MON, ROI_C # 시스템 커서 아이드롭퍼 적용(실패 시 HUD 스포이드로 대체) applied = False if USE_SYSTEM_CURSOR: applied = set_system_cursor_to_file(EYEDROPPER_CUR) if not applied: print("[Cursor] 시스템 커서 교체 실패 → HUD 스포이드로 대체 표시") # SHOW_HUD_EYEDROPPER = True # 필요 시 자동 표시 # Qt 앱/스크린 지오메트리로 ROI 초기값 보정 app = QtWidgets.QApplication(sys.argv) gui_app = app scr = QtGui.QGuiApplication.primaryScreen().geometry() MON = {'left': scr.left(), 'top': scr.top(), 'width': scr.width(), 'height': scr.height()} ROI_C = { 'cx': MON['left'] + MON['width']//2, 'cy': MON['top'] + MON['height']//2, 'r': min(MON['width'], MON['height'])//3 } # 전역 단축키 시작 hotkeys = HotkeyManager() try: hotkeys.start() except Exception as e: print("[Hotkeys] start fail:", e) # HUD 시작 overlay = Overlay() overlay.showFullScreen() # 로직 스레드 시작 t = threading.Thread(target=logic_loop, args=(hotkeys,), daemon=True) t.start() # Qt 루프 ret = 0 try: ret = app.exec() finally: # 종료 정리 stop_event.set() try: t.join(timeout=1.0) except: pass try: hotkeys.stop() except: pass reset_system_cursors() sys.exit(ret) if __name__ == "__main__": main() 이게 chat gpt한테 물어물어가며 만든 코드입데 기능은 작동하는 것 같은데 문제는 Q나 Ctrl+Alt+Q를 누르면 정상적으로 종료가 되어야 할 툴이 강제 종료를 해야만 종료가 되고요. 정상적으로 종료하지 않을 시 마우스 포인터로 지정했던 eyedropper 이미지로 변한 마우스 커서가 원래의 마우스 커서로 돌아오지 않는다는 문제가 있고요. 오버레이로 툴이 켜지긴 하나 기능키들이 이 툴에 우선권으로 들어가지 않는다는 문제가 있습니다. 어떻게 해결해야 할까요? 급합니다.
|
로스트아크 인벤 자유 게시판 게시판
인벤 전광판
[더워요33] 무적007은 부활할 것이다.
[규듀기] 여름 신캐는 남자 창술사
[전국절제협회] 소멸의 왕, 절제가 하늘에 서겠다.
로아 인벤 전광판 시작!!





