네트워크 프로토콜
게임과 게임 서버가 주고받는 메시지예요. 봇, 서버 목록 사이트, 다른 클라이언트처럼 게임 밖에서 서버에 붙는 프로그램을 만들 때 보세요. 서버의 규칙이나 콘텐츠를 바꾸려면 플러그인을 보세요.
서버는 모든 행동을 스스로 검사해요(이동 속도, 벽, 손이 닿는 거리, 1초에 10번이 넘는 행동, 너무 고른 간격). 규칙에 안 맞는 것은 거절되고, 이상한 것은 계정에 기록돼요. 이 문서는 정상적인 클라이언트를 만들기 위한 것이에요.
연결
| 전송 | QUIC (UDP), 기본 포트 7777 |
| TLS | 1.3만, ALPN greenhaven |
| 인증서 | 서버가 처음 켜질 때 데이터 폴더에 만든 자체 서명 인증서. 처음 접속할 때 SHA-256을 기억해 두고(핀) 다음부터 다르면 끊으세요. 서버가 켜질 때 콘솔에 찍혀요. |
| 메시지 | 클라이언트가 여는 양방향 스트림 하나. 프레임은 [길이 u32 리틀 엔디언][JSON], 한 메시지 최대 128 KB |
| 데이터그램 | QUIC 데이터그램. 위치와 음성처럼 최신 것만 쓸모 있는 것. 첫 바이트가 종류, 숫자는 모두 리틀 엔디언 |
연결 후 10초 안에 스트림을 열고 첫 메시지를 보내야 해요. 첫 메시지는 info, manage, join 중 하나예요.
서버 정보 (info)
접속하지 않고 서버 목록에 보일 것만 물어요. 답을 보내고 서버가 연결을 닫아요.
{"t": "info"}
{
"t": "info",
"name": "",
"motd": "",
"players": 3,
"max": 20,
"version": "<규칙 버전>",
"protocol": 2,
"game": "0.1.0",
"password": false,
"plugins": [],
"host": false,
"files": [{"kind": "pack", "size": 1048576}],
"tags": [],
"region": "",
"scopes": []
}
game: 서버가 받는 게임 버전,protocol: 메시지 버전files: 접속하면 받게 될 파일의 종류와 크기만(이름과 해시는 접속 뒤에)host: 플레이어 게임의 자체 서버(접속하면 그 농장의 일꾼)scopes: 서버의 플러그인이 쓰는 바깥 기능
접속 (join)
{
"t": "join",
"name": "닉네임",
"secret": "<계정 비밀>",
"game": "0.1.0",
"proto": 2,
"version": "<규칙 버전>",
"ticket": "<중앙 서버 티켓>",
"lang": "ko",
"tz": 540,
"reg": "<가진 레지스트리 해시>"
}
| 필드 | 뜻 |
|---|---|
name, secret | 계정. 처음 보는 이름이면 그 비밀로 계정이 만들어지고, 다음부터는 비밀이 맞아야 해요. 이름은 1-32자, @로 시작 못 해요 (온라인 모드에서는 중앙 서버의 계정 id) |
game, proto | 게임 버전과 프로토콜 버전. 게임 버전이 서버와 다르면 proto가 같을 때만 들어가요. game을 안 보내면 version(규칙 버전)이 같아야 해요 |
ticket | 온라인 모드 서버(server.properties online-mode가 false가 아닌 곳)는 공식 중앙 서버가 이 서버의 인증서 핀에 맞춰 발급한 티켓을 확인해요 |
password | 서버 비밀번호(SHA-256 16진수, 또는 그대로) |
fresh | 새 농장으로 시작 (농장 자리는 들어간 뒤 site로 골라요: 행동) |
seed | 게임 자체 서버에서만: 새 월드의 씨앗 (새 월드 만들기에서 친 것) |
lang, tz | 언어, 시간대(UTC에서 동쪽으로 분) |
reg | 가지고 있는 레지스트리의 해시. 서버 것과 다를 때만 registry가 와요 |
peek | 로비용: 친구 소식만 받고 누구에게도 접속 중으로 안 보임 ({"t":"peeked","name","code"} 다음 친구 목록) |
플러그인이 접속 전에 물을 것이 있으면 {"t":"prompt","gui":{...}}가 오고, {"t":"prompt_answer","gui","button","inputs":{}}로 답해요.
성공:
{
"t": "joined",
"r": 0,
"packs": [],
"reg_hash": "...",
"registry": {},
"scopes": [],
"now": 1761955200000
}
r: 서버가 마지막으로 받은 행동 번호. 월드와 게임 전체는joined에 없고 바로 뒤의enter로 와요packs: (있을 때만) 받을 파일[{"name","size","sha","kind":"pack"|"plugin","title","ver"}]registry: 서버의 게임 콘텐츠(규칙 JSON, 플러그인 패치 포함). 가진 것(reg)과reg_hash가 다를 때만 와요.reg_hash로 저장해 두세요scopes: 서버의 플러그인이 쓰는 바깥 기능now: 서버 시계(유닉스 ms). 행동과 위치의 시각은 이 시계로 찍어요
이어서 friends(친구 목록)와 {"t":"enter","world","look","station","terrain","state"}(들어간 월드와 게임 전체: 아래 상태; look: 보기만 할 수 있음, station: 걷기가 열기구 정거장에서 시작, terrain: 땅의 씨앗)가 와요. enter는 그 월드의 아침(그때는 "summary":[[id,n,값]...], "total": 밤사이 정산), 월드가 통째로 바뀔 때, 플러그인이 다른 월드로 옮길 때(Player::move_world)도 와요.
서버의 월드는 모두가 함께 쓰는 하나(world)뿐이고, 플러그인이 만든 월드만 더 있어요.
거절: {"t":"rejected","reason":"..."}. 덧붙는 것: "update":true,"game":"<서버 버전>"(게임을 업데이트해야 함), "password":true(비밀번호 틀림). 다른 곳에서 같은 계정으로 접속하면 먼저 연결에 rejected가 가요. 서버가 내보내면 {"t":"kicked","reason"}.
파일 받기
{"t": "pack", "name": "<이름>", "off": 0}
{"t": "pack", "name": "<이름>", "off": 0, "size": 123456, "d": "<base64, 최대 48 KB>"}
off를 늘려가며 받고 sha(SHA-256)로 확인하세요. 리소스 팩은 미디어(PNG, JPG, WebP, OGG, WAV, GLB)만 들어 있는 zip이에요. 실행되는 것은 없어요.
행동 (act)
게임 안에서 한 일은 일어날 때마다 하나씩 보내요. r은 1씩 늘어나요. 이미 받은 번호(마지막 이하)를 다시 보내면 ok가 다시 오고 두 번 적용되지 않아요.
{
"t": "act",
"r": 12,
"e": {
"k": "till",
"t": 1761955200000,
"d": 3,
"m": 420,
"x": 10.5,
"z": 4.5,
"c": [10, 4],
"tool": "hoe"
}
}
e 필드 | 뜻 |
|---|---|
k | 종류 (아래) |
t | 시각(서버 시계 유닉스 ms) |
d, m | 게임 날짜, 하루의 분(6:00 = 360) |
x, z | 플레이어 위치(셀) |
c | 대상 셀 [x, z] |
id, n, a | 물건 id, 개수, 동물·주민·장비 칸·마법 부여 |
tool, why | 쓴 도구, 이유(행동마다 다름) |
pw | cast: 던지는 힘 0-100 |
ch | till / water: 모아 쓰기 단계 |
fx, fz | strike: 바라본 방향 |
inp, ticks, hk | fish: 릴 게임에서 버튼이 바뀐 틱, 플레이한 틱 수, 입질부터 챔질까지 ms |
vals | plugin: 플러그인 패널 입력값 |
종류: till water plant fertilize harvest hit strike cast fish forage item pickup discard eat use buy sell ship craft place unplace store take furnish unfurnish equip unequip upgrade smith_pick enchant perk talk gift answer words request daily quest giveup mail read_mail animal pet feed product collect release sapling fruit door enter mine gate build enlarge donate bundle love festival keg preserves_jar bee_house crab_pot skin_buy skin_use tutorial plugin gui button command sleep day. 서버가 플러그인과 규칙에 따라 판정하니 값은 레지스트리를 보세요.
답:
{"t":"ok","r":12,"res":{...},"s":{...}}: 받아들임.res는 클라이언트가 알 수 없는 결과(부서진 것의drops, 채집n, 낚시cast의levelbitereel,fish의id("": 놓침),strike의hits[[몬스터, 남은 hp]], 땅에 떨어진 것g).s는 바뀐 플레이어 상태의 키만{"t":"no","r","why","state"}: 거절, 그 행동은 일어나지 않았어요.state(서버가 가진 상태)를 그대로 받아들이세요. 행동이 너무 빨라 거절될 때는state가 없어요- 하루는 플레이어 한 사람이 끝내지 않아요: 월드의 모두가 잠들거나 하루의 끝 시각이면 월드의 하루가 끝나고, 아침은
enter(summarytotal포함)로 와요 - 집터 고르기:
{"t":"site","at":[x,y,z]}→{"t":"site","state"}
상태
state는 한 플레이어의 게임 전체예요: day m weather weather_next gold energy max_energy health max_health bag layout tiles trees debris forage animals shipped xp levels perks gear placed chests equip quests friendship mail 등, 그리고 town(서버가 정한 마을: 셀, 부지, 구조물, 장소). 모르는 키는 무시하세요. 키는 플러그인과 버전에 따라 늘어나요.
서버가 보내는 것
| 메시지 | 언제 |
|---|---|
{"t":"clock","d","m"} | 2초마다 월드 시계. 클라이언트 시계를 맞추세요 |
{"t":"world","d","m"} | 같은 월드의 누군가가 바꾼 것 |
{"t":"blocks","l":[[x,y,z,블록]]} | 블록이 바뀜 |
{"t":"back","why","x","z","y"} | 이동이 거절됨: 그 자리로 돌아가세요 (y: 마지막으로 통과한 자리의 높이, 날던 중이면 그 높이로) |
{"t":"hurt","s","n","health"} | 몬스터에게 맞음 |
{"t":"fell","state","hold"} | 쓰러짐(진료소에서 일어남). hold면 {"t":"respawn","state"}까지 기다림 |
{"t":"says","l":[[주민, 말, 지연]]} | 주민의 말 |
{"t":"fx","list":[[종류,이름,x,y,z]]} | 다른 사람 행동의 효과음과 파티클 |
{"t":"ents","e"} | 플러그인 엔티티 |
{"t":"pdata","d"} | 플러그인의 플레이어 값(바) |
{"t":"gui","g"} | 플러그인 패널 |
{"t":"ext","k","v"} | 플러그인 API 메시지 (예: world.time) |
{"t":"beds","all","s","ids"} | 월드에 누가 있는지(그래서 누가 들어오고 나갔는지)와 그중 누가 자고 있는지, ids는 이름 → 데이터그램 6이 쓰는 번호 |
{"t":"chat","name","text"} | 채팅 (보낸 사람에게도) |
그 밖의 클라이언트 메시지
| 보냄 | 답 |
|---|---|
{"t":"chunk","k":[x,z]} | {"t":"chunk","k","c"} (청크) |
{"t":"chat","text"} | 1-200자, 5초에 5줄까지. 넘으면 버려져요 |
{"t":"layout","layout"} | 가방 순서와 선택 칸 (최대 64 KB) |
{"t":"look","look"} | 모습(species, skin, hat, glasses, back, body). 온라인 모드에서는 중앙 서버의 것이 쓰여요 |
{"t":"looks","names":[]} | {"t":"looks","looks":{이름:모습},"held":{이름:모델}} (한 번에 32명) |
{"t":"held","m","slot","id"} | 손에 든 것의 모델 이름("": 없음), 단축바에서 고른 칸, 그 칸의 아이템. 다른 사람에게 {"t":"held","name","m"} |
{"t":"mode","m"} | 게임 쪽 코드가 정한 이동 모드 ("" 게임 그대로; 플러그인의 PlayerMove mode) |
{"t":"line","at":[x,y,z]} | 낚시찌 위치 (null: 감음). 다른 사람에게 {"t":"line","name","at"} |
{"t":"status","name"} | {"t":"status","name","online"} (친구만 true) |
{"t":"pause","on"} | 게임 자체 서버에서 혼자일 때만: 월드 시계 멈춤 |
{"t":"plugin_settings"} / {"t":"plugin_setting","plugin","key","value"} | 플러그인 설정 목록 / 바꾸기 (게임 자체 서버의 주인만) |
친구
중앙 서버를 쓰는 서버에서는 친구 목록이 중앙 서버의 것이에요. 아래는 online-mode=false 서버의 자체 친구예요.
friend (name, yes?), friend_find (q), friend_cancel, unfriend, block (undo), friend_note (memo, fav), friend_policy (all / played / none), friends. 서버는 friends (전체 목록), friend_status, friend_ask, friend_found, friend(why: 거절)를 보내요.
데이터그램
클라이언트 → 서버
| 종류 | 형식 | |
|---|---|---|
| 1 | [1][walk u16][x f64][y f32][z f64][yaw f32][doing u8][t i64 ms][flags u8] (37바이트) | 플레이어 위치. 주변에 누가 있으면 초당 30번, 아니면 5번, 서 있으면 1초에 한 번 |
| 2 | [2][Opus 프레임] (최대 1024바이트) | 음성: 같은 월드 48 m 안의 사람에게만 전해져요(게임은 40 m까지 들려줘요). 1초에 60개까지 |
| 11 | [11][플러그인 이름 길이 u8][플러그인 이름][채널 길이 u8][채널][데이터] (최대 1200바이트) | 플러그인 게임 쪽 코드의 빠른 패킷: 그 플러그인의 FastPacket에만. 1초에 60개까지 |
위치 하나로 세 가지를 해요. 같은 월드의 다른 사람에게 전해지고, 서버가 걷기를 검사하고(벽, 속도, 물, 공중: 안 되면 back), 서버의 몬스터가 검사를 통과한 자리를 쫓아요. 걷기 검사는 t 기준 0.19초에 한 번까지만 해요. 좌표는 지금 있는 월드의 셀(1 m). walk는 새 걷기(부활, 순간이동)마다 늘리는 번호. t는 클라이언트 시각 (유닉스 ms). flags는 1 = 지금은 걷기 검사 안 함(문 응답 대기, 도구 동작, 열기구, 쓰러짐), 2 = 쓰러짐(몬스터가 안 쫓음). doing은 하위 7비트가 0 서 있음, 1 걷기, 2 달리기, 3 잠, 4 도구, 5 수영, 6 점프, 7 낙하, 8 입질, 9 릴 감기, 최상위 비트는 도구 동작을 시작할 때마다 뒤집어요. 프로토콜 2부터 (그 전엔 1, 3, 4번이 따로였어요).
서버 → 클라이언트
| 종류 | 형식 |
|---|---|
| 6 | [6][개수 u8] 다음 각 [id u16][ms u32][x i32][y f32][z i32][yaw u8][doing u8] (20바이트): 같은 월드의 다른 사람들 (16 ms마다 묶어서, 최대 1200바이트). id는 그 월드 안 번호(beds의 ids가 이름을 알려 줘요), ms는 보낸 쪽 시각의 하위 32비트(순서용), x/z는 칸의 1/256 단위, yaw는 256분의 1바퀴. 받는 사람마다 거리에 따라: 10 m 안은 올 때마다(그 사람에게 가장 가까운 10명만; 더 붐비면 한 단계 아래), 25 m 안은 초당 15번, 50 m 안은 10번, 150 m 안은 5번, 그 밖은 1초에 한 번(지도·근처 목록용). 그 사람에게 마지막으로 보낸 것과 위치·높이·방향·동작이 같으면(서 있으면) 거리와 상관없이 1초에 한 번 |
| 2 | [2][이름 길이 u8][이름][Opus]: 48 m 안에서 말한 음성 (바로) |
| 3 | [3][개수 u8][층 u32] 다음 각 [serial u16][kind u8][tier u8][hp u16][x f32][z f32]: 몬스터 (층 0 = 지상; 한 데이터그램에 81마리까지) |
| 5 | [5][개수 u8][all u8] 다음 각 [n u8][x f32][z f32][yaw u8][place u8][work u8][flags u8][in_for u8][act u8][acts u8][with u8]: 주민. all 1이면 전체 목록(1초마다; 여러 개로 나뉘면 첫 것만 1), 0이면 그 뒤로 바뀐 것만. 바뀐 게 없으면 안 와요. 광산·던전에 있는 사람에게는 전체 목록만 가요 |
| 7 | [7][개수 u8][all u8] 다음 각 [id u32][x f32][z f32][flags u8]: 농장 동물, 5와 같은 방식(all 1이면 목록을 비우고 다시 채우세요) |
| 12 | [12][플러그인 이름 길이 u8][플러그인 이름][채널 길이 u8][채널][데이터]: 플러그인 서버 쪽의 빠른 패킷 (Hud::send_fast) |
- 데이터그램은 잃어버릴 수 있어요. 보이는 사람은 1초마다 다시 오니 3초 동안 소식 없는 사람은 지우세요
- 3(몬스터)은 목록이 비면 한 번, 그 뒤로는 1초에 한 번만 와요(비어 있지 않으면 초당 10번)
- 5의
yaw는 256분의 1바퀴,flags는1 움직임, 2 달림, 4 실내, 8 숨김,place/in_for는 레지스트리 장소 번호 - 7의
id는 동물 id의 FNV-1a 32비트,flags는1 걷기, 2 누움(밤), 4 풀 뜯기, 8 먹기
청크
월드는 32 x 32 칸, 높이 64칸(y -8부터) 블록 청크예요. 서버는 {"t":"chunk","k","z","n"}로 답해요: z는 아래 바이트를 zlib(deflate)로 눌러 base64로 적은 것, n은 누르기 전 크기(평균 10 KB가 650 B쯤으로 줄어요). 요청은 플레이어가 있는 청크(마지막으로 보낸 위치)에서 8청크 안만, 초당 60개까지 받아요(그 밖은 읽지 않고 버려요: 게임이 3초 뒤 다시 물어요). 누른 바이트는:
- 1024개 열(x가 빠르게, 그다음 z). 각 열은
[런 개수 u8]다음[블록 u8, 개수 u8]쌍, y -8부터 위로. 마지막 런 위는 공기 - 나머지는 MessagePack으로 된 소품 배열:
{"m": 모델 res:// 경로, "x","y","z","size","yaw", "solid": [반지름, 높이] 또는 null}
블록: 0 air, 1 grass, 2 sand, 3 snow, 4 stone, 5 dirt, 6 road, 7 bed(물 밑 모래), 8 water. 그 위의 번호는 플러그인 블록이에요. 블록 y는 y-0.5부터 y+0.5까지 차지해요.
관리 (manage)
24시간 서버의 주인이 접속 없이 설정을 묻거나 바꿔요. 중앙 서버 티켓으로 주인을 확인해요.
{
"t": "manage",
"name": "닉네임",
"ticket": "<중앙 서버 티켓>",
"props": {"motd": "어서 오세요"}
}
{
"t": "manage",
"owner": true,
"props": {},
"running": {},
"restart": false,
"players": 2,
"saved": null,
"online": ["닉네임"],
"started": 1761955200000,
"log": [[1761955200000, "닉네임 들어옴"]],
"bans": [],
"plugins": [],
"plan_plugins": []
}
owner: 주인이 아니면false만 와요props: 저장된 설정(다음 시작에 쓰임),running: 지금 쓰이는 설정,restart: 둘이 달라 다시 시작해야 함props대신op로kick,ban,unban, 플러그인 켜기/끄기 같은 일을 시켜요