
실시간 DPS 미터를 구축하기 위한 MMO Aion 2 네트워크 프로토콜 역공학 (Rust + Tauri)
요약
MMO Aion 2의 실시간 DPS 미터를 구축하기 위해 Rust와 Tauri를 사용하여 네트워크 프로토콜을 역공학한 기술 보고서입니다. 패킷 캡처를 통해 게임 클라이언트에 영향을 주지 않고 전투 데이터를 분석하는 아키텍처와 엔티티 식별 문제를 다룹니다.
핵심 포인트
- Rust 백엔드와 Tauri v2를 활용한 데스크톱 앱 아키텍처 구축
- npcap을 이용한 TCP 트래픽 수동 캡처 및 바이너리 프로토콜 파싱
- 패치 대응을 위한 효율적인 트레이싱(tracing) 시스템 설계
- 가변적인 엔티티 ID 식별 문제 해결을 위한 데이터 모델링
공지: 이 글은 저의 개인 사이드 프로젝트인 AION 2용 전투 분석 도구에 관한 기술 보고서입니다. 게임 퍼블리셔와는 아무런 관련이 없습니다. 링크는 마지막에 있습니다.
아키텍처 (Architecture)
Windows 데스크톱 앱: Rust 백엔드 + Tauri v2 웹뷰 UI입니다. 이 앱은 게임의 TCP 트래픽을 수동적으로 캡처(npcap)하고, 스트림을 재조립하며, 게임의 문서화되지 않은 바이너리 프로토콜 (binary protocol)을 파싱합니다. 그 후 전투 모델(데미지, 힐, 버프, 사망, 보스 감지)에 데이터를 공급하고, 집계된 데이터를 작은 JS 프론트엔드로 푸시합니다. 게임 클라이언트에 어떠한 영향도 주지 않습니다 — 인젝션(injection)이나 메모리 읽기(memory reading)를 수행하지 않습니다. 패킷에 명시되어 있지 않다면, 우리는 알 수 없습니다.
고충 #1: 프로토콜은 움직이는 타겟이다
아무도 당신에게 명세서(spec)를 건네주지 않습니다. 프로토콜은 가변 길이 정수(varint)가 많고, 부분적으로 압축되어 있으며, 게임 패치에 따라 변경됩니다. 결국 당신은 패킷 고고학(packet archaeology)을 하게 됩니다: 전투를 캡처하고, 헥스 덤프(hex dumps)를 응시하며, "19:32:04에 이 스킬을 사용했다"는 사실을 바이트 패턴과 상관시키고, 파서를 구축합니다. 그리고 — 가장 즐거운 부분인 — 매 패치 이후에도 이를 유지해야 합니다. 보통 사용자들의 레이드(raid)가 다음 주가 아니라 오늘 밤이기 때문에, 몇 시간 내에 차이점(diff)을 역공학(reverse-engineering)해야 합니다.
어렵게 얻은 교훈 하나: 토글 가능한 추적 카테고리(trace categories) 뒤에 _모든 것_을 로그로 남기세요. 우리의 트레이싱(tracing) 설정은 비활성화되었을 때 핫 패스(hot-path) 로그 호출 지점의 비용을 말 그대로 제로(zero)로 유지합니다 (Rust tracing 라이브러리와 Interest::never() + 카테고리별 원자적 플래그 사용). 따라서 사용자가 "Trace: Packets" 체크박스를 선택하여 버그를 재현하고, 우리가 실제로 필요한 바이트가 포함된 로그를 보낼 수 있습니다.
고충 #2: 엔티티 식별(entity identity)은 거짓이다
가장 어려웠던 단일 정확성 문제는 파싱이 아니라 _식별(identity)_이었습니다. 플레이어는 하나의 ID가 아닙:
- 당신의 캐릭터는 버프 바(buff bar)를 운반하는 안정적인 "소유자 (owner)" 엔티티를 가집니다.
- 당신의 **데미지 (damage)**는 풀(pull) 사이에서 ID가 변경되는 일시적인 전투 엔티티(transient combat entity) 아래에 기록됩니다.
- 던전을 떠나면 게임은 당신을 완전히 새로운 ID로 다시 바인딩(re-bind)합니다.
- 이름(name)은 데미지와 다른 패킷을 통해 전달되며, 때로는 몇 초 뒤에, 때로는 전혀 전달되지 않기도 합니다 (전투 중간에 앱을 시작한 경우).
이 중 하나라도 잘못되면 힐러의 치유량이 유령 행(ghost row)에 기록되거나, 보스가 죽고 7분 뒤에 ID가 재사용되었다는 이유로 플레이어의 버프가 저장된 전투 기록에서 사라집니다 (네, 실제로 정확히 그런 버그를 겪었습니다 — 던전 종료 후의 재바인딩이 이미 저장된 전투 기록을 손상된 데이터로 조용히 덮어씌웠습니다). 해결책은 정규화(canonicalization) 레이어입니다. 모든 ID는 소환 해결(summon-resolution) → 고아 재매핑(orphan remapping) → 닉네임 → 정규 ID(canonical ID) 과정을 거쳐 통합되어야 하며, ID가 재바인딩될 때 모든 저장소 마이그레이션(데미지, 버프, 힐, 장비 점수)이 함께 이동해야 합니다.
고통 #3: 가공되지 않은 DPS는 불공평하므로, rDPS를 구축했습니다
가공되지 않은 데미지 수치는 서포트 클래스를 보이지 않게 만듭니다. 만약 버퍼의 파티 전체 버프가 모든 사람의 데미지를 12% 증폭시킨다면, 그 데미지를 누가 "획득"한 것일까요? 우리는 rDPS라고 부르는 기여 모델을 구현했습니다: 당신의 버프/디버프에 의해 추가된 데미지는 _당신_에게 귀속되고, 단순히 그것을 받은 플레이어들에게서는 차감됩니다 (rDPS = 당신의 데미지 − 받은 증폭 + 제공한 증폭). 구현을 어렵게 만드는 세부 사항들은 다음과 같습니다:
- 동일한 클래스의 두 플레이어가 유지하는 중첩되지 않는 디버프는 승자 독식 방식이 아니라 업타임(uptime)에 따라 분할되어야 합니다.
- 일부 디버프는 보스에게 보이는 디버프 형태가 아니라 시전자의 셀프 버프(self-buffs) 형태로 전달됩니다.
- 버프 레벨은 스킬 변형 ID(skill variant ID)의 중간 자릿수에 인코딩되어 있습니다 — 인코딩되어 있는 경우에만 해당하며, 패시브 스킬은 그렇지 않습니다.
고통 #4: 힐은 대부분 오버힐(overheal)입니다
힐 패킷은 시전량(cast amount)을 알려줄 뿐, 무엇이 중요했는지는 알려주지 않습니다. 우리는 대상의 실제 HP 변화량(delta)과 각 힐을 대조하여 확인합니다 (3초 창 내에서의 FIFO 매칭): 커버된 부분은 _유효한 치유(effective healing)_이며, 나머지는 오버힐(overheal)로서 점수를 얻지 못합니다. 이로 인해 갑자기 힐러 순위가 의미를 갖게 됩니다.
보너스로 따라오는 프론트엔드 고충, 언제나 하나씩은 있기 마련이죠
CSS의 text-overflow: ellipsis는 텍스트를 전체 글리프(glyph) 단위로 자르지만, 박스의 전체 flex 너비는 그대로 유지합니다. CJK(한중일) 닉네임의 경우, 이로 인해 "..."와 다음 요소 사이에 최대 한 글자(약 16px) 정도의 데드 존(dead zone)이 발생합니다. 레이아웃에 무작위로 구멍이 난 것처럼 보이게 되죠. 해결 방법은 JS에서 canvas의 measureText를 사용하여 _문자열(string)_의 길이를 측정하고 잘라내는 것이었습니다. 그래야 박스가 보이는 텍스트에 딱 맞게 붙습니다. 16픽셀을 잡으려고 내 인생의 2시간을 썼네요.
현재 모습
이 미터기는 커뮤니티 리더보드 사이트로 데이터를 전송하며, 해당 사이트에는 검증된 보스 처치 기록이 업로드되어 클래스/서버별로 순위가 매겨집니다. 이때 플레이어의 스킬, 특성(specs), 장비, 칭호 등 전체 빌드(build)의 실제 스냅샷이 함께 제공됩니다. 이 기술 스택은 여러 번의 게임 패치를 견뎌냈으며, 지역별 게임 버전과 10개의 UI 언어를 모두 지원합니다.
궁금하신 분들을 위해: 미터기와 리더보드는 aion2t.com에서 확인하실 수 있습니다. 패킷 역공학(packet reverse-engineering), Tauri v2의 특이사항(quirks), 또는 rDPS 모델에 대한 질문은 댓글로 남겨주시면 기꺼이 답변해 드리겠습니다. AI 활용 측면에서는 Claude Code, Mimo, Deepseek, 그리고 Grok을 약간 사용했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기