1편에서 D2R 메모리를 읽고, 2편에서 투명 오버레이 창을 만들었다. 이제 실제로 화면에 무언가를 그릴 차례다. 몬스터 등급별 마커, 워프포인트와 출구 표시, 그리고 아직 방문하지 않은 방의 오브젝트까지 감지하는 PresetUnit 개념이 이번 편의 핵심이다. 좌하단 미니맵과 화면 가장자리 방향 화살표까지 구현하면 Phase 1 맵핵이 완성된다.
01 무엇을 표시할 것인가
정보 밀도가 높은 오버레이에서 빠르게 구분하려면 색상과 모양을 동시에 다르게 해야 한다. 색상만 다르면 색맹 환경에서 구분이 어렵고, 모양만 다르면 직관성이 떨어진다.
오브젝트 종류별로도 마커를 구분한다:
-
워프포인트(WP) 하늘색 다이아몬드 + "WP" 레이블 — 게임에서 가장 중요한 오브젝트
-
출구 (Exit) 이전 구역은 녹색, 다음 구역은 주황색 다이아몬드 + 목적지 이름
-
포탈 · 슈라인 · 상자 각각 보라, 초록, 금색 도형으로 구분
02 오브젝트 분류 — txtFileNo 기반
D2R의 모든 오브젝트는 txtFileNo(텍스트 데이터 파일 번호)로 종류를 구분한다.
워프포인트, 슈라인, 상자에 해당하는 번호를 Set으로 관리하고,
UnitAny에서 읽은 값을 조회해 분류한다.
# Act별로 WP txtFileNo가 다르다 — 각 Act의 오브젝트가 별도 항목으로 정의되기 때문 WAYPOINT_IDS = {119, 145, 237, 324, 398, 402, 429, 494, 496, 511} SHRINE_IDS = {2, 81, 82, 83, 84, 85, 86, 176, 177, 178} CHEST_IDS = {3, 4, 5, 6, 50, 51, 79, 80, 267, 268} def classify_object(txt_file_no: int) -> str: if txt_file_no in WAYPOINT_IDS: return "waypoint" if txt_file_no in SHRINE_IDS: return "shrine" if txt_file_no in CHEST_IDS: return "chest" return "other"
이 ID 테이블은 처음부터 직접 역공학한 것이 아니다. 오픈소스 D2R 맵뷰어(d2r-mapview, D2RMH) 프로젝트에서 검증된 값을 참고했다.
03 몬스터 등급 판별
MonsterData 구조체의 flag 바이트 하나로 등급을 결정한다.
비트 플래그 방식이라 AND 연산으로 각 등급을 독립적으로 확인할 수 있다.
# union_ptr + 0x1A 위치의 1바이트 플래그 flag = read_uint8(union_ptr + 0x1A) is_champion = bool(flag & 0x02) is_unique = bool(flag & 0x04) is_super_unique = bool(flag & 0x08) # 퀘스트 보스는 txtFileNo로 판별 BOSS_IDS = {156, 211, 242, 243, 544, 570} # Andariel=156, Duriel=211, Mephisto=242, Diablo=243, Baal=544 is_boss = txt_file_no in BOSS_IDS
유니크와 보스 옆에는 레벨과 면역 정보도 텍스트로 표시한다. 레벨은 StatList에서 stat ID=12를 읽고, 각 저항 stat(불=39, 번개=41, 냉기=43)이 100 이상이면 면역으로 판단한다.
★ L87 F/C ← 87레벨, 불꽃/냉기 면역 슈퍼유니크 ★ L92 L ← 92레벨, 번개 면역 퀘스트 보스
보스와 슈퍼유니크 주변에는 QRadialGradient로 빛나는 글로우 효과를 추가한다. 눈에 잘 띄게 하는 것이 목적이므로 보스일수록 글로우 반경을 크게 잡는다.
04 등급별 마커 그리기 — QPainter
| 등급 | 도형 | 색상 | 크기 |
|---|---|---|---|
| 일반 몬스터 | 원 (circle) | #e74c3c 빨간색 | r = 4px |
| 챔피언 | 다이아몬드 (diamond) | #3498db 파란색 | s = 6px |
| 유니크 | 삼각형 (triangle) | #e67e22 주황색 | s = 7px |
| 슈퍼유니크 | 별 + 글로우 | #f1c40f 금색 | r = 8px |
| 퀘스트 보스 | 별 + 강한 글로우 | #e74c3c 빨간색 | r = 10px |
def _draw_object(self, painter, obj, px, py): sx, sy = self._g2s(obj.x, obj.y, px, py) cat = classify_object(obj.txt_file_no) if cat == "waypoint": self._draw_diamond(painter, sx, sy, 9, CYAN, "WP") elif cat == "shrine": self._draw_star(painter, sx, sy, 6, GREEN) elif cat == "chest": self._draw_square(painter, sx, sy, 5, GOLD) def _draw_diamond(self, painter, x, y, s, color, label=""): path = QPainterPath() path.moveTo(x, y - s) path.lineTo(x + s, y ) path.lineTo(x, y + s) path.lineTo(x - s, y ) path.closeSubpath() painter.drawPath(path)
05 PresetUnit — 미방문 방의 오브젝트도 감지하기
이 섹션이 3편의 기술적 핵심이다.
문제 — HashTable의 한계
1편에서 설명한 HashTable에는 플레이어가 실제로 방문한 방의 유닛만 올라온다. 워프포인트가 아직 가보지 않은 방에 있으면 HashTable에서 찾을 수 없다. 맵핵의 핵심 기능인 "전체 맵 미리 보기"가 이 방법으로는 불가능하다.
PresetUnit 링크드 리스트가 있다.
Room2는 방문 여부와 관계없이 순회할 수 있으므로,
PresetUnit을 통해 던전 전체의 고정 오브젝트를 미리 파악할 수 있다.
출구(Exit) 감지와 목적지 판별
PresetUnit에서 unit_type == 5인 항목이 타일 출구다.
출구의 목적지 Area는 인접한 Room2들의 Level을 확인해 현재 Area와 다른 것을 찾으면 된다.
def _find_dest_area(self, room2: int, current_area: int) -> int: # 인접 Room2들의 Level을 순회 → 현재 Area와 다른 Area ID 반환 for near_r2 in self._get_near_rooms(room2): near_area = self._read_area_id(near_r2) if near_area != current_area: return near_area return 0
목적지 Area ID를 알면 출구에 목적지 이름을 표시할 수 있다. 현재 Area보다 ID가 낮으면 이전 구역(녹색), 높으면 다음 구역(주황색)으로 색상을 구분해 방향성을 직관적으로 전달한다.
06 화면 가장자리 방향 화살표
화면 밖에 있는 출구는 화면 경계에 화살표로 방향과 거리를 표시한다. 화살표 위치는 화면 중심에서 목표까지의 방향벡터를 화면 경계에 닿는 지점까지만 스케일하면 된다.
import math # 화면 중심 기준 목표까지의 방향벡터 dx = target_sx - center_x dy = target_sy - center_y # 화면 경계까지의 스케일 계산 scale_x = (screen_w / 2 - margin) / abs(dx) if dx else float("inf") scale_y = (screen_h / 2 - margin) / abs(dy) if dy else float("inf") scale = min(scale_x, scale_y, 1.0) # 이미 화면 안이면 1.0 이하 arrow_x = center_x + dx * scale arrow_y = center_y + dy * scale # 화살표 방향 각도 → 삼각형 3꼭지점 계산 angle = math.atan2(dy, dx)
07 좌하단 미니맵
현재 Area 전체를 200×180 픽셀 박스에 축소해 표시한다. 전체 방 좌표 범위를 계산하고, 그 범위를 박스 크기에 맞게 이소메트릭 스케일을 적용한다.
# 전체 방 범위의 이소메트릭 투영 크기 계산 iso_w = (map_w + map_h) * 0.707 iso_h = (map_w + map_h) * 0.5 # 미니맵 박스에 맞게 스케일 결정 scale = min(mm_w / iso_w, mm_h / iso_h) * 0.7
방 색상 코딩
미니맵의 각 방을 내용물에 따라 색상으로 구분한다. 방 안에 여러 오브젝트가 있을 경우 우선순위가 높은 것의 색상을 사용한다.
우선순위 순서는 퀘스트 > 출구 > 워프포인트 > 포탈 > 상자 > 슈라인이다. 그 위에 WP, 출구, 포탈 마커를 작은 크기로 한 번 더 덮어 그려 미니맵만 봐도 중요 오브젝트 위치를 즉시 파악할 수 있게 한다.
08 Phase 1 완성 — 맵핵의 전체 그림
3편에 걸쳐 구현한 내용을 정리하면 다음과 같다.
pymem · 패턴스캔
PyQt6 · Win32
PresetUnit · 미니맵
이 상태만으로도 실제 플레이에서 상당한 이점이 생긴다. 몬스터 등급을 미리 파악하고, 방문하지 않은 방의 워프포인트와 보스 위치를 던전에 입장하는 순간부터 알 수 있다.
Phase 2에서는 이 데이터를 바탕으로 실제 봇 동작을 구현한다. 워프포인트로의 자동 이동, 몬스터 탐지 후 공격 루틴, 아이템 픽업 자동화가 다음 목표다.
txtFileNo 기반 오브젝트 분류와 MonsterData flag 비트 연산으로
게임 내 모든 오브젝트를 종류별로 구분해 마커를 그렸다.
이번 편의 핵심인 PresetUnit을 통해 방문하지 않은 방의 워프포인트와 출구를
던전 입장 즉시 감지할 수 있게 됐다.
좌하단 미니맵과 화면 가장자리 화살표까지 더해지면서 Phase 1 맵핵이 완성됐다.
'비전공자의 바이브 코딩 > 일상 & 기초' 카테고리의 다른 글
| 유튜브 AI 요약기를 1시간 만에 만들었다— 편향 감지 + 카테고리 분류 + 히스토리, API 비용 0원 (0) | 2026.06.14 |
|---|---|
| 쿠팡 최저가 알림 봇 만들기 — Claude Code로 1시간 완성 (네이버 쇼핑 API) (1) | 2026.06.09 |
| 디아블로2 봇 만들기 #2— (맵핵 구현)미니맵 오버레이 구현 (0) | 2026.06.03 |
| 디아블로2 봇 만들기 #1— (맵핵구현)D2R 게임 메모리 구조 분석 (0) | 2026.06.03 |
| 클로드코드(Claude Code)로 코인 자동매매 프로그램 만들기 : #4. 마무리, 계획 (1) | 2026.05.16 |