11년의 집념: DOS판 Wing Commander를 브라우저 협동 플레이로 되살린 리버스 엔지니어링 이야기

한눈에 보기
어린 시절 함께 즐기던 고전 게임을 친구와 같이 플레이하고 싶어서 원본 바이너리를 직접 파고든 사람들이 있습니다. Wing Commander Together는 1990년 DOS판 Wing Commander에서 두 사람이 각자 전투기를 조종하며 편대원으로 협동할 수 있게 만든 프로젝트입니다. 2015년에 시작해 2026년에 브라우저 버전으로 완성되기까지 약 11년이 걸렸습니다.
- 메모리 비교와 코드 추적으로 위치, 방향, 전투 동작이 저장된 위치를 찾아냈습니다.
- 수정한 DOSBox에서 원본 게임 함수를 직접 호출해 레이저 발사와 피해 처리를 구현했습니다.
- 두 게임 인스턴스 사이에서 기체 상태, 이벤트, 임무 진행 정보를 동기화합니다.
- 2026년에는 WebRTC와 Emscripten을 이용해 브라우저에서 방 코드만 입력하면 접속하는 방식으로 바뀌었습니다.
- GOG 설치 파일을 브라우저에 드래그 앤드 드롭하면 바로 플레이할 수 있고, 문자 채팅과 선택형 음성 채팅도 지원합니다.
출발점: 한 대의 전투기를 둘이 몰던 기억
어릴 때 형제는 전투기 한 대를 나눠서 조종했습니다. 한 명은 조종을 맡고, 다른 한 명은 레이저와 애프터버너를 맡는 식이었죠. 이 프로젝트의 목표는 조작을 나누는 대신 각자 자기 기체를 몰고 편대원으로 함께 싸우는 것이었습니다.
2015년: 메모리에서 좌표 찾기
2015년 10월, DOSBox로 게임을 하던 중 이런 생각이 떠올랐습니다. '플레이어와 편대원의 X/Y/Z 좌표가 메모리 어디에 있는지 알면, 네트워크 패킷으로 편대원을 움직일 수 있지 않을까?'
탐색 방법
- DOS에서 주소를 지정할 수 있는 메모리는 약 1MB이고, Wing Commander는 게임 상태를 64KB 데이터 세그먼트에 저장합니다.
- 정지 상태 스냅샷 두 개와 직진한 뒤의 스냅샷을 비교해 "정지 중에는 그대로이고, 움직이면 바뀌는" 바이트를 골라냈습니다.
- 변수를 줄이려고 함께 움직이는 NPC가 없는 임무를 고르고, 마우스를 꺼서 회전을 막은 뒤
MEMDUMPBIN명령으로 메모리를 덤프했습니다. - 변경된 영역 35곳을 눈으로 살펴 약 12곳으로 줄였고, 결국 **위치 주소
13D3:A9C2**를 찾았습니다. SM명령으로 이 값을 바꾸자 기체가 실제로 순간이동했습니다. 정지하면 0이 되는 32비트 정수 3개를 단서로 **속도 벡터(CAE2)**도 찾았습니다.
참고로 "640K면 누구에게나 충분하다"는 빌 게이츠의 말은 잘못 알려진 인용이라고 합니다.
2016년: 방향 벡터에서 메인 루프까지
방향 정보는 위치보다 훨씬 찾기 어려웠습니다. 그래서 스냅샷을 자동으로 비교하는 도구를 따로 만들었습니다.
- 객체 정보는 64개 슬롯 배열(슬롯당 12바이트)에 들어 있습니다. 플레이어는 슬롯
0, 카메라는0x3D, 임시 값은0x3F를 씁니다. - 방향 벡터의 각 성분은 256을 1.0으로 보는 4바이트 고정소수점 정수입니다. 그래서 작은 회전에는 한 바이트만 바뀌어 눈에 잘 띄지 않았습니다.
- 결정적 단서는 기체를 롤 회전시켰을 때 부호 확장 때문에 인접한 3바이트가
0x00에서0xff로 한꺼번에 바뀐 것이었습니다. - 찾은 주소에 메모리 감시점을 걸어 벡터 덧셈 함수 → 기체별 물리 함수 → 프레임 함수 → 메인 루프 순서로 호출 경로를 거슬러 올라갔고, 이후 IDA에서 코드에 주석을 달며 분석을 이어갔습니다.
원본 함수를 호출하는 '트램펄린'
메모리 값만 바꿔서는 레이저나 열추적 미사일을 만들 수 없었습니다. 결국 게임의 원래 함수를 직접 호출해야 했는데, 여기서 Borland 오버레이가 걸림돌이 됐습니다. 함수가 여러 오버레이에 흩어져 있고, 그때까지의 실행 흐름에 따라 서로 다른 메모리 위치에 동적으로 로드되기 때문입니다.
해결 방법
- 게임 안에서 쓰이지 않는 문자열 공간(처음에는
Loading WING COMMANDER, 나중에는 오류 메시지 영역)에 트램펄린 코드를 넣었습니다. - DOSBox가
CS:IP를 스택에 넣고 트램펄린으로 점프하면, 트램펄린이 레지스터와 플래그를 보존한 채 스텁을 거쳐 원거리 호출(far call)을 하고retf로 원래 자리로 돌아옵니다. - 이렇게 해서 레이저 발사에 성공했습니다.
- 2017년에는 메인 루프 안의 안정적인 훅 지점(
0560:20E3)을 찾았고, 보조 플레이어 쪽에서는do_damage호출을 막아 피해가 두 번 계산되지 않게 했습니다.
2017~2018년: 네트워크 동기화와 객체 ID 문제
- TCP 클라이언트/서버 구조와 protobuf로 프로토콜을 만들었습니다.
- 주도(host) 인스턴스는 매 프레임 모든 기체의 위치, 발사, 객체 제거, 피해 이벤트를 보냅니다.
- 클라이언트는 자기 위치, 레이저 발사 여부, 탈출/착륙/사망 이벤트를 돌려보냅니다.
- 각 플레이어 입장에서 자기 기체는 항상 객체 0, 상대 기체는 객체 1이라서 패킷을 받을 때마다 번호를 서로 바꿔야 했습니다.
원인 불명의 크래시
2017년 말 몇 달 동안 불규칙한 크래시가 이어졌습니다. 원인은 Borland 오버레이 로더였습니다. 로더는 스택의 BP 프레임 체인을 따라가며 반환 주소를 고치는데, 직접 만든 스텁에는 정상적인 프레임이 없어서 엉뚱한 값을 읽고 있었던 겁니다. 해결책은 다음과 같았습니다.
- 트램펄린에 정상적인 Borland 함수 프레임을 만들고 그 안에
nop을 둡니다. - DOSBox가 프로그램 카운터가
nop에 도달한 것을 감지하면 큐에 있는 다음 함수 호출을 주입합니다. - 큐가 빌 때까지 이 과정을 반복합니다.
객체 슬롯 매핑
발사체처럼 잠깐 생겼다 사라지는 객체는 두 인스턴스에서 서로 다른 슬롯 번호로 만들어질 수 있습니다. 처음에는 슬롯을 12~15번으로 강제로 맞췄지만, 이후 서버 ID와 로컬 ID를 대응시키는 매핑 테이블을 도입했습니다. 이 덕분에 소행성도 멀티플레이에서 제대로 동작하게 됐습니다. 2018년에는 폭발 애니메이션, 미사일, 자동 항법을 구현했습니다.
2019~2020년: 임무 진행과 게임 내 채팅
- 임무 승리 판정이 불안정한 문제를 해결하려고 데이터 세그먼트를 이분 탐색했습니다. 64KB 전체를 네트워크로 복사한 뒤 복사 범위를 계속 반으로 줄여서
13D3:D248에 있는 80바이트짜리 임무 트리를 찾아냈습니다. - 승리 점수 카운터(
13D3:C280)와 임무 번호(C255,C256)도 발견했고, 이는 나중에 임무 선택 드롭다운 기능이 됐습니다. - 통신 문구를 띄우는 함수를 찾아서, 입력한 메시지가 상대방 게임의 편대원 VDU 화면에 뜨도록 연결했습니다. 원작의 통신 화면 그대로 대화할 수 있게 된 거죠.
2020년에 플레이 가능한 상태가 됐지만, 친구와 같이 하려면 게임 사본 구하기, 수정된 DOSBox 빌드, 공인 IP 공유, 방화벽 설정까지 거쳐야 했습니다. 너무 번거로워서 가족끼리 몇 번 즐긴 뒤로는 사실상 멈춰 있었습니다.
2026년: 브라우저와 방 코드로 다시 태어나다
프로젝트를 다시 움직인 건 **WebRTC의 보급, 안정된 Emscripten 지원, 그리고 새로 만든 lobbylink**였습니다.
lobbylink란?
- WebSocket 라이브러리 하나에만 의존하는 Go 기반 로비 서버로, 작은 서버에서 P2P WebRTC 연결을 중개합니다.
- 같은 방 코드를 입력한 참가자끼리 연결해 주고, 출처(origin)는 정적 허용 목록으로 제한합니다.
- coturn을 함께 실행해 TURN 자격 증명을 발급하기 때문에, 방화벽이 까다로운 환경에서도 릴레이로 연결할 수 있습니다.
LLM의 도움
Emscripten 빌드 스크립트 구성, GOG의 Inno Setup 설치 파일에서 게임 데이터를 추출하는 작업, 실시간 음성 채팅 추가(두 번째 WebRTC 피어 연결로 오디오 전송)에는 LLM을 활용했다고 합니다.
지금은 이렇게 플레이합니다
- GOG에서 Wing Commander를 구매해 내려받습니다.
- 설치 파일을 브라우저에 드래그 앤드 드롭합니다. 데이터는 클라이언트 측 저장소에 보관됩니다.
- 친구와 같은 방 코드를 입력하면 바로 함께 출격할 수 있습니다.
브라우저 플레이 페이지, 수정된 에뮬레이터 코드, 플레이 영상이 모두 공개돼 있습니다.
에디터의 한마디
이 프로젝트에서 눈에 띄는 점은 소스 코드 없이 원본 바이너리를 존중하는 방식으로 멀티플레이를 붙였다는 것입니다. 게임 로직을 새로 짜지 않고 원래 함수를 불러 쓰는 트램펄린, 오버레이 로더의 동작까지 맞춰 준 스택 프레임, 이분 탐색으로 찾아낸 임무 트리까지, 리버스 엔지니어링의 고전적인 기법이 단계별로 잘 드러납니다.
또 하나 흥미로운 부분은 2020년에 거의 완성돼 있던 프로젝트를 실제로 쓸 수 있게 만든 것이 2026년의 웹 기술과 LLM이었다는 점입니다. 어려운 핵심 작업은 사람이 10년 동안 해냈고, 배포와 접근성이라는 마지막 단계를 최신 도구가 해결한 셈입니다. 레트로 게임, 에뮬레이터, 리버스 엔지니어링에 관심 있는 분이라면 원문과 코드를 꼭 살펴보시길 권합니다.
원문: https://news.hada.io/topic?id=36643&utm_source=geeknews&utm_medium=rss&utm_campaign=news