W3P 프로토콜
런타임과 외부 프로그램 사이의 모든 계약입니다. 공유 메모리 여덟 블록, 월드 상태 읽기, 이벤트 읽기, 명령 내리기, 회신, 레인 역할, 캔버스, UI와 입력. Python 이외의 언어로 연동하려면 이 페이지를 보세요.
런타임과 외부 프로그램은 공유 메모리로만 데이터를 주고받으며, 아래 내용이 그 전부입니다.
- 참조 구현은 Python의
sdk/python/w3world.py(읽기)와sdk/python/w3fast.py(쓰기)입니다. 모든 구조체의 크기와 오프셋이 여기에 정의되어 있으며, 테스트로 고정되어 있습니다. - 프로토콜은 의미만 기술하며, 게임 버전과 무관합니다. 게임 버전이 바뀌면 런타임이 알아서 맞추고 프로토콜은 그대로입니다. 새 필드는 블록 끝에만 추가하므로 기존 클라이언트도 그대로 동작합니다.
대부분은 이 페이지를 읽을 필요가 없습니다 — Python SDK를 쓰면 됩니다. C++ / C# / Rust / Go 같은 언어로 직접 연동하고 싶거나, SDK 아래에서 무슨 일이 일어나는지 알고 싶을 때만 필요합니다.
1. 공유 메모리 여덟 블록
<pid>는 게임 프로세스 ID입니다.
| 이름 | 방향 | 내용 | 동기화 방식 |
|---|---|---|---|
Local\War3World_<pid> | 런타임 → 클라이언트 | 월드 상태: 헤더 + 플레이어 16명 + 최대 1024개 유닛 + 256개 유닛 상세 정보 + 바닥 아이템 256개 + 확장 영역 + 생산 테이블 | seqlock |
Local\War3Trees_<pid> | 런타임 → 클라이언트 | 최대 4096개 파괴 가능 오브젝트(나무 등), 2초마다 갱신 | seqlock |
Local\War3Events_<pid> | 런타임 → 클라이언트 | 이벤트 링, 8192개 | 항목마다 시퀀스 번호 포함 |
Local\War3Map_<pid> | 런타임 → 클라이언트 | 맵: 지형 격자(한 칸 128, 최대 256×256) + 플레이 가능 영역 경계 + 시작 지점. 게임 시작 후 몇 초에 걸쳐 나눠서 계산 | seqlock(계산이 끝나면 더 이상 바뀌지 않음) |
Local\War3Fast_<pid> | 양방향 | 명령 레인: 16개 × 16슬롯. 슬롯마다 명령 하나 + 회신. 레인마다 역할이 있음 | 슬롯마다 단일 작성자·단일 독자 |
Local\War3Canvas_<pid> | 클라이언트 → 런타임 | 캔버스: 헤더 64바이트 + 요소 256개 × 112바이트 + 64 KB 텍스트 / 점 풀. canvas_enable을 한 번 보낸 뒤에야 생성됨 | seqlock(클라이언트가 쓰고, 런타임이 매 프레임 읽음) |
Local\War3Msgs_<pid> | 런타임 → 클라이언트 | 화면 메시지 링: 게임 안내, 채팅, 시스템 메시지의 전체 텍스트, 128개 × 256바이트 | 항목마다 시퀀스 번호 포함 |
Local\War3Input_<pid> | 양방향 | UI와 입력: 런타임이 마우스 위치, 가리키는 지면 지점, 호버 중인 항목을 되쓰고, 클라이언트는 단축키 표와 마우스 스위치를 씀. input_enable을 한 번 보낸 뒤에야 런타임이 입력을 넘겨받기 시작함 | 단축키 표는 seqlock |
여러 클라이언트가 캔버스와 입력을 동시에 쓸 때: 이 두 블록은 하나씩밖에 없어서, 각자 따로 쓰면 서로 덮어씁니다. 약속은 다음과 같으며, 클라이언트를 직접 작성할 때도 이대로 따라야 합니다.
- 캔버스: 이름 있는 뮤텍스
Local\War3CanvasMutex_<pid>를 잡은 채 읽기 - 수정 - 쓰기를 하며, 자기 요소만 바꾸고 다른 클라이언트의 요소는 그대로 둡니다(풀 오프셋은 다시 배치). 주인 프로세스가 이미 종료된 요소와 주인이 없는 요소는 지웁니다. 요소의reserved[1]= 주인 프로세스 ID,reserved[2]= 프로세스 내 일련번호이며, 요소 번호는 블록 헤더 오프셋 60의 카운터에서 할당합니다(0x10000부터). - 입력: 각 클라이언트는 자기 단축키와 마우스 스위치를
Local\War3InputClients_<pid>(헤더 16바이트 + 클라이언트 16개 × 528바이트)에 등록하고,Local\War3InputMutex_<pid>를 잡은 채 자기 항목을 고친 뒤, 살아 있는 클라이언트들의 것을 합쳐 입력 블록에 씁니다. 단축키는 “키 코드 + 보조 키” 기준으로 중복을 없애고, 마우스 스위치는 합집합을 취합니다. 이벤트는 모든 클라이언트에 보내며, 각자 “키 코드 + 보조 키”로 자기 단축키를 알아봅니다. 등록표에 살아 있는 다른 클라이언트가 남아 있으면input_enable 0을 보내지 마세요. - 런타임: 주인 프로세스가 종료된 클릭 가능 요소는 더 이상 클릭을 가로채지 않습니다. 2초마다 등록표를 확인해, 등록했던 클라이언트가 모두 종료되었으면 입력 블록의 단축키 표와 마우스 스위치를 0으로 지웁니다.
2. 월드 상태 읽기(seqlock)
loop:
s1 = block.seq (오프셋 8, int32)
if s1 이 홀수: 재시도 (런타임이 쓰는 중)
헤더 + players + units[unitCount] + details[detailCount] + items[itemCount] 복사
if block.seq != s1: 재시도
- 헤더: 발행 카운터(늘지 않으면 발행이 끊긴 것), 엔진 게임 시계, 게임마다 +1 되는 epoch, 자기 플레이어 번호, 게임 중인지 여부, 배속, 발행 주기, 이번 수집에 게임 스레드에서 걸린 마이크로초, 이벤트 시퀀스 번호, 구간별 소요 시간. 클라이언트는
requestedPeriodMs를 써서 발행 주기(16 ~ 1000 ms)를 요청할 수 있습니다. - 유닛(112바이트): 핸들 쌍(유닛은 핸들 쌍으로 식별하세요. 주소는 재사용됩니다), 종류 4자 코드, 소유자, 플래그, 좌표, 체력 / 마나(최대치 포함), 현재 오더 + 오더 대상, 작업 대상(실제로 공격 중인 대상), 영웅 레벨 / 경험치 / 스킬 포인트, 상세 정보 인덱스,
visibleTo(비트 p = 플레이어 p가 지금 이 유닛을 볼 수 있음). - 상세 정보(288바이트, 영웅 > 플레이어 유닛 > 크립, 최대 256개): 스킬 12개(코드 / 레벨 / 플래그 / 남은 쿨다운 초), 버프 코드 8개, 인벤토리 6칸.
- 블록 끝 확장(추가만 하고 앞쪽 오프셋은 옮기지 않으므로 기존 클라이언트도 그대로 동작): 확장 영역
EXT1(게임 내 시각, 낮밤 진행 속도, 생산 테이블 항목 수)과 생산 테이블prods[128](훈련 / 연구 / 건설 / 업그레이드 중인 건물, 대기열, 총 소요 시간, 경과 시간, 막힘 여부). magic이 일치할 때만 사용하세요.
3. 이벤트 읽기
head = ring.writeSeq (오프셋 8)
for seq in (cursor, head]:
e = ring.events[(seq - 1) % 8192]
if e.seq > seq: 하나를 놓침(너무 늦게 읽어서 덮어써짐)
elif e.seq != seq: 아직 쓰는 중, 다음에 다시 읽기
else: e 처리
이벤트 구조체는 64바이트입니다: seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra.
- 인접한 두 발행을 비교해 얻는 이벤트(정밀도 = 발행 주기):
unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started - 엔진 수준(런타임이 게임 스레드에서 발생 즉시 기록, 타격마다 발생):
damage(공격자, 피해 타입, 공격 타입, 위치, 실제로 깎인 체력, 방어력 적용 전 피해),killed(처치한 유닛) - 생산 테이블을 추적해 얻는 이벤트:
production.done(완료된 4자 코드, 종류, 걸린 게임 초. 상대의 것도 발생) - 런타임이 발행할 때마다 함께 확인하는 이벤트:
spell.cast(스킬 쿨다운 시작:a스킬 4자 코드,b레벨,value쿨다운 초,x/y시전 지점),player.left(a플레이어 번호,b새 슬롯 상태),selection.changed(로컬 플레이어의 선택. 전체 목록은 월드 블록 확장 영역에 있음),game.ended(게임에서 나감) - 화면 메시지:
message(a= 메시지 시퀀스 번호. 전체 텍스트는 공유 메모리Local\War3Msgs_<pid>에서 조회: 128개 × 256바이트, 게임 안내, 채팅, 시스템 메시지가 모두 들어 있음.b= 메시지 영역 번호) - UI와 입력(
input_enable을 켠 뒤):ui.click(a캔버스 항목 id,b1 왼쪽 버튼 / 2 오른쪽 버튼),ui.hover,hotkey(a단축키 id,b가상 키 코드),mouse.world(x/y지면 좌표,value= 1이면 가로챔). 보조 키는 모두extra에 있음
4. 명령 내리기
- 클라이언트 객체 하나가 레인 하나를 차지합니다:
Local\War3FastMutex_<pid>를 잡고 비어 있는(또는 주인 프로세스가 죽은) 레인을 찾아, 역할, 플레이어 번호, 자신의 pid를 씁니다. 한 프로세스에서 두 역할이 필요하면 레인을 두 개 엽니다. - 슬롯 채우기: 시맨틱 명령 플래그, 연산 코드,
args[11], 마감 시각deadlineMs. - 모든 슬롯을 다 쓴 뒤 제출 표시를 하고, 레인의
submitSeq를 1 늘립니다. Local\War3FastDone_<pid>_<lane>이벤트를 기다리고(또는 폴링), 회신을 읽은 뒤 슬롯을 반납합니다.
런타임은 게임 스레드의 이벤트 디스패치 안에서 명령을 모아 실행합니다. 한 번 비우는 데 쓰는 시간 예산은 4 ms(실제 고정밀 타이머)이며, 넘으면 나머지는 다음 디스패치로 넘깁니다. 마감 시각이 지난 슬롯은 더 이상 실행되지 않습니다 — “일시정지를 풀었더니 예전 명령이 다시 실행되는” 일은 생기지 않습니다.
args 인덱스: 0..2 유닛(주소, 핸들 lo, 핸들 hi), 3 오더 ID 또는 4자 코드, 4..6 대상, 7/8 x / y(float 비트), 9 extra(플레이어 번호 / 칸 번호 / 스위치 / 대기열 비트), 10 mode(0 대상 없음 / 1 지점 대상 / 2 유닛 대상).
연산 코드
| 연산 코드 | 이름 | 설명 |
|---|---|---|
| 1 | point | 유닛에게 지점 대상 명령(이동 / 공격 이동 / 순찰 / 지면 공격 / 지점 시전). extra bit0 = 대기열(현재 오더 뒤에 삽입) |
| 2 | target | 유닛에게 대상 지정 명령(우클릭 공격 / 채집 / 수리 / 대상 시전 / 아이템 줍기). 대상은 반드시 보여야 함 |
| 3 | immediate | 대상 없는 명령(정지 / 위치 사수 / 훈련 / 연구 / 업그레이드 / 대상 없는 시전) |
| 4 | build | 일꾼의 건물 건설(좌표는 32 단위로 정렬) |
| 5 | learn | 영웅의 스킬 습득 |
| 6 | use_item | 인벤토리 extra번째 칸 사용 |
| 7 | revive | 제단에서 영웅 부활 |
| 8 | rally | 집결 지점(지점 대상 / 유닛 대상) |
| 9 | buy | 상점이 옆에 있는 영웅에게 아이템 판매 |
| 10 | item_drop | 아이템을 손에서 놓기: 아군에게 주기, 상점에 팔기(code = 받는 유닛), 또는 바닥에 버리기 |
| 20 ~ 25 | 쿼리 | q_tech 기술 카운트, q_feasible 실행 가능성, q_visible 가시성, q_mine_gold 금광 잔량, q_captain 컴퓨터 대장, q_dead_heroes 사망 영웅 목록 |
| 30 | pause | 일시정지 / 재개 |
| 40 ~ 50 | 카메라 | 카메라 상태 읽기, 필드 설정, 한 지점 바라보기, 따라가기, 초기화, 회전, 경계, 부드러운 이동, UI 표시/숨김, 깨끗한 화면, 안개 |
| 60 ~ 63 | HUD | 퀘스트 버튼 텍스트, 퀘스트 패널 제목과 설명, 새로 고침, 패널이 열렸는지 읽기 |
| 70 | jass | JASS native를 이름으로 호출(1291개). 이름과 문자열 인수는 슬롯의 부가 영역에, 나머지 인수는 시그니처에 따라 args에 넣음. 반환값은 value[0]. 로컬 도구 레인 전용이며, 함수 인수를 받거나 스크립트 스레드를 일시 중단시킬 수 있는 native는 모두 거부. JASS 채널 참고 |
| 71 / 72 | jass_handle_of / jass_unit_of | 스냅샷 속 유닛 ↔ JASS 핸들 상호 변환(스냅샷의 핸들 쌍은 JASS 핸들이 아님) |
| 73 | canvas_enable | 캔버스 공유 메모리를 만들고 그리기 후크를 설치. 어떤 레인에서든 보낼 수 있음(캔버스는 로컬 화면에만 그림). 처음에는 후크를 설치해야 하므로 타임아웃을 2초 이상으로 |
| 74 | input_enable | extra = 1이면 게임 창의 입력(캔버스 항목 클릭 / 호버, 단축키, 지면 클릭)을 넘겨받고, 0이면 돌려줌. 입력 블록 Local\War3Input_<pid>: 헤더 128바이트 + 단축키 32개 × 16바이트. 클라이언트는 단축키 표와 마우스 스위치를 쓰고, 런타임은 마우스 위치, 가리키는 지면 지점, 호버 중인 항목을 되씀. 어떤 레인에서든 보낼 수 있음(로컬 입력에만 영향). UI와 입력 참고 |
5. 회신
회신은 52바이트(+ 소요 시간 8바이트)입니다: status, engineReturn, verdict(거부 원인 코드), orderBefore / orderAfter(같은 프레임에 다시 읽은 유닛 오더), value[8](쿼리 결과), execUs(이 명령이 게임 스레드에서 실행된 마이크로초), engineUs(그중 엔진의 명령 함수 자체가 걸린 시간).
모든 상태 코드와 원인 코드는 회신과 원인 코드를 참고하세요.
6. 레인 역할
| 역할 | 할 수 있는 것 |
|---|---|
dev | 로컬 도구: 시맨틱 명령(로컬 플레이어의 유닛 지휘) + JASS 채널 |
player | 시맨틱 명령만 가능하며, 레인이 속한 플레이어의 유닛만 지휘 가능(다른 플레이어의 유닛 = not_owner) |
observer | 쿼리, 카메라, HUD 패널 상태 읽기, 캔버스와 로컬 입력 켜기만 가능. 나머지는 모두 forbidden |
AI 두 개의 대결 = 한 게임에서 player 레인 두 개(player 0 / player 1)를 엽니다.
로컬 모드에서는 클라이언트가 역할을 스스로 선언합니다(약속일 뿐 보안 경계가 아닙니다). 아레나에서는 심판 프로세스가 레인을 만들고, 선수에게는 player 레인만 넘깁니다.
7. 실측으로 확인한 동작
- 적에게 우클릭(smart) = 그 대상 하나를 공격합니다(오더 대상, 작업 대상 모두 그 대상). 원시 공격 오더는 대상 명령을 거쳐도 공격 오더만 바뀌고 대상을 기억하지 않아, 근처의 다른 적을 공격하러 갑니다.
- 엔진은 보이지 않는 유닛에게 대상 명령을 허용하지 않습니다. 해가 진 뒤 먼 캠프가 안개에 들어가면 우클릭은 모두 거부됩니다(1001).
- 건설 “수락”은 일꾼이 명령을 받았다는 뜻일 뿐입니다. 숲 속 지점도 즉시 수락되고, 일꾼이 도착해서야 실패합니다. 명백히 점유된 지점은 즉시 거부됩니다.
- 영웅은 사망 후 약 3 게임 초가 지나야 부활할 수 있습니다. 인구가 부족해도 거부됩니다(영웅도 인구를 차지함).
- 인벤토리의 아이템은 바닥 아이템에 포함되지 않습니다. 주우면
item.removed가 발생합니다. - 일시정지 중에는 엔진 시계가 멈추지만, 명령은 평소처럼 내릴 수 있습니다.
- 최소화 상태로 띄운 게임은 시뮬레이션이 멈춰 있습니다(시계가 흐르지 않음).