자유 게시판

전체보기

모바일 상단 메뉴

본문 페이지

[잡담] 코딩 잘 아시는 분 계신가요? 급합니다.

장산타이거
댓글: 12 개
조회: 241
2025-09-27 12:50:14
파판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 이미지로 변한 마우스 커서가 원래의 마우스 커서로 돌아오지 않는다는 문제가 있고요.
오버레이로 툴이 켜지긴 하나 기능키들이 이 툴에 우선권으로 들어가지 않는다는 문제가 있습니다.

어떻게 해결해야 할까요? 급합니다.

모바일 게시판 하단버튼

댓글

새로고침
새로고침

모바일 게시판 하단버튼

지금 뜨는 인벤

더보기+

모바일 게시판 리스트

모바일 게시판 하단버튼

글쓰기

모바일 게시판 페이징

최근 HOT한 콘텐츠

  • 로아
  • 게임
  • IT
  • 유머
  • 연예
AD